Browse Source
Read actual games from game dirs
Read actual games from game dirs
TODO: GameList/GameListModel can be abstracted in some way GameListWorker should work on this abstraction. I also need to add grouped directories just like Widgets Signed-off-by: crueter <crueter@eden-emu.dev>pull/3016/head
No known key found for this signature in database
GPG Key ID: 425ACD2D4830EBC6
13 changed files with 811 additions and 94 deletions
-
17src/Eden/Interface/CMakeLists.txt
-
5src/Eden/Main/GameCarouselCard.qml
-
6src/Eden/Main/GameGridCard.qml
-
32src/Eden/Main/GameList.qml
-
5src/Eden/Models/CMakeLists.txt
-
45src/Eden/Models/GameIconProvider.cpp
-
16src/Eden/Models/GameIconProvider.h
-
143src/Eden/Models/GameListModel.cpp
-
26src/Eden/Models/GameListModel.h
-
512src/Eden/Models/GameListWorker.cpp
-
81src/Eden/Models/GameListWorker.h
-
15src/Eden/Native/main.cpp
-
2src/video_core/CMakeLists.txt
@ -0,0 +1,45 @@ |
|||||
|
#include <qnamespace.h>
|
||||
|
#include "GameIconProvider.h"
|
||||
|
#include "qt_common/uisettings.h"
|
||||
|
|
||||
|
/**
|
||||
|
* 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(const QSize &size) |
||||
|
{ |
||||
|
QPixmap icon(size.width(), size.height()); |
||||
|
icon.fill(Qt::transparent); |
||||
|
return icon; |
||||
|
} |
||||
|
|
||||
|
GameIconProvider::GameIconProvider() |
||||
|
: QQuickImageProvider(QQuickImageProvider::Pixmap) |
||||
|
{} |
||||
|
|
||||
|
QPixmap GameIconProvider::requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) |
||||
|
{ |
||||
|
const u32 default_size = UISettings::values.game_icon_size.GetValue(); |
||||
|
QSize trueSize = QSize(default_size, default_size); |
||||
|
if (requestedSize.isValid()) { |
||||
|
trueSize = requestedSize; |
||||
|
} |
||||
|
|
||||
|
QPixmap pixmap = m_pixmaps.value(id, GetDefaultIcon(trueSize)); |
||||
|
|
||||
|
if (size) |
||||
|
*size = QSize(trueSize.width(), trueSize.height()); |
||||
|
|
||||
|
return pixmap; |
||||
|
} |
||||
|
|
||||
|
void GameIconProvider::addPixmap(const QString &key, const QPixmap pixmap) |
||||
|
{ |
||||
|
m_pixmaps.insert(key, pixmap); |
||||
|
} |
||||
|
|
||||
|
void GameIconProvider::clear() |
||||
|
{ |
||||
|
m_pixmaps.clear(); |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
#pragma once |
||||
|
|
||||
|
#include <QQuickImageProvider> |
||||
|
|
||||
|
class GameIconProvider : public QQuickImageProvider |
||||
|
{ |
||||
|
public: |
||||
|
GameIconProvider(); |
||||
|
|
||||
|
QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override; |
||||
|
|
||||
|
void addPixmap(const QString &key, const QPixmap pixmap); |
||||
|
void clear(); |
||||
|
private: |
||||
|
QMap<QString, QPixmap> m_pixmaps; |
||||
|
}; |
||||
@ -0,0 +1,512 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
|
||||
|
#include <memory>
|
||||
|
#include <string>
|
||||
|
#include <utility>
|
||||
|
#include <vector>
|
||||
|
|
||||
|
#include "GameListModel.h"
|
||||
|
#include "common/logging/types.h"
|
||||
|
|
||||
|
#include <QDir>
|
||||
|
#include <QFile>
|
||||
|
#include <QFileInfo>
|
||||
|
#include <QSettings>
|
||||
|
#include <QStandardItem>
|
||||
|
|
||||
|
#include "GameListWorker.h"
|
||||
|
#include "common/fs/fs.h"
|
||||
|
#include "common/fs/path_util.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/qt_common.h"
|
||||
|
#include "qt_common/uisettings.h"
|
||||
|
|
||||
|
namespace { |
||||
|
|
||||
|
/**
|
||||
|
* 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; |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
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 FileSys::PatchManager& patch) |
||||
|
{ |
||||
|
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()); |
||||
|
}); |
||||
|
|
||||
|
QStandardItem* item = new QStandardItem(QString::fromStdString(name)); |
||||
|
enum GLMRoleTypes { NAME = Qt::UserRole + 1, PATH, FILESIZE }; |
||||
|
|
||||
|
item->setData(QString::fromStdString(name), GameListModel::NAME); |
||||
|
item->setData(QString::fromStdString(path), GameListModel::PATH); |
||||
|
|
||||
|
const u32 pic_size = UISettings::values.game_icon_size.GetValue(); |
||||
|
|
||||
|
QPixmap picture; |
||||
|
if (!picture.loadFromData(icon.data(), static_cast<u32>(icon.size()))) { |
||||
|
picture = GetDefaultIcon(pic_size); |
||||
|
} |
||||
|
picture = picture.scaled(pic_size, pic_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); |
||||
|
|
||||
|
item->setData(picture, GameListModel::ICON); |
||||
|
// item->setData(QVariant::fromValue(size), GameListModel::FILESIZE);
|
||||
|
|
||||
|
return item; |
||||
|
} |
||||
|
|
||||
|
} // Anonymous namespace
|
||||
|
|
||||
|
GameListWorker::GameListWorker(QVector<UISettings::GameDir>& game_dirs_) |
||||
|
: game_dirs{game_dirs_} |
||||
|
{ |
||||
|
// 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* game_list) |
||||
|
{ |
||||
|
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(game_list); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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(UISettings::GameDir& parent_dir) |
||||
|
{ |
||||
|
using namespace FileSys; |
||||
|
|
||||
|
const auto& cache = QtCommon::system->GetContentProviderUnion(); |
||||
|
|
||||
|
auto installed_games = cache.ListEntriesFilterOrigin(std::nullopt, |
||||
|
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(*QtCommon::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, |
||||
|
QtCommon::system->GetFileSystemController(), |
||||
|
QtCommon::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, |
||||
|
patch); |
||||
|
RecordEvent([=](GameListModel* game_list) { game_list->addEntry(entry, parent_dir); }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void GameListWorker::ScanFileSystem(ScanTarget target, |
||||
|
const std::string& dir_path, |
||||
|
bool deep_scan, |
||||
|
UISettings::GameDir& 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 = QtCommon::vfs->OpenFile(physical_name, FileSys::OpenMode::Read); |
||||
|
if (!file) { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
auto loader = Loader::GetLoader(*QtCommon::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; |
||||
|
} |
||||
|
|
||||
|
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) { |
||||
|
QtCommon::provider->AddEntry(FileSys::TitleType::Application, |
||||
|
FileSys::GetCRTypeFromNCAType( |
||||
|
FileSys::NCA{file}.GetType()), |
||||
|
program_id, |
||||
|
file); |
||||
|
} else if (res2 == Loader::ResultStatus::Success |
||||
|
&& (file_type == Loader::FileType::XCI |
||||
|
|| file_type == Loader::FileType::NSP)) { |
||||
|
const auto nsp = file_type == Loader::FileType::NSP |
||||
|
? std::make_shared<FileSys::NSP>(file) |
||||
|
: FileSys::XCI{file}.GetSecurePartitionNSP(); |
||||
|
for (const auto& title : nsp->GetNCAs()) { |
||||
|
for (const auto& entry : title.second) { |
||||
|
QtCommon::provider->AddEntry(entry.first.first, |
||||
|
entry.first.second, |
||||
|
title.first, |
||||
|
entry.second->GetBaseFile()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} 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) { |
||||
|
loader = Loader::GetLoader(*QtCommon::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, |
||||
|
QtCommon::system->GetFileSystemController(), |
||||
|
QtCommon::system->GetContentProvider()}; |
||||
|
|
||||
|
auto entry = MakeGameListEntry(physical_name, |
||||
|
name, |
||||
|
Common::FS::GetSize(physical_name), |
||||
|
icon, |
||||
|
*loader, |
||||
|
id, |
||||
|
patch); |
||||
|
|
||||
|
RecordEvent([=](GameListModel* game_list) { |
||||
|
game_list->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, |
||||
|
QtCommon::system->GetFileSystemController(), |
||||
|
QtCommon::system->GetContentProvider()}; |
||||
|
|
||||
|
auto entry = MakeGameListEntry(physical_name, |
||||
|
name, |
||||
|
Common::FS::GetSize(physical_name), |
||||
|
icon, |
||||
|
*loader, |
||||
|
program_id, |
||||
|
patch); |
||||
|
|
||||
|
RecordEvent( |
||||
|
[=](GameListModel* game_list) { game_list->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(); |
||||
|
QtCommon::provider->ClearAllEntries(); |
||||
|
|
||||
|
const auto DirEntryReady = [&](UISettings::GameDir& game_list_dir) { |
||||
|
RecordEvent([=](GameListModel* game_list) { game_list->addDirEntry(game_list_dir); }); |
||||
|
}; |
||||
|
|
||||
|
for (UISettings::GameDir& game_dir : game_dirs) { |
||||
|
if (stop_requested) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
watch_list.append(QString::fromStdString(game_dir.path)); |
||||
|
DirEntryReady(game_dir); |
||||
|
ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path, game_dir.deep_scan, |
||||
|
game_dir); |
||||
|
ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path, game_dir.deep_scan, |
||||
|
game_dir); |
||||
|
} |
||||
|
|
||||
|
RecordEvent([this](GameListModel* game_list) { game_list->donePopulating(watch_list); }); |
||||
|
processing_completed.Set(); |
||||
|
} |
||||
@ -0,0 +1,81 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-3.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 "qt_common/uisettings.h" |
||||
|
|
||||
|
namespace Core { class System; } |
||||
|
|
||||
|
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(QVector<UISettings::GameDir>& game_dirs_); |
||||
|
~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 game list for every discovered directory. |
||||
|
* AddEntry is called on the game list for every discovered program. |
||||
|
* DonePopulating is called on the game list when processing completes. |
||||
|
*/ |
||||
|
void ProcessEvents(GameListModel* game_list); |
||||
|
|
||||
|
signals: |
||||
|
void DataAvailable(); |
||||
|
|
||||
|
private: |
||||
|
template <typename F> |
||||
|
void RecordEvent(F&& func); |
||||
|
|
||||
|
private: |
||||
|
void AddTitlesToGameList(UISettings::GameDir& parent_dir); |
||||
|
|
||||
|
enum class ScanTarget { |
||||
|
FillManualContentProvider, |
||||
|
PopulateGameList, |
||||
|
}; |
||||
|
|
||||
|
void ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan, |
||||
|
UISettings::GameDir& parent_dir); |
||||
|
|
||||
|
QVector<UISettings::GameDir>& game_dirs; |
||||
|
|
||||
|
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; |
||||
|
}; |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue