mirror of
https://git.eden-emu.dev/eden-emu/eden
synced 2026-06-29 05:15:24 +02:00
[desktop] Rework game list to use MVP architecture (#4042)
Closes #3480 moves the game list model/worker/private stuff to qt_common for later use in QML - `qt_common/game_list/model.{cpp,h}` is the model - `yuzu/game/game_{grid,tree}.*` are the views - `yuzu/game/game_list.cpp` is the presenter This was done very lazily in a manner that "works" while largely maintaining existing structure as much as possible. Most of it is copy-paste, with some bonus reworks/cleanups thrown in. Signed-off-by: crueter <crueter@eden-emu.dev> Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4042 Reviewed-by: MaranBr <maranbr@eden-emu.dev> Reviewed-by: Lizzie <lizzie@eden-emu.dev>
This commit is contained in:
parent
27189f39d2
commit
cc8451f764
25 changed files with 1307 additions and 1045 deletions
|
|
@ -31,6 +31,10 @@ add_library(qt_common STATIC
|
|||
abstract/frontend.h abstract/frontend.cpp
|
||||
abstract/progress.h abstract/progress.cpp
|
||||
|
||||
game_list/model.h game_list/model.cpp
|
||||
game_list/worker.h game_list/worker.cpp
|
||||
game_list/game_list_p.h
|
||||
|
||||
qt_string_lookup.h
|
||||
qt_compat.h
|
||||
|
||||
|
|
|
|||
399
src/qt_common/game_list/game_list_p.h
Normal file
399
src/qt_common/game_list/game_list_p.h
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2015 Citra Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QFileInfo>
|
||||
#include <QObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QStandardItem>
|
||||
#include <QString>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "common/logging.h"
|
||||
#include "common/string_util.h"
|
||||
#include "frontend_common/play_time_manager.h"
|
||||
#include "qt_common/config/uisettings.h"
|
||||
#include "qt_common/qt_common.h"
|
||||
|
||||
enum class GameListItemType {
|
||||
Game = QStandardItem::UserType + 1,
|
||||
CustomDir = QStandardItem::UserType + 2,
|
||||
SdmcDir = QStandardItem::UserType + 3,
|
||||
UserNandDir = QStandardItem::UserType + 4,
|
||||
SysNandDir = QStandardItem::UserType + 5,
|
||||
AddDir = QStandardItem::UserType + 6,
|
||||
Favorites = QStandardItem::UserType + 7,
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(GameListItemType);
|
||||
|
||||
/**
|
||||
* Gets the default icon (for games without valid title metadata)
|
||||
* @param size The desired width and height of the default icon.
|
||||
* @return QPixmap default icon
|
||||
*/
|
||||
static QPixmap GetDefaultIcon(u32 size) {
|
||||
QPixmap icon(size, size);
|
||||
icon.fill(Qt::transparent);
|
||||
return icon;
|
||||
}
|
||||
|
||||
class GameListItem : public QStandardItem {
|
||||
|
||||
public:
|
||||
// used to access type from item index
|
||||
static constexpr int TypeRole = Qt::UserRole + 1;
|
||||
static constexpr int SortRole = Qt::UserRole + 2;
|
||||
GameListItem() = default;
|
||||
explicit GameListItem(const QString& string) : QStandardItem(string) {
|
||||
setData(string, SortRole);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A specialization of GameListItem for path values.
|
||||
* This class ensures that for every full path value it holds, a correct string representation
|
||||
* of just the filename (with no extension) will be displayed to the user.
|
||||
* If this class receives valid title metadata, it will also display game icons and titles.
|
||||
*/
|
||||
class GameListItemPath : public GameListItem {
|
||||
public:
|
||||
static constexpr int TitleRole = SortRole + 1;
|
||||
static constexpr int FullPathRole = SortRole + 2;
|
||||
static constexpr int ProgramIdRole = SortRole + 3;
|
||||
static constexpr int FileTypeRole = SortRole + 4;
|
||||
|
||||
GameListItemPath() = default;
|
||||
GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data,
|
||||
const QString& game_name, const QString& game_type, u64 program_id,
|
||||
u64 play_time, const QString& patch_versions) {
|
||||
setData(type(), TypeRole);
|
||||
setData(game_path, FullPathRole);
|
||||
setData(game_name, TitleRole);
|
||||
setData(qulonglong(program_id), ProgramIdRole);
|
||||
setData(game_type, FileTypeRole);
|
||||
|
||||
const auto readable_play_time =
|
||||
play_time > 0 ? QObject::tr("Play Time: %1")
|
||||
.arg(QString::fromStdString(
|
||||
PlayTime::PlayTimeManager::GetReadablePlayTime(play_time)))
|
||||
: QObject::tr("Never Played");
|
||||
|
||||
const auto enabled_update = [patch_versions]() -> QString {
|
||||
const QStringList lines = patch_versions.split(QLatin1Char('\n'));
|
||||
const QRegularExpression regex{QStringLiteral(R"(^Update \(([0-9\.]+)\))")};
|
||||
for (const QString& line : std::as_const(lines)) {
|
||||
const auto match = regex.match(line);
|
||||
if (match.hasMatch() && match.hasCaptured(1))
|
||||
return QObject::tr("Version: %1").arg(match.captured(1));
|
||||
}
|
||||
return QObject::tr("Version: 1.0.0");
|
||||
}();
|
||||
|
||||
const auto tooltip = QStringLiteral("%1\n%2").arg(readable_play_time, enabled_update);
|
||||
|
||||
setData(tooltip, Qt::ToolTipRole);
|
||||
|
||||
const u32 size = UISettings::values.game_icon_size.GetValue();
|
||||
|
||||
QPixmap picture;
|
||||
if (!picture.loadFromData(picture_data.data(), static_cast<u32>(picture_data.size()))) {
|
||||
picture = GetDefaultIcon(size);
|
||||
}
|
||||
picture = picture.scaled(size, size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
|
||||
setData(picture, Qt::DecorationRole);
|
||||
}
|
||||
|
||||
int type() const override {
|
||||
return static_cast<int>(GameListItemType::Game);
|
||||
}
|
||||
|
||||
QVariant data(int role) const override {
|
||||
if (role == Qt::DisplayRole || role == SortRole) {
|
||||
std::string filename;
|
||||
Common::SplitPath(data(FullPathRole).toString().toStdString(), nullptr, &filename,
|
||||
nullptr);
|
||||
|
||||
const std::array<QString, 4> row_data{{
|
||||
QString::fromStdString(filename),
|
||||
data(FileTypeRole).toString(),
|
||||
QString::fromStdString(fmt::format("0x{:016X}", data(ProgramIdRole).toULongLong())),
|
||||
data(TitleRole).toString(),
|
||||
}};
|
||||
|
||||
const auto& row1 = row_data.at(UISettings::values.row_1_text_id.GetValue());
|
||||
// don't show row 2 on grid view
|
||||
switch (UISettings::values.game_list_mode.GetValue()) {
|
||||
|
||||
case Settings::GameListMode::TreeView: {
|
||||
const int row2_id = UISettings::values.row_2_text_id.GetValue();
|
||||
|
||||
if (role == SortRole) {
|
||||
return row1.toLower();
|
||||
}
|
||||
|
||||
// None
|
||||
if (row2_id == 4) {
|
||||
return row1;
|
||||
}
|
||||
|
||||
const auto& row2 = row_data.at(row2_id);
|
||||
|
||||
if (row1 == row2) {
|
||||
return row1;
|
||||
}
|
||||
|
||||
return QStringLiteral("%1\n %2").arg(row1, row2);
|
||||
}
|
||||
case Settings::GameListMode::GridView:
|
||||
return row1;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return GameListItem::data(role);
|
||||
}
|
||||
};
|
||||
|
||||
class GameListItemCompat : public GameListItem {
|
||||
Q_DECLARE_TR_FUNCTIONS(GameListItemCompat)
|
||||
public:
|
||||
static constexpr int CompatNumberRole = SortRole;
|
||||
GameListItemCompat() = default;
|
||||
explicit GameListItemCompat(const QString& compatibility) {
|
||||
setData(type(), TypeRole);
|
||||
|
||||
struct CompatStatus {
|
||||
QString color;
|
||||
const char* text;
|
||||
const char* tooltip;
|
||||
};
|
||||
// clang-format off
|
||||
const auto ingame_status =
|
||||
CompatStatus{QStringLiteral("#f2d624"), QT_TR_NOOP("Ingame"), QT_TR_NOOP("Game starts, but crashes or major glitches prevent it from being completed.")};
|
||||
static const std::map<QString, CompatStatus> status_data = {
|
||||
{QStringLiteral("0"), {QStringLiteral("#5c93ed"), QT_TR_NOOP("Perfect"), QT_TR_NOOP("Game can be played without issues.")}},
|
||||
{QStringLiteral("1"), {QStringLiteral("#47d35c"), QT_TR_NOOP("Playable"), QT_TR_NOOP("Game functions with minor graphical or audio glitches and is playable from start to finish.")}},
|
||||
{QStringLiteral("2"), ingame_status},
|
||||
{QStringLiteral("3"), ingame_status}, // Fallback for the removed "Okay" category
|
||||
{QStringLiteral("4"), {QStringLiteral("#FF0000"), QT_TR_NOOP("Intro/Menu"), QT_TR_NOOP("Game loads, but is unable to progress past the Start Screen.")}},
|
||||
{QStringLiteral("5"), {QStringLiteral("#828282"), QT_TR_NOOP("Won't Boot"), QT_TR_NOOP("The game crashes when attempting to startup.")}},
|
||||
{QStringLiteral("99"), {QStringLiteral("#000000"), QT_TR_NOOP("Not Tested"), QT_TR_NOOP("The game has not yet been tested.")}},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
auto iterator = status_data.find(compatibility);
|
||||
if (iterator == status_data.end()) {
|
||||
LOG_WARNING(Frontend, "Invalid compatibility number {}", compatibility.toStdString());
|
||||
return;
|
||||
}
|
||||
const CompatStatus& status = iterator->second;
|
||||
setData(compatibility, CompatNumberRole);
|
||||
setText(tr(status.text));
|
||||
setToolTip(tr(status.tooltip));
|
||||
setData(QtCommon::CreateCirclePixmapFromColor(status.color), Qt::DecorationRole);
|
||||
}
|
||||
|
||||
int type() const override {
|
||||
return static_cast<int>(GameListItemType::Game);
|
||||
}
|
||||
|
||||
bool operator<(const QStandardItem& other) const override {
|
||||
return data(CompatNumberRole).value<QString>() <
|
||||
other.data(CompatNumberRole).value<QString>();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A specialization of GameListItem for size values.
|
||||
* This class ensures that for every numerical size value it holds (in bytes), a correct
|
||||
* human-readable string representation will be displayed to the user.
|
||||
*/
|
||||
class GameListItemSize : public GameListItem {
|
||||
public:
|
||||
static constexpr int SizeRole = SortRole;
|
||||
|
||||
GameListItemSize() = default;
|
||||
explicit GameListItemSize(const qulonglong size_bytes) {
|
||||
setData(type(), TypeRole);
|
||||
setData(size_bytes, SizeRole);
|
||||
}
|
||||
|
||||
void setData(const QVariant& value, int role) override {
|
||||
// By specializing setData for SizeRole, we can ensure that the numerical and string
|
||||
// representations of the data are always accurate and in the correct format.
|
||||
if (role == SizeRole) {
|
||||
qulonglong size_bytes = value.toULongLong();
|
||||
GameListItem::setData(QtCommon::ReadableByteSize(size_bytes), Qt::DisplayRole);
|
||||
GameListItem::setData(value, SizeRole);
|
||||
} else {
|
||||
GameListItem::setData(value, role);
|
||||
}
|
||||
}
|
||||
|
||||
int type() const override {
|
||||
return static_cast<int>(GameListItemType::Game);
|
||||
}
|
||||
|
||||
/**
|
||||
* This operator is, in practice, only used by the TreeView sorting systems.
|
||||
* Override it so that it will correctly sort by numerical value instead of by string
|
||||
* representation.
|
||||
*/
|
||||
bool operator<(const QStandardItem& other) const override {
|
||||
return data(SizeRole).toULongLong() < other.data(SizeRole).toULongLong();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* GameListItem for Play Time values.
|
||||
* This object stores the play time of a game in seconds, and its readable
|
||||
* representation in minutes/hours
|
||||
*/
|
||||
class GameListItemPlayTime : public GameListItem {
|
||||
public:
|
||||
static constexpr int PlayTimeRole = SortRole;
|
||||
|
||||
GameListItemPlayTime() = default;
|
||||
explicit GameListItemPlayTime(const qulonglong time_seconds) {
|
||||
setData(time_seconds, PlayTimeRole);
|
||||
}
|
||||
|
||||
void setData(const QVariant& value, int role) override {
|
||||
qulonglong time_seconds = value.toULongLong();
|
||||
GameListItem::setData(
|
||||
QString::fromStdString(PlayTime::PlayTimeManager::GetReadablePlayTime(time_seconds)),
|
||||
Qt::DisplayRole);
|
||||
GameListItem::setData(value, PlayTimeRole);
|
||||
}
|
||||
|
||||
bool operator<(const QStandardItem& other) const override {
|
||||
return data(PlayTimeRole).toULongLong() < other.data(PlayTimeRole).toULongLong();
|
||||
}
|
||||
};
|
||||
|
||||
class GameListDir : public GameListItem {
|
||||
public:
|
||||
static constexpr int GameDirRole = Qt::UserRole + 2;
|
||||
|
||||
explicit GameListDir(UISettings::GameDir& directory,
|
||||
GameListItemType dir_type_ = GameListItemType::CustomDir)
|
||||
: dir_type{dir_type_} {
|
||||
setData(type(), TypeRole);
|
||||
|
||||
UISettings::GameDir* game_dir = &directory;
|
||||
setData(QVariant(UISettings::values.game_dirs.indexOf(directory)), GameDirRole);
|
||||
|
||||
const int icon_size = UISettings::values.folder_icon_size.GetValue();
|
||||
switch (dir_type) {
|
||||
case GameListItemType::SdmcDir:
|
||||
setData(
|
||||
QIcon::fromTheme(QStringLiteral("sd_card"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
setData(QObject::tr("Installed SD Titles"), Qt::DisplayRole);
|
||||
break;
|
||||
case GameListItemType::UserNandDir:
|
||||
setData(
|
||||
QIcon::fromTheme(QStringLiteral("chip"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
setData(QObject::tr("Installed NAND Titles"), Qt::DisplayRole);
|
||||
break;
|
||||
case GameListItemType::SysNandDir:
|
||||
setData(
|
||||
QIcon::fromTheme(QStringLiteral("chip"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
setData(QObject::tr("System Titles"), Qt::DisplayRole);
|
||||
break;
|
||||
case GameListItemType::CustomDir: {
|
||||
const QString path = QString::fromStdString(game_dir->path);
|
||||
const QString icon_name =
|
||||
QFileInfo::exists(path) ? QStringLiteral("folder") : QStringLiteral("bad_folder");
|
||||
setData(QIcon::fromTheme(icon_name).pixmap(icon_size).scaled(
|
||||
icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
setData(path, Qt::DisplayRole);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int type() const override {
|
||||
return static_cast<int>(dir_type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to prevent automatic sorting between folders and the addDir button.
|
||||
*/
|
||||
bool operator<(const QStandardItem& other) const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
GameListItemType dir_type;
|
||||
};
|
||||
|
||||
class GameListAddDir : public GameListItem {
|
||||
public:
|
||||
explicit GameListAddDir() {
|
||||
setData(type(), TypeRole);
|
||||
|
||||
const int icon_size = UISettings::values.folder_icon_size.GetValue();
|
||||
|
||||
setData(QIcon::fromTheme(QStringLiteral("list-add"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
setData(QObject::tr("Add New Game Directory"), Qt::DisplayRole);
|
||||
}
|
||||
|
||||
int type() const override {
|
||||
return static_cast<int>(GameListItemType::AddDir);
|
||||
}
|
||||
|
||||
bool operator<(const QStandardItem& other) const override {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
class GameListFavorites : public GameListItem {
|
||||
public:
|
||||
explicit GameListFavorites() {
|
||||
setData(type(), TypeRole);
|
||||
|
||||
const int icon_size = UISettings::values.folder_icon_size.GetValue();
|
||||
|
||||
setData(QIcon::fromTheme(QStringLiteral("star"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
setData(QObject::tr("Favorites"), Qt::DisplayRole);
|
||||
}
|
||||
|
||||
int type() const override {
|
||||
return static_cast<int>(GameListItemType::Favorites);
|
||||
}
|
||||
|
||||
bool operator<(const QStandardItem& other) const override {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
303
src/qt_common/game_list/model.cpp
Normal file
303
src/qt_common/game_list/model.cpp
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <QDir>
|
||||
#include <QIcon>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QThreadPool>
|
||||
|
||||
#include "common/logging.h"
|
||||
#include "common/settings.h"
|
||||
#include "core/core.h"
|
||||
#include "core/file_sys/patch_manager.h"
|
||||
#include "core/file_sys/registered_cache.h"
|
||||
#include "core/hle/service/filesystem/filesystem.h"
|
||||
#include "qt_common/config/uisettings.h"
|
||||
#include "qt_common/qt_common.h"
|
||||
#include "qt_common/util/game.h"
|
||||
|
||||
#include "qt_common/game_list/game_list_p.h"
|
||||
#include "qt_common/game_list/worker.h"
|
||||
#include "qt_common/game_list/model.h"
|
||||
|
||||
GameListModel::GameListModel(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
|
||||
FileSys::ManualContentProvider* provider_,
|
||||
const PlayTime::PlayTimeManager& play_time_manager_,
|
||||
Core::System& system_, QObject* parent)
|
||||
: QStandardItemModel{parent}, vfs{std::move(vfs_)}, provider{provider_},
|
||||
play_time_manager{play_time_manager_}, system{system_} {
|
||||
watcher = new QFileSystemWatcher(this);
|
||||
external_watcher = new QFileSystemWatcher(this);
|
||||
|
||||
connect(watcher, &QFileSystemWatcher::directoryChanged, this,
|
||||
&GameListModel::RefreshGameDirectory);
|
||||
connect(external_watcher, &QFileSystemWatcher::directoryChanged, this,
|
||||
&GameListModel::RefreshExternalContent);
|
||||
|
||||
insertColumns(0, COLUMN_COUNT);
|
||||
RetranslateUI();
|
||||
|
||||
setSortRole(GameListItemPath::SortRole);
|
||||
}
|
||||
|
||||
GameListModel::~GameListModel() = default;
|
||||
|
||||
void GameListModel::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
|
||||
current_worker.reset();
|
||||
removeRows(0, rowCount());
|
||||
|
||||
current_worker = std::make_unique<GameListWorker>(vfs, provider, game_dirs, compatibility_list,
|
||||
play_time_manager, system);
|
||||
|
||||
connect(current_worker.get(), &GameListWorker::DataAvailable, this, &GameListModel::WorkerEvent,
|
||||
Qt::QueuedConnection);
|
||||
|
||||
QThreadPool::globalInstance()->start(current_worker.get());
|
||||
}
|
||||
|
||||
void GameListModel::WorkerEvent() {
|
||||
current_worker->ProcessEvents(this);
|
||||
}
|
||||
|
||||
void GameListModel::AddDirEntry(GameListDir* entry_items) {
|
||||
if (m_flat) {
|
||||
return;
|
||||
}
|
||||
invisibleRootItem()->appendRow(entry_items);
|
||||
}
|
||||
|
||||
void GameListModel::AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent) {
|
||||
if (m_flat) {
|
||||
invisibleRootItem()->appendRow(entry_items);
|
||||
} else {
|
||||
parent->appendRow(entry_items);
|
||||
}
|
||||
}
|
||||
|
||||
void GameListModel::DonePopulating(const QStringList& watch_list) {
|
||||
emit ShowList(!IsEmpty());
|
||||
|
||||
if (!m_flat) {
|
||||
invisibleRootItem()->appendRow(new GameListAddDir());
|
||||
invisibleRootItem()->insertRow(0, new GameListFavorites());
|
||||
|
||||
for (const auto id : std::as_const(UISettings::values.favorited_ids)) {
|
||||
AddFavorite(id);
|
||||
}
|
||||
}
|
||||
|
||||
emit PopulatingCompleted(watch_list);
|
||||
}
|
||||
|
||||
bool GameListModel::IsEmpty() const {
|
||||
for (int i = 0; i < rowCount(); i++) {
|
||||
const QStandardItem* child = invisibleRootItem()->child(i);
|
||||
const auto type = static_cast<GameListItemType>(child->type());
|
||||
|
||||
if (!child->hasChildren() &&
|
||||
(type == GameListItemType::SdmcDir || type == GameListItemType::UserNandDir ||
|
||||
type == GameListItemType::SysNandDir)) {
|
||||
invisibleRootItem()->removeRow(child->row());
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
return !invisibleRootItem()->hasChildren();
|
||||
}
|
||||
|
||||
void GameListModel::ToggleFavorite(u64 program_id) {
|
||||
if (!UISettings::values.favorited_ids.contains(program_id)) {
|
||||
UISettings::values.favorited_ids.append(program_id);
|
||||
AddFavorite(program_id);
|
||||
} else {
|
||||
UISettings::values.favorited_ids.removeOne(program_id);
|
||||
RemoveFavorite(program_id);
|
||||
}
|
||||
emit SaveConfig();
|
||||
}
|
||||
|
||||
void GameListModel::AddFavorite(u64 program_id) {
|
||||
auto* favorites_row = item(0);
|
||||
|
||||
for (int i = 1; i < rowCount() - 1; i++) {
|
||||
const auto* folder = item(i);
|
||||
for (int j = 0; j < folder->rowCount(); j++) {
|
||||
if (folder->child(j)->data(GameListItemPath::ProgramIdRole).toULongLong() ==
|
||||
program_id) {
|
||||
QList<QStandardItem*> list;
|
||||
for (int k = 0; k < COLUMN_COUNT; k++) {
|
||||
list.append(folder->child(j, k)->clone());
|
||||
}
|
||||
list[0]->setData(folder->child(j)->data(GameListItem::SortRole),
|
||||
GameListItem::SortRole);
|
||||
list[0]->setText(folder->child(j)->data(Qt::DisplayRole).toString());
|
||||
|
||||
favorites_row->appendRow(list);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameListModel::RemoveFavorite(u64 program_id) {
|
||||
auto* favorites_row = item(0);
|
||||
|
||||
for (int i = 0; i < favorites_row->rowCount(); i++) {
|
||||
const auto* game = favorites_row->child(i);
|
||||
if (game->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) {
|
||||
favorites_row->removeRow(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameListModel::LoadCompatibilityList() {
|
||||
QFile compat_list{QStringLiteral(":compatibility_list/compatibility_list.json")};
|
||||
|
||||
if (!compat_list.open(QFile::ReadOnly | QFile::Text)) {
|
||||
LOG_ERROR(Frontend, "Unable to open game compatibility list");
|
||||
return;
|
||||
}
|
||||
|
||||
if (compat_list.size() == 0) {
|
||||
LOG_WARNING(Frontend, "Game compatibility list is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
const QByteArray content = compat_list.readAll();
|
||||
if (content.isEmpty()) {
|
||||
LOG_ERROR(Frontend, "Unable to completely read game compatibility list");
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonDocument json = QJsonDocument::fromJson(content);
|
||||
const QJsonArray arr = json.array();
|
||||
|
||||
for (const QJsonValue& value : arr) {
|
||||
const QJsonObject game = value.toObject();
|
||||
const QString compatibility_key = QStringLiteral("compatibility");
|
||||
|
||||
if (!game.contains(compatibility_key) || !game[compatibility_key].isDouble()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int compatibility = game[compatibility_key].toInt();
|
||||
const QString directory = game[QStringLiteral("directory")].toString();
|
||||
const QJsonArray ids = game[QStringLiteral("releases")].toArray();
|
||||
|
||||
for (const QJsonValue& id_ref : ids) {
|
||||
const QJsonObject id_object = id_ref.toObject();
|
||||
const QString id = id_object[QStringLiteral("id")].toString();
|
||||
|
||||
compatibility_list.emplace(id.toUpper().toStdString(),
|
||||
std::make_pair(QString::number(compatibility), directory));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameListModel::RefreshGameDirectory() {
|
||||
ResetExternalWatcher();
|
||||
|
||||
if (!UISettings::values.game_dirs.empty() && current_worker != nullptr) {
|
||||
LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
|
||||
QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs);
|
||||
PopulateAsync(UISettings::values.game_dirs);
|
||||
}
|
||||
}
|
||||
|
||||
void GameListModel::RefreshExternalContent() {
|
||||
if (!UISettings::values.game_dirs.empty() && current_worker != nullptr) {
|
||||
LOG_INFO(Frontend, "External content directory changed. Clearing metadata cache.");
|
||||
QtCommon::Game::ResetMetadata(false);
|
||||
QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs);
|
||||
PopulateAsync(UISettings::values.game_dirs);
|
||||
}
|
||||
}
|
||||
|
||||
void GameListModel::ResetExternalWatcher() {
|
||||
auto watch_dirs = external_watcher->directories();
|
||||
if (!watch_dirs.isEmpty()) {
|
||||
external_watcher->removePaths(watch_dirs);
|
||||
}
|
||||
|
||||
for (const std::string& dir : Settings::values.external_content_dirs) {
|
||||
external_watcher->addPath(QString::fromStdString(dir));
|
||||
}
|
||||
}
|
||||
|
||||
void GameListModel::OnUpdateThemedIcons() {
|
||||
for (int i = 0; i < invisibleRootItem()->rowCount(); i++) {
|
||||
QStandardItem* child = invisibleRootItem()->child(i);
|
||||
|
||||
const int icon_size = UISettings::values.folder_icon_size.GetValue();
|
||||
|
||||
switch (child->data(GameListItem::TypeRole).value<GameListItemType>()) {
|
||||
case GameListItemType::SdmcDir:
|
||||
child->setData(
|
||||
QIcon::fromTheme(QStringLiteral("sd_card"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
break;
|
||||
case GameListItemType::UserNandDir:
|
||||
case GameListItemType::SysNandDir:
|
||||
child->setData(
|
||||
QIcon::fromTheme(QStringLiteral("chip"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
break;
|
||||
case GameListItemType::CustomDir: {
|
||||
const UISettings::GameDir& game_dir =
|
||||
UISettings::values.game_dirs[child->data(GameListDir::GameDirRole).toInt()];
|
||||
const QString icon_name = QFileInfo::exists(QString::fromStdString(game_dir.path))
|
||||
? QStringLiteral("folder")
|
||||
: QStringLiteral("bad_folder");
|
||||
child->setData(
|
||||
QIcon::fromTheme(icon_name).pixmap(icon_size).scaled(
|
||||
icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
break;
|
||||
}
|
||||
case GameListItemType::AddDir:
|
||||
child->setData(
|
||||
QIcon::fromTheme(QStringLiteral("list-add"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
break;
|
||||
case GameListItemType::Favorites:
|
||||
child->setData(
|
||||
QIcon::fromTheme(QStringLiteral("star"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameListModel::RetranslateUI() {
|
||||
setHeaderData(COLUMN_NAME, Qt::Horizontal, tr("Name"));
|
||||
setHeaderData(COLUMN_COMPATIBILITY, Qt::Horizontal, tr("Compatibility"));
|
||||
setHeaderData(COLUMN_ADD_ONS, Qt::Horizontal, tr("Add-ons"));
|
||||
setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, tr("File type"));
|
||||
setHeaderData(COLUMN_SIZE, Qt::Horizontal, tr("Size"));
|
||||
setHeaderData(COLUMN_PLAY_TIME, Qt::Horizontal, tr("Play time"));
|
||||
}
|
||||
|
||||
QFileSystemWatcher* GameListModel::GetWatcher() const {
|
||||
return watcher;
|
||||
}
|
||||
|
||||
const CompatibilityList& GameListModel::GetCompatibilityList() const {
|
||||
return compatibility_list;
|
||||
}
|
||||
|
||||
void GameListModel::SetFlat(bool flat) {
|
||||
m_flat = flat;
|
||||
}
|
||||
98
src/qt_common/game_list/model.h
Normal file
98
src/qt_common/game_list/model.h
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QFileSystemWatcher>
|
||||
#include <QStandardItemModel>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
#include <memory>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "frontend_common/play_time_manager.h"
|
||||
#include "qt_common/config/uisettings.h"
|
||||
#include "yuzu/compatibility_list.h"
|
||||
|
||||
namespace Core {
|
||||
class System;
|
||||
}
|
||||
|
||||
class GameListDir;
|
||||
class GameListWorker;
|
||||
class QStandardItem;
|
||||
|
||||
namespace FileSys {
|
||||
class ManualContentProvider;
|
||||
class VfsFilesystem;
|
||||
} // namespace FileSys
|
||||
|
||||
class GameListModel : public QStandardItemModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Column {
|
||||
COLUMN_NAME,
|
||||
COLUMN_FILE_TYPE,
|
||||
COLUMN_SIZE,
|
||||
COLUMN_PLAY_TIME,
|
||||
COLUMN_ADD_ONS,
|
||||
COLUMN_COMPATIBILITY,
|
||||
COLUMN_COUNT,
|
||||
};
|
||||
|
||||
explicit GameListModel(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
|
||||
FileSys::ManualContentProvider* provider_,
|
||||
const PlayTime::PlayTimeManager& play_time_manager_,
|
||||
Core::System& system_, QObject* parent = nullptr);
|
||||
~GameListModel() override;
|
||||
|
||||
void AddDirEntry(GameListDir* entry_items);
|
||||
void AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent);
|
||||
void DonePopulating(const QStringList& watch_list);
|
||||
|
||||
void PopulateAsync(QVector<UISettings::GameDir>& game_dirs);
|
||||
void WorkerEvent();
|
||||
|
||||
bool IsEmpty() const;
|
||||
|
||||
void ToggleFavorite(u64 program_id);
|
||||
|
||||
void RefreshGameDirectory();
|
||||
void RefreshExternalContent();
|
||||
void ResetExternalWatcher();
|
||||
|
||||
void LoadCompatibilityList();
|
||||
|
||||
void OnUpdateThemedIcons();
|
||||
void RetranslateUI();
|
||||
|
||||
QFileSystemWatcher* GetWatcher() const;
|
||||
|
||||
const CompatibilityList& GetCompatibilityList() const;
|
||||
|
||||
void SetFlat(bool flat);
|
||||
|
||||
signals:
|
||||
void ShowList(bool show);
|
||||
void PopulatingCompleted(const QStringList& watch_list);
|
||||
void SaveConfig();
|
||||
|
||||
private:
|
||||
friend class GameListWorker;
|
||||
|
||||
void AddFavorite(u64 program_id);
|
||||
void RemoveFavorite(u64 program_id);
|
||||
|
||||
bool m_flat = false;
|
||||
|
||||
std::shared_ptr<FileSys::VfsFilesystem> vfs;
|
||||
FileSys::ManualContentProvider* provider;
|
||||
CompatibilityList compatibility_list;
|
||||
const PlayTime::PlayTimeManager& play_time_manager;
|
||||
Core::System& system;
|
||||
|
||||
std::unique_ptr<GameListWorker> current_worker;
|
||||
QFileSystemWatcher* watcher = nullptr;
|
||||
QFileSystemWatcher* external_watcher = nullptr;
|
||||
};
|
||||
497
src/qt_common/game_list/worker.cpp
Normal file
497
src/qt_common/game_list/worker.cpp
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QSettings>
|
||||
|
||||
#include "common/fs/fs.h"
|
||||
#include "common/fs/path_util.h"
|
||||
#include "common/settings.h"
|
||||
#include "core/core.h"
|
||||
#include "core/file_sys/card_image.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/control_metadata.h"
|
||||
#include "core/file_sys/fs_filesystem.h"
|
||||
#include "core/file_sys/nca_metadata.h"
|
||||
#include "core/file_sys/patch_manager.h"
|
||||
#include "core/file_sys/registered_cache.h"
|
||||
#include "core/file_sys/submission_package.h"
|
||||
#include "core/loader/loader.h"
|
||||
|
||||
#include "qt_common/config/uisettings.h"
|
||||
#include "qt_common/qt_common.h"
|
||||
|
||||
#include "yuzu/compatibility_list.h"
|
||||
#include "qt_common/game_list/game_list_p.h"
|
||||
|
||||
#include "qt_common/game_list/worker.h"
|
||||
#include "qt_common/game_list/model.h"
|
||||
|
||||
namespace {
|
||||
|
||||
QString GetGameListCachedObject(const std::string& filename, const std::string& ext,
|
||||
const std::function<QString()>& generator) {
|
||||
if (!UISettings::values.cache_game_list || filename == "0000000000000000") {
|
||||
return generator();
|
||||
}
|
||||
|
||||
const auto path =
|
||||
Common::FS::PathToUTF8String(Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) /
|
||||
"game_list" / fmt::format("{}.{}", filename, ext));
|
||||
|
||||
void(Common::FS::CreateParentDirs(path));
|
||||
|
||||
if (!Common::FS::Exists(path)) {
|
||||
const auto str = generator();
|
||||
|
||||
QFile file{QString::fromStdString(path)};
|
||||
if (file.open(QFile::WriteOnly)) {
|
||||
file.write(str.toUtf8());
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
QFile file{QString::fromStdString(path)};
|
||||
if (file.open(QFile::ReadOnly)) {
|
||||
return QString::fromUtf8(file.readAll());
|
||||
}
|
||||
|
||||
return generator();
|
||||
}
|
||||
|
||||
std::pair<std::vector<u8>, std::string> GetGameListCachedObject(
|
||||
const std::string& filename, const std::string& ext,
|
||||
const std::function<std::pair<std::vector<u8>, std::string>()>& generator) {
|
||||
if (!UISettings::values.cache_game_list || filename == "0000000000000000") {
|
||||
return generator();
|
||||
}
|
||||
|
||||
const auto game_list_dir =
|
||||
Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "game_list";
|
||||
const auto jpeg_name = fmt::format("{}.jpeg", filename);
|
||||
const auto app_name = fmt::format("{}.appname.txt", filename);
|
||||
|
||||
const auto path1 = Common::FS::PathToUTF8String(game_list_dir / jpeg_name);
|
||||
const auto path2 = Common::FS::PathToUTF8String(game_list_dir / app_name);
|
||||
|
||||
void(Common::FS::CreateParentDirs(path1));
|
||||
|
||||
if (!Common::FS::Exists(path1) || !Common::FS::Exists(path2)) {
|
||||
const auto [icon, nacp] = generator();
|
||||
|
||||
QFile file1{QString::fromStdString(path1)};
|
||||
if (!file1.open(QFile::WriteOnly)) {
|
||||
LOG_ERROR(Frontend, "Failed to open cache file.");
|
||||
return generator();
|
||||
}
|
||||
|
||||
if (!file1.resize(icon.size())) {
|
||||
LOG_ERROR(Frontend, "Failed to resize cache file to necessary size.");
|
||||
return generator();
|
||||
}
|
||||
|
||||
if (file1.write(reinterpret_cast<const char*>(icon.data()), icon.size()) !=
|
||||
s64(icon.size())) {
|
||||
LOG_ERROR(Frontend, "Failed to write data to cache file.");
|
||||
return generator();
|
||||
}
|
||||
|
||||
QFile file2{QString::fromStdString(path2)};
|
||||
if (file2.open(QFile::WriteOnly)) {
|
||||
file2.write(nacp.data(), nacp.size());
|
||||
}
|
||||
|
||||
return std::make_pair(icon, nacp);
|
||||
}
|
||||
|
||||
QFile file1(QString::fromStdString(path1));
|
||||
QFile file2(QString::fromStdString(path2));
|
||||
|
||||
if (!file1.open(QFile::ReadOnly)) {
|
||||
LOG_ERROR(Frontend, "Failed to open cache file for reading.");
|
||||
return generator();
|
||||
}
|
||||
|
||||
if (!file2.open(QFile::ReadOnly)) {
|
||||
LOG_ERROR(Frontend, "Failed to open cache file for reading.");
|
||||
return generator();
|
||||
}
|
||||
|
||||
std::vector<u8> vec(file1.size());
|
||||
if (file1.read(reinterpret_cast<char*>(vec.data()), vec.size()) !=
|
||||
static_cast<s64>(vec.size())) {
|
||||
return generator();
|
||||
}
|
||||
|
||||
const auto data = file2.readAll();
|
||||
return std::make_pair(vec, data.toStdString());
|
||||
}
|
||||
|
||||
void GetMetadataFromControlNCA(const FileSys::PatchManager& patch_manager, const FileSys::NCA& nca,
|
||||
std::vector<u8>& icon, std::string& name) {
|
||||
std::tie(icon, name) = GetGameListCachedObject(
|
||||
fmt::format("{:016X}", patch_manager.GetTitleID()), {}, [&patch_manager, &nca] {
|
||||
const auto [nacp, icon_f] = patch_manager.ParseControlNCA(nca);
|
||||
return std::make_pair(icon_f->ReadAllBytes(), nacp->GetApplicationName());
|
||||
});
|
||||
}
|
||||
|
||||
bool HasSupportedFileExtension(const std::string& file_name) {
|
||||
const QFileInfo file = QFileInfo(QString::fromStdString(file_name));
|
||||
return QtCommon::supported_file_extensions.contains(file.suffix(), Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
bool IsExtractedNCAMain(const std::string& file_name) {
|
||||
return QFileInfo(QString::fromStdString(file_name)).fileName() == QStringLiteral("main");
|
||||
}
|
||||
|
||||
QString FormatGameName(const std::string& physical_name) {
|
||||
const QString physical_name_as_qstring = QString::fromStdString(physical_name);
|
||||
const QFileInfo file_info(physical_name_as_qstring);
|
||||
|
||||
if (IsExtractedNCAMain(physical_name)) {
|
||||
return file_info.dir().path();
|
||||
}
|
||||
|
||||
return physical_name_as_qstring;
|
||||
}
|
||||
|
||||
QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager,
|
||||
Loader::AppLoader& loader, bool updatable = true) {
|
||||
QString out;
|
||||
FileSys::VirtualFile update_raw;
|
||||
loader.ReadUpdateRaw(update_raw);
|
||||
for (const auto& patch : patch_manager.GetPatches(update_raw)) {
|
||||
const bool is_update = patch.name == "Update";
|
||||
if (!updatable && is_update) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString type =
|
||||
QString::fromStdString(patch.enabled ? patch.name : "[D] " + patch.name);
|
||||
|
||||
if (patch.version.empty()) {
|
||||
out.append(QStringLiteral("%1\n").arg(type));
|
||||
} else {
|
||||
auto ver = patch.version;
|
||||
|
||||
// Display container name for packed updates
|
||||
if (is_update && ver == "PACKED") {
|
||||
ver = Loader::GetFileTypeString(loader.GetFileType());
|
||||
}
|
||||
|
||||
out.append(QStringLiteral("%1 (%2)\n").arg(type, QString::fromStdString(ver)));
|
||||
}
|
||||
}
|
||||
|
||||
out.chop(1);
|
||||
return out;
|
||||
}
|
||||
|
||||
QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::string& name,
|
||||
const std::size_t size, const std::vector<u8>& icon,
|
||||
Loader::AppLoader& loader, u64 program_id,
|
||||
const CompatibilityList& compatibility_list,
|
||||
const PlayTime::PlayTimeManager& play_time_manager,
|
||||
const FileSys::PatchManager& patch) {
|
||||
auto const it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
|
||||
// The game list uses 99 as compatibility number for untested games
|
||||
QString compatibility =
|
||||
it != compatibility_list.end() ? it->second.first : QStringLiteral("99");
|
||||
|
||||
auto const file_type = loader.GetFileType();
|
||||
auto const file_type_string = QString::fromStdString(Loader::GetFileTypeString(file_type));
|
||||
|
||||
QString patch_versions = GetGameListCachedObject(
|
||||
fmt::format("{:016X}", patch.GetTitleID()), "pv.txt", [&patch, &loader] {
|
||||
return FormatPatchNameVersions(patch, loader, loader.IsRomFSUpdatable());
|
||||
});
|
||||
|
||||
u64 play_time = play_time_manager.GetPlayTime(program_id);
|
||||
return QList<QStandardItem*>{
|
||||
new GameListItemPath(FormatGameName(path), icon, QString::fromStdString(name),
|
||||
file_type_string, program_id, play_time, patch_versions),
|
||||
new GameListItem(file_type_string),
|
||||
new GameListItemSize(size),
|
||||
new GameListItemPlayTime(play_time),
|
||||
new GameListItem(patch_versions),
|
||||
new GameListItemCompat(compatibility),
|
||||
};
|
||||
}
|
||||
} // Anonymous namespace
|
||||
|
||||
GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs_,
|
||||
FileSys::ManualContentProvider* provider_,
|
||||
QVector<UISettings::GameDir>& game_dirs_,
|
||||
const CompatibilityList& compatibility_list_,
|
||||
const PlayTime::PlayTimeManager& play_time_manager_,
|
||||
Core::System& system_)
|
||||
: vfs{std::move(vfs_)}, provider{provider_}, game_dirs{game_dirs_},
|
||||
compatibility_list{compatibility_list_}, play_time_manager{play_time_manager_},
|
||||
system{system_} {
|
||||
// We want the game list to manage our lifetime.
|
||||
setAutoDelete(false);
|
||||
}
|
||||
|
||||
GameListWorker::~GameListWorker() {
|
||||
this->disconnect();
|
||||
stop_requested.store(true);
|
||||
processing_completed.Wait();
|
||||
}
|
||||
|
||||
void GameListWorker::ProcessEvents(GameListModel* model) {
|
||||
while (true) {
|
||||
std::function<void(GameListModel*)> func;
|
||||
{
|
||||
// Lock queue to protect concurrent modification.
|
||||
std::scoped_lock lk(lock);
|
||||
|
||||
// If we can't pop a function, return.
|
||||
if (queued_events.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pop a function.
|
||||
func = std::move(queued_events.back());
|
||||
queued_events.pop_back();
|
||||
}
|
||||
|
||||
// Run the function.
|
||||
func(model);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
void GameListWorker::RecordEvent(F&& func) {
|
||||
{
|
||||
// Lock queue to protect concurrent modification.
|
||||
std::scoped_lock lk(lock);
|
||||
|
||||
// Add the function into the front of the queue.
|
||||
queued_events.emplace_front(std::move(func));
|
||||
}
|
||||
|
||||
// Data now available.
|
||||
emit DataAvailable();
|
||||
}
|
||||
|
||||
void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
|
||||
using namespace FileSys;
|
||||
|
||||
const auto& cache = system.GetContentProviderUnion();
|
||||
|
||||
auto installed_games = cache.ListEntriesFilterOrigin(std::nullopt, TitleType::Application,
|
||||
ContentRecordType::Program);
|
||||
|
||||
if (parent_dir->type() == static_cast<int>(GameListItemType::SdmcDir)) {
|
||||
installed_games = cache.ListEntriesFilterOrigin(
|
||||
ContentProviderUnionSlot::SDMC, TitleType::Application, ContentRecordType::Program);
|
||||
} else if (parent_dir->type() == static_cast<int>(GameListItemType::UserNandDir)) {
|
||||
installed_games = cache.ListEntriesFilterOrigin(
|
||||
ContentProviderUnionSlot::UserNAND, TitleType::Application, ContentRecordType::Program);
|
||||
} else if (parent_dir->type() == static_cast<int>(GameListItemType::SysNandDir)) {
|
||||
installed_games = cache.ListEntriesFilterOrigin(
|
||||
ContentProviderUnionSlot::SysNAND, TitleType::Application, ContentRecordType::Program);
|
||||
}
|
||||
|
||||
for (const auto& [slot, game] : installed_games) {
|
||||
if (slot == ContentProviderUnionSlot::FrontendManual) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto file = cache.GetEntryUnparsed(game.title_id, game.type);
|
||||
std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(system, file);
|
||||
if (!loader) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<u8> icon;
|
||||
std::string name;
|
||||
u64 program_id = 0;
|
||||
const auto result = loader->ReadProgramId(program_id);
|
||||
|
||||
if (result != Loader::ResultStatus::Success) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const PatchManager patch{program_id, system.GetFileSystemController(),
|
||||
system.GetContentProvider()};
|
||||
LOG_INFO(Frontend, "PatchManager initiated for id {:X}", program_id);
|
||||
const auto control = cache.GetEntry(game.title_id, ContentRecordType::Control);
|
||||
if (control != nullptr) {
|
||||
GetMetadataFromControlNCA(patch, *control, icon, name);
|
||||
}
|
||||
|
||||
auto entry = MakeGameListEntry(file->GetFullPath(), name, file->GetSize(), icon, *loader,
|
||||
program_id, compatibility_list, play_time_manager, patch);
|
||||
RecordEvent([=](GameListModel* model) { model->AddEntry(entry, parent_dir); });
|
||||
}
|
||||
}
|
||||
|
||||
void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan,
|
||||
GameListDir* parent_dir) {
|
||||
const auto callback = [this, target, parent_dir](const std::filesystem::path& path) -> bool {
|
||||
if (stop_requested) {
|
||||
// Breaks the callback loop.
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto physical_name = Common::FS::PathToUTF8String(path);
|
||||
const auto is_dir = Common::FS::IsDir(path);
|
||||
|
||||
if (!is_dir &&
|
||||
(HasSupportedFileExtension(physical_name) || IsExtractedNCAMain(physical_name))) {
|
||||
const auto file = vfs->OpenFile(physical_name, FileSys::OpenMode::Read);
|
||||
if (!file) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto loader = Loader::GetLoader(system, file);
|
||||
if (!loader) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto file_type = loader->GetFileType();
|
||||
if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (target == ScanTarget::PopulateGameList &&
|
||||
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP) &&
|
||||
!Loader::IsBootableGameContainer(file, file_type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
u64 program_id = 0;
|
||||
const auto res2 = loader->ReadProgramId(program_id);
|
||||
|
||||
if (target == ScanTarget::FillManualContentProvider) {
|
||||
if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) {
|
||||
provider->AddEntry(FileSys::TitleType::Application,
|
||||
FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
|
||||
program_id, file);
|
||||
} else if (Settings::values.ext_content_from_game_dirs.GetValue() &&
|
||||
(file_type == Loader::FileType::XCI ||
|
||||
file_type == Loader::FileType::NSP)) {
|
||||
void(provider->AddEntriesFromContainer(file));
|
||||
}
|
||||
} else {
|
||||
std::vector<u64> program_ids;
|
||||
loader->ReadProgramIds(program_ids);
|
||||
|
||||
if (res2 == Loader::ResultStatus::Success && program_ids.size() > 1 &&
|
||||
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) {
|
||||
for (const auto id : program_ids) {
|
||||
// dravee suggested this, only viable way to
|
||||
// not show sub-games in qlaunch for now.
|
||||
if ((id & 0xFFF) != 0) {
|
||||
continue;
|
||||
}
|
||||
loader = Loader::GetLoader(system, file, id);
|
||||
if (!loader) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<u8> icon;
|
||||
[[maybe_unused]] const auto res1 = loader->ReadIcon(icon);
|
||||
|
||||
std::string name = " ";
|
||||
[[maybe_unused]] const auto res3 = loader->ReadTitle(name);
|
||||
|
||||
const FileSys::PatchManager patch{id, system.GetFileSystemController(),
|
||||
system.GetContentProvider()};
|
||||
|
||||
auto entry = MakeGameListEntry(
|
||||
physical_name, name, Common::FS::GetSize(physical_name), icon, *loader,
|
||||
id, compatibility_list, play_time_manager, patch);
|
||||
|
||||
RecordEvent(
|
||||
[=](GameListModel* model) { model->AddEntry(entry, parent_dir); });
|
||||
}
|
||||
} else {
|
||||
std::vector<u8> icon;
|
||||
[[maybe_unused]] const auto res1 = loader->ReadIcon(icon);
|
||||
|
||||
std::string name = " ";
|
||||
[[maybe_unused]] const auto res3 = loader->ReadTitle(name);
|
||||
|
||||
const FileSys::PatchManager patch{program_id, system.GetFileSystemController(),
|
||||
system.GetContentProvider()};
|
||||
|
||||
auto entry = MakeGameListEntry(
|
||||
physical_name, name, Common::FS::GetSize(physical_name), icon, *loader,
|
||||
program_id, compatibility_list, play_time_manager, patch);
|
||||
|
||||
RecordEvent(
|
||||
[=](GameListModel* model) { model->AddEntry(entry, parent_dir); });
|
||||
}
|
||||
}
|
||||
} else if (is_dir) {
|
||||
watch_list.append(QString::fromStdString(physical_name));
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
if (deep_scan) {
|
||||
Common::FS::IterateDirEntriesRecursively(dir_path, callback,
|
||||
Common::FS::DirEntryFilter::All);
|
||||
} else {
|
||||
Common::FS::IterateDirEntries(dir_path, callback, Common::FS::DirEntryFilter::File);
|
||||
}
|
||||
}
|
||||
|
||||
void GameListWorker::run() {
|
||||
watch_list.clear();
|
||||
provider->ClearAllEntries();
|
||||
|
||||
const auto DirEntryReady = [&](GameListDir* game_list_dir) {
|
||||
RecordEvent([=](GameListModel* model) { model->AddDirEntry(game_list_dir); });
|
||||
};
|
||||
|
||||
for (UISettings::GameDir& game_dir : game_dirs) {
|
||||
if (stop_requested) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (game_dir.path == std::string("SDMC")) {
|
||||
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SdmcDir);
|
||||
DirEntryReady(game_list_dir);
|
||||
AddTitlesToGameList(game_list_dir);
|
||||
} else if (game_dir.path == std::string("UserNAND")) {
|
||||
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::UserNandDir);
|
||||
DirEntryReady(game_list_dir);
|
||||
AddTitlesToGameList(game_list_dir);
|
||||
} else if (game_dir.path == std::string("SysNAND")) {
|
||||
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SysNandDir);
|
||||
DirEntryReady(game_list_dir);
|
||||
AddTitlesToGameList(game_list_dir);
|
||||
} else {
|
||||
const QString qpath = QString::fromStdString(game_dir.path);
|
||||
if (QDir(qpath).exists()) {
|
||||
watch_list.append(qpath);
|
||||
}
|
||||
auto* const game_list_dir = new GameListDir(game_dir);
|
||||
DirEntryReady(game_list_dir);
|
||||
ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path, game_dir.deep_scan,
|
||||
game_list_dir);
|
||||
ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path, game_dir.deep_scan,
|
||||
game_list_dir);
|
||||
}
|
||||
}
|
||||
|
||||
RecordEvent([this](GameListModel* model) { model->DonePopulating(watch_list); });
|
||||
processing_completed.Set();
|
||||
}
|
||||
100
src/qt_common/game_list/worker.h
Normal file
100
src/qt_common/game_list/worker.h
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QRunnable>
|
||||
#include <QString>
|
||||
|
||||
#include "common/thread.h"
|
||||
#include "core/file_sys/registered_cache.h"
|
||||
#include "frontend_common/play_time_manager.h"
|
||||
#include "qt_common/config/uisettings.h"
|
||||
#include "yuzu/compatibility_list.h"
|
||||
|
||||
namespace Core {
|
||||
class System;
|
||||
}
|
||||
|
||||
class GameListDir;
|
||||
class GameListModel;
|
||||
class QStandardItem;
|
||||
|
||||
namespace FileSys {
|
||||
class NCA;
|
||||
class VfsFilesystem;
|
||||
} // namespace FileSys
|
||||
|
||||
/**
|
||||
* Asynchronous worker object for populating the game list.
|
||||
* Communicates with other threads through Qt's signal/slot system.
|
||||
*/
|
||||
class GameListWorker : public QObject, public QRunnable {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
|
||||
FileSys::ManualContentProvider* provider_,
|
||||
QVector<UISettings::GameDir>& game_dirs_,
|
||||
const CompatibilityList& compatibility_list_,
|
||||
const PlayTime::PlayTimeManager& play_time_manager_,
|
||||
Core::System& system_);
|
||||
~GameListWorker() override;
|
||||
|
||||
/// Starts the processing of directory tree information.
|
||||
void run() override;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Synchronously processes any events queued by the worker.
|
||||
*
|
||||
* AddDirEntry is called on the model for every discovered directory.
|
||||
* AddEntry is called on the model for every discovered program.
|
||||
* DonePopulating is called on the model when processing completes.
|
||||
*/
|
||||
void ProcessEvents(GameListModel* model);
|
||||
|
||||
signals:
|
||||
void DataAvailable();
|
||||
|
||||
private:
|
||||
template <typename F>
|
||||
void RecordEvent(F&& func);
|
||||
|
||||
private:
|
||||
void AddTitlesToGameList(GameListDir* parent_dir);
|
||||
|
||||
enum class ScanTarget {
|
||||
FillManualContentProvider,
|
||||
PopulateGameList,
|
||||
};
|
||||
|
||||
void ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan,
|
||||
GameListDir* parent_dir);
|
||||
|
||||
std::shared_ptr<FileSys::VfsFilesystem> vfs;
|
||||
FileSys::ManualContentProvider* provider;
|
||||
QVector<UISettings::GameDir>& game_dirs;
|
||||
const CompatibilityList& compatibility_list;
|
||||
const PlayTime::PlayTimeManager& play_time_manager;
|
||||
|
||||
QStringList watch_list;
|
||||
|
||||
std::mutex lock;
|
||||
std::condition_variable cv;
|
||||
std::deque<std::function<void(GameListModel*)>> queued_events;
|
||||
std::atomic_bool stop_requested = false;
|
||||
Common::Event processing_completed;
|
||||
|
||||
Core::System& system;
|
||||
};
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
#include "common/memory_detect.h"
|
||||
#include "core/hle/service/filesystem/filesystem.h"
|
||||
#include "frontend_common/data_manager.h"
|
||||
#include "hid_core/hid_core.h"
|
||||
#include "network/network.h"
|
||||
#include "qt_common.h"
|
||||
|
|
@ -27,6 +28,7 @@
|
|||
|
||||
#include <thread>
|
||||
#include <JlCompress.h>
|
||||
#include <QPainter>
|
||||
|
||||
#if !defined(WIN32) && !defined(__APPLE__)
|
||||
#include <qpa/qplatformnativeinterface.h>
|
||||
|
|
@ -307,4 +309,19 @@ void SetupHID() {
|
|||
system->HIDCore().ReloadInputDevices();
|
||||
}
|
||||
|
||||
QString ReadableByteSize(qulonglong size) {
|
||||
return QString::fromStdString(FrontendCommon::DataManager::ReadableBytesSize(size));
|
||||
}
|
||||
|
||||
QPixmap CreateCirclePixmapFromColor(const QColor& color) {
|
||||
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}, 7.0, 7.0);
|
||||
return circle_pixmap;
|
||||
}
|
||||
|
||||
} // namespace QtCommon
|
||||
|
|
|
|||
|
|
@ -46,6 +46,20 @@ void SetupHID();
|
|||
const QString tr(const char* str);
|
||||
const QString tr(const std::string& str);
|
||||
|
||||
// TODO: Find a better place for these
|
||||
|
||||
/// Convert a size in bytes into a readable format (KiB, MiB, etc.)
|
||||
[[nodiscard]] QString ReadableByteSize(qulonglong size);
|
||||
|
||||
/**
|
||||
* Creates a circle pixmap from a specified color
|
||||
* @param color The color the pixmap shall have
|
||||
* @return QPixmap circle pixmap
|
||||
*/
|
||||
[[nodiscard]] QPixmap CreateCirclePixmapFromColor(const QColor& color);
|
||||
|
||||
|
||||
|
||||
std::filesystem::path GetEdenCommand();
|
||||
} // namespace QtCommon
|
||||
#endif
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue