mirror of
https://git.citron-emu.org/citron/emulator
synced 2026-02-02 23:53:36 +00:00
Merge pull request 'fs: Fix directory scanning crashes and optimize library performance & Include a Progress Bar' (#72) from fs/linux-ntfs-fix into main
Reviewed-on: https://git.citron-emu.org/Citron/Emulator/pulls/72
This commit is contained in:
@@ -72,6 +72,9 @@ void ConfigureFilesystem::SetConfiguration() {
|
||||
ui->cache_game_list->setChecked(UISettings::values.cache_game_list.GetValue());
|
||||
ui->prompt_for_autoloader->setChecked(UISettings::values.prompt_for_autoloader.GetValue());
|
||||
|
||||
// NCA Scanning Toggle
|
||||
ui->scan_nca->setChecked(UISettings::values.scan_nca.GetValue());
|
||||
|
||||
#ifdef __linux__
|
||||
ui->enable_backups_checkbox->setChecked(UISettings::values.updater_enable_backups.GetValue());
|
||||
const std::string& backup_path = UISettings::values.updater_backup_path.GetValue();
|
||||
@@ -100,6 +103,9 @@ void ConfigureFilesystem::ApplyConfiguration() {
|
||||
UISettings::values.cache_game_list = ui->cache_game_list->isChecked();
|
||||
UISettings::values.prompt_for_autoloader = ui->prompt_for_autoloader->isChecked();
|
||||
|
||||
// NCA Scanning Toggle
|
||||
UISettings::values.scan_nca = ui->scan_nca->isChecked();
|
||||
|
||||
#ifdef __linux__
|
||||
UISettings::values.updater_enable_backups = ui->enable_backups_checkbox->isChecked();
|
||||
const bool new_custom_backup_enabled = ui->custom_backup_location_checkbox->isChecked();
|
||||
|
||||
@@ -7,294 +7,90 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>453</width>
|
||||
<height>561</height>
|
||||
<height>650</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<property name="accessibleName">
|
||||
<string>Filesystem</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Storage Directories</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>NAND</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="QToolButton" name="nand_directory_button">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLineEdit" name="nand_directory_edit"/>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QLineEdit" name="sdmc_directory_edit"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>SD Card</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="3">
|
||||
<widget class="QToolButton" name="sdmc_directory_button">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Maximum</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>60</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
<string>Gamecard</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="2" column="1">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Path</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QLineEdit" name="gamecard_path_edit"/>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QCheckBox" name="gamecard_inserted">
|
||||
<property name="text">
|
||||
<string>Inserted</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QCheckBox" name="gamecard_current_game">
|
||||
<property name="text">
|
||||
<string>Current Game</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="3">
|
||||
<widget class="QToolButton" name="gamecard_path_button">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_4">
|
||||
<property name="title">
|
||||
<string>Patch Manager</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="1" column="2">
|
||||
<widget class="QLineEdit" name="load_path_edit"/>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLineEdit" name="dump_path_edit"/>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="QToolButton" name="dump_path_button">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="3">
|
||||
<widget class="QToolButton" name="load_path_button">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="4">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="dump_nso">
|
||||
<property name="text">
|
||||
<string>Dump Decompressed NSOs</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="dump_exefs">
|
||||
<property name="text">
|
||||
<string>Dump ExeFS</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="text">
|
||||
<string>Mod Load Root</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="text">
|
||||
<string>Dump Root</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="autoloader_group">
|
||||
<property name="title">
|
||||
<string>Autoloader</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_Autoloader">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="prompt_for_autoloader">
|
||||
<property name="text">
|
||||
<string>Prompt to run Autoloader when a new game directory is added</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="run_autoloader_button">
|
||||
<property name="text">
|
||||
<string>Run Autoloader Now</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="updater_group">
|
||||
<property name="title">
|
||||
<string>Updater</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_updater">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="enable_backups_checkbox">
|
||||
<property name="text">
|
||||
<string>Enable AppImage Backups</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="custom_backup_location_checkbox">
|
||||
<property name="text">
|
||||
<string>Use Custom Backup Location for AppImage Updates</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLineEdit" name="custom_backup_location_edit"/>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QToolButton" name="custom_backup_location_button">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_5">
|
||||
<property name="title">
|
||||
<string>Caching</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cache_game_list">
|
||||
<property name="text">
|
||||
<string>Cache Game List Metadata</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="reset_game_list_cache">
|
||||
<property name="text">
|
||||
<string>Reset Metadata Cache</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Storage Directories</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0"><widget class="QLabel" name="label"><property name="text"><string>NAND</string></property></widget></item>
|
||||
<item row="0" column="2"><widget class="QLineEdit" name="nand_directory_edit"/></item>
|
||||
<item row="0" column="3"><widget class="QToolButton" name="nand_directory_button"><property name="text"><string>...</string></property></widget></item>
|
||||
<item row="1" column="0"><widget class="QLabel" name="label_2"><property name="text"><string>SD Card</string></property></widget></item>
|
||||
<item row="1" column="2"><widget class="QLineEdit" name="sdmc_directory_edit"/></item>
|
||||
<item row="1" column="3"><widget class="QToolButton" name="sdmc_directory_button"><property name="text"><string>...</string></property></widget></item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title"><string>Gamecard</string></property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="1"><widget class="QCheckBox" name="gamecard_inserted"><property name="text"><string>Inserted</string></property></widget></item>
|
||||
<item row="1" column="1"><widget class="QCheckBox" name="gamecard_current_game"><property name="text"><string>Current Game</string></property></widget></item>
|
||||
<item row="2" column="1"><widget class="QLabel" name="label_3"><property name="text"><string>Path</string></property></widget></item>
|
||||
<item row="2" column="2"><widget class="QLineEdit" name="gamecard_path_edit"/></item>
|
||||
<item row="2" column="3"><widget class="QToolButton" name="gamecard_path_button"><property name="text"><string>...</string></property></widget></item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_4">
|
||||
<property name="title"><string>Patch Manager</string></property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="0" column="0"><widget class="QLabel" name="label_8"><property name="text"><string>Dump Root</string></property></widget></item>
|
||||
<item row="0" column="2"><widget class="QLineEdit" name="dump_path_edit"/></item>
|
||||
<item row="0" column="3"><widget class="QToolButton" name="dump_path_button"><property name="text"><string>...</string></property></widget></item>
|
||||
<item row="1" column="0"><widget class="QLabel" name="label_9"><property name="text"><string>Mod Load Root</string></property></widget></item>
|
||||
<item row="1" column="2"><widget class="QLineEdit" name="load_path_edit"/></item>
|
||||
<item row="1" column="3"><widget class="QToolButton" name="load_path_button"><property name="text"><string>...</string></property></widget></item>
|
||||
<item row="2" column="0" colspan="4">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item><widget class="QCheckBox" name="dump_nso"><property name="text"><string>Dump Decompressed NSOs</string></property></widget></item>
|
||||
<item><widget class="QCheckBox" name="dump_exefs"><property name="text"><string>Dump ExeFS</string></property></widget></item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="autoloader_group">
|
||||
<property name="title"><string>Autoloader</string></property>
|
||||
<layout class="QVBoxLayout">
|
||||
<item><widget class="QCheckBox" name="prompt_for_autoloader"><property name="text"><string>Prompt to run Autoloader when a new game directory is added</string></property></widget></item>
|
||||
<item><widget class="QPushButton" name="run_autoloader_button"><property name="text"><string>Run Autoloader Now</string></property></widget></item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="updater_group">
|
||||
<property name="title"><string>Updater</string></property>
|
||||
<layout class="QGridLayout" name="gridLayout_updater">
|
||||
<item row="0" column="0" colspan="2"><widget class="QCheckBox" name="enable_backups_checkbox"><property name="text"><string>Enable AppImage Backups</string></property></widget></item>
|
||||
<item row="1" column="0"><widget class="QCheckBox" name="custom_backup_location_checkbox"><property name="text"><string>Use Custom Backup Location</string></property></widget></item>
|
||||
<item row="2" column="0"><widget class="QLineEdit" name="custom_backup_location_edit"/></item>
|
||||
<item row="2" column="1"><widget class="QToolButton" name="custom_backup_location_button"><property name="text"><string>...</string></property></widget></item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_5">
|
||||
<property name="title"><string>Caching & Scanning</string></property>
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<item row="0" column="0"><widget class="QCheckBox" name="cache_game_list"><property name="text"><string>Cache Game List Metadata</string></property></widget></item>
|
||||
<item row="0" column="1"><widget class="QPushButton" name="reset_game_list_cache"><property name="text"><string>Reset Metadata Cache</string></property></widget></item>
|
||||
<item row="1" column="0" colspan="2"><widget class="QCheckBox" name="scan_nca"><property name="text"><string>Scan for .nca files (Advanced: Significantly slows down scanning)</string></property></widget></item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item><spacer name="verticalSpacer"><property name="orientation"><enum>Qt::Vertical</enum></property></spacer></item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QProgressDialog>
|
||||
#include <QProgressBar>
|
||||
#include <QScrollBar>
|
||||
#include <QStyle>
|
||||
#include <QThreadPool>
|
||||
@@ -660,6 +661,16 @@ play_time_manager{play_time_manager_}, system{system_} {
|
||||
));
|
||||
connect(btn_sort_az, &QToolButton::clicked, this, &GameList::ToggleSortOrder);
|
||||
|
||||
// Create progress bar
|
||||
progress_bar = new QProgressBar(this);
|
||||
progress_bar->setVisible(false);
|
||||
progress_bar->setFixedHeight(4);
|
||||
progress_bar->setTextVisible(false);
|
||||
progress_bar->setStyleSheet(QStringLiteral(
|
||||
"QProgressBar { border: none; background: transparent; } "
|
||||
"QProgressBar::chunk { background-color: #0078d4; }"
|
||||
));
|
||||
|
||||
// Add widgets to toolbar
|
||||
toolbar_layout->addWidget(btn_list_view);
|
||||
toolbar_layout->addWidget(btn_grid_view);
|
||||
@@ -671,6 +682,7 @@ play_time_manager{play_time_manager_}, system{system_} {
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(0);
|
||||
layout->addWidget(toolbar);
|
||||
layout->addWidget(progress_bar);
|
||||
layout->addWidget(tree_view);
|
||||
layout->addWidget(list_view);
|
||||
setLayout(layout);
|
||||
@@ -749,16 +761,21 @@ void GameList::UpdateOnlineStatus() {
|
||||
|
||||
// Run the blocking network call in a background thread using QtConcurrent
|
||||
QFuture<std::map<u64, std::pair<int, int>>> future = QtConcurrent::run([session]() {
|
||||
std::map<u64, std::pair<int, int>> stats;
|
||||
AnnounceMultiplayerRoom::RoomList room_list = session->GetRoomList();
|
||||
for (const auto& room : room_list) {
|
||||
u64 game_id = room.information.preferred_game.id;
|
||||
if (game_id != 0) {
|
||||
stats[game_id].first += room.members.size();
|
||||
stats[game_id].second++;
|
||||
try {
|
||||
std::map<u64, std::pair<int, int>> stats;
|
||||
AnnounceMultiplayerRoom::RoomList room_list = session->GetRoomList();
|
||||
for (const auto& room : room_list) {
|
||||
u64 game_id = room.information.preferred_game.id;
|
||||
if (game_id != 0) {
|
||||
stats[game_id].first += (int)room.members.size();
|
||||
stats[game_id].second++;
|
||||
}
|
||||
}
|
||||
return stats;
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(Frontend, "Exception in Online Status thread: {}", e.what());
|
||||
return std::map<u64, std::pair<int, int>>{};
|
||||
}
|
||||
return stats;
|
||||
});
|
||||
|
||||
online_status_watcher->setFuture(future);
|
||||
@@ -859,6 +876,9 @@ bool GameList::IsEmpty() const {
|
||||
}
|
||||
|
||||
void GameList::DonePopulating(const QStringList& watch_list) {
|
||||
if (progress_bar) {
|
||||
progress_bar->setVisible(false);
|
||||
}
|
||||
emit ShowList(!IsEmpty());
|
||||
item_model->invisibleRootItem()->appendRow(new GameListAddDir());
|
||||
item_model->invisibleRootItem()->insertRow(0, new GameListFavorites());
|
||||
@@ -1283,7 +1303,9 @@ QStandardItemModel* GameList::GetModel() const {
|
||||
}
|
||||
|
||||
void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
|
||||
UpdateProgressBarColor();
|
||||
tree_view->setEnabled(false);
|
||||
emit ShowList(true);
|
||||
tree_view->setColumnHidden(COLUMN_ADD_ONS, !UISettings::values.show_add_ons);
|
||||
tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat);
|
||||
tree_view->setColumnHidden(COLUMN_FILE_TYPE, !UISettings::values.show_types);
|
||||
@@ -1293,8 +1315,19 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
|
||||
current_worker.reset();
|
||||
item_model->removeRows(0, item_model->rowCount());
|
||||
search_field->clear();
|
||||
|
||||
if (progress_bar) {
|
||||
progress_bar->setValue(0);
|
||||
progress_bar->setVisible(true);
|
||||
}
|
||||
|
||||
current_worker = std::make_unique<GameListWorker>(vfs, provider, game_dirs, compatibility_list, play_time_manager, system, main_window->GetMultiplayerState()->GetSession());
|
||||
connect(current_worker.get(), &GameListWorker::DataAvailable, this, &GameList::WorkerEvent, Qt::QueuedConnection);
|
||||
|
||||
if (progress_bar) {
|
||||
connect(current_worker.get(), &GameListWorker::ProgressUpdated, progress_bar, &QProgressBar::setValue, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
QThreadPool::globalInstance()->start(current_worker.get());
|
||||
}
|
||||
|
||||
@@ -1312,15 +1345,16 @@ void GameList::LoadInterfaceLayout() {
|
||||
}
|
||||
|
||||
const QStringList GameList::supported_file_extensions = {
|
||||
QStringLiteral("nso"), QStringLiteral("nro"), QStringLiteral("nca"),
|
||||
QStringLiteral("xci"), QStringLiteral("nsp"), QStringLiteral("kip")};
|
||||
QStringLiteral("xci"), QStringLiteral("nsp"),
|
||||
QStringLiteral("nso"), QStringLiteral("nro"), QStringLiteral("kip")
|
||||
};
|
||||
|
||||
void GameList::RefreshGameDirectory() {
|
||||
if (!UISettings::values.game_dirs.empty() && current_worker != nullptr) {
|
||||
LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
|
||||
PopulateAsync(UISettings::values.game_dirs);
|
||||
}
|
||||
void GameList::RefreshGameDirectory() {
|
||||
if (!UISettings::values.game_dirs.empty() && current_worker != nullptr) {
|
||||
LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
|
||||
PopulateAsync(UISettings::values.game_dirs);
|
||||
}
|
||||
}
|
||||
|
||||
void GameList::ToggleFavorite(u64 program_id) {
|
||||
if (!UISettings::values.favorited_ids.contains(program_id)) {
|
||||
@@ -1588,3 +1622,26 @@ const QStringList GameList::supported_file_extensions = {
|
||||
}
|
||||
btn_sort_az->setIcon(sort_icon);
|
||||
}
|
||||
|
||||
void GameList::UpdateProgressBarColor() {
|
||||
if (!progress_bar) return;
|
||||
|
||||
// Convert the Hex String from settings to a QColor
|
||||
QColor accent(QString::fromStdString(UISettings::values.accent_color.GetValue()));
|
||||
|
||||
if (UISettings::values.enable_rainbow_mode.GetValue()) {
|
||||
progress_bar->setStyleSheet(QStringLiteral(
|
||||
"QProgressBar { border: none; background: transparent; } "
|
||||
"QProgressBar::chunk { "
|
||||
"background: qlineargradient(x1:0, y1:0, x2:1, y2:0, "
|
||||
"stop:0 #ff0000, stop:0.16 #ffff00, stop:0.33 #00ff00, "
|
||||
"stop:0.5 #00ffff, stop:0.66 #0000ff, stop:0.83 #ff00ff, stop:1 #ff0000); "
|
||||
"}"
|
||||
));
|
||||
} else {
|
||||
progress_bar->setStyleSheet(QStringLiteral(
|
||||
"QProgressBar { border: none; background: transparent; } "
|
||||
"QProgressBar::chunk { background-color: %1; }"
|
||||
).arg(accent.name()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <QListView>
|
||||
#include <QPushButton>
|
||||
#include <QSlider>
|
||||
#include <QProgressBar>
|
||||
#include <QStandardItemModel>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
@@ -146,6 +147,7 @@ public slots:
|
||||
void OnConfigurationChanged();
|
||||
|
||||
private slots:
|
||||
void UpdateProgressBarColor();
|
||||
void OnItemExpanded(const QModelIndex& item);
|
||||
void OnTextChanged(const QString& new_text);
|
||||
void OnFilterCloseClicked();
|
||||
@@ -201,6 +203,7 @@ private:
|
||||
QListView* list_view = nullptr;
|
||||
QStandardItemModel* item_model = nullptr;
|
||||
std::unique_ptr<GameListWorker> current_worker;
|
||||
QProgressBar* progress_bar = nullptr;
|
||||
QFileSystemWatcher* watcher = nullptr;
|
||||
ControllerNavigation* controller_navigation = nullptr;
|
||||
CompatibilityList compatibility_list;
|
||||
|
||||
@@ -349,7 +349,19 @@ void GetMetadataFromControlNCA(const FileSys::PatchManager& patch_manager, const
|
||||
|
||||
bool HasSupportedFileExtension(const std::string& file_name) {
|
||||
const QFileInfo file = QFileInfo(QString::fromStdString(file_name));
|
||||
return GameList::supported_file_extensions.contains(file.suffix(), Qt::CaseInsensitive);
|
||||
const QString suffix = file.suffix().toLower();
|
||||
|
||||
// 1. Check if it's a standard game container (.nsp, .xci, etc.)
|
||||
if (GameList::supported_file_extensions.contains(suffix)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Only allow .nca if the user explicitly enabled it in the UI Settings
|
||||
if (suffix == QStringLiteral("nca") && UISettings::values.scan_nca.GetValue()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsExtractedNCAMain(const std::string& file_name) {
|
||||
@@ -551,145 +563,116 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir, const std::map
|
||||
}
|
||||
|
||||
void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan,
|
||||
GameListDir* parent_dir, const std::map<u64, std::pair<int, int>>& online_stats) {
|
||||
const auto callback = [this, target, parent_dir, &online_stats](const std::filesystem::path& path) -> bool {
|
||||
if (stop_requested) {
|
||||
// Breaks the callback loop.
|
||||
return false;
|
||||
}
|
||||
GameListDir* parent_dir, const std::map<u64, std::pair<int, int>>& online_stats,
|
||||
int& processed_files, int total_files) {
|
||||
const auto callback = [this, target, parent_dir, &online_stats, &processed_files, total_files](const std::filesystem::path& path) -> bool {
|
||||
if (stop_requested) 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))) {
|
||||
// Try to get cached metadata first
|
||||
const auto* cached_metadata = GetCachedGameMetadata(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;
|
||||
}
|
||||
|
||||
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 (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) {
|
||||
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(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, online_stats);
|
||||
|
||||
RecordEvent(
|
||||
[=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
|
||||
}
|
||||
} else {
|
||||
// Use cached metadata if available, otherwise read from file
|
||||
std::vector<u8> icon;
|
||||
std::string name = " ";
|
||||
std::size_t file_size = 0;
|
||||
|
||||
if (cached_metadata && cached_metadata->program_id == program_id) {
|
||||
// Use cached data
|
||||
icon = cached_metadata->icon;
|
||||
name = cached_metadata->title;
|
||||
file_size = cached_metadata->file_size;
|
||||
} else {
|
||||
// Read from file
|
||||
[[maybe_unused]] const auto res1 = loader->ReadIcon(icon);
|
||||
[[maybe_unused]] const auto res3 = loader->ReadTitle(name);
|
||||
file_size = Common::FS::GetSize(physical_name);
|
||||
|
||||
// Cache it for next time
|
||||
if (res2 == Loader::ResultStatus::Success) {
|
||||
CacheGameMetadata(physical_name, program_id, file_type, file_size, name, icon);
|
||||
}
|
||||
}
|
||||
|
||||
const FileSys::PatchManager patch{program_id, system.GetFileSystemController(),
|
||||
system.GetContentProvider()};
|
||||
|
||||
auto entry = MakeGameListEntry(
|
||||
physical_name, name, file_size, icon, *loader,
|
||||
program_id, compatibility_list, play_time_manager, patch, online_stats);
|
||||
|
||||
RecordEvent(
|
||||
[=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
|
||||
}
|
||||
}
|
||||
} else if (is_dir) {
|
||||
watch_list.append(QString::fromStdString(physical_name));
|
||||
if (physical_name.find("/nand/") != std::string::npos ||
|
||||
physical_name.find("\\nand\\") != std::string::npos ||
|
||||
physical_name.find("/registered/") != std::string::npos ||
|
||||
physical_name.find("\\registered\\") != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!HasSupportedFileExtension(physical_name) && !IsExtractedNCAMain(physical_name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cache Check
|
||||
const auto* cached = GetCachedGameMetadata(physical_name);
|
||||
if (cached && cached->IsValid() && (target == ScanTarget::PopulateGameList || target == ScanTarget::Both)) {
|
||||
if ((cached->program_id & 0xFFF) == 0) {
|
||||
const FileSys::PatchManager patch{cached->program_id, system.GetFileSystemController(), system.GetContentProvider()};
|
||||
auto file = vfs->OpenFile(physical_name, FileSys::OpenMode::Read);
|
||||
if (file) {
|
||||
auto loader = Loader::GetLoader(system, file);
|
||||
if (loader) {
|
||||
auto entry = MakeGameListEntry(physical_name, cached->title, cached->file_size, cached->icon, *loader,
|
||||
cached->program_id, compatibility_list, play_time_manager, patch, online_stats);
|
||||
RecordEvent([=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
|
||||
}
|
||||
}
|
||||
}
|
||||
processed_files++;
|
||||
emit ProgressUpdated(std::min(100, (processed_files * 100) / total_files));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Full Scan
|
||||
const auto file = vfs->OpenFile(physical_name, FileSys::OpenMode::Read);
|
||||
if (!file) {
|
||||
processed_files++;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto loader = Loader::GetLoader(system, file);
|
||||
if (!loader) {
|
||||
processed_files++;
|
||||
return true;
|
||||
}
|
||||
|
||||
u64 program_id = 0;
|
||||
const auto res2 = loader->ReadProgramId(program_id);
|
||||
const auto file_type = loader->GetFileType();
|
||||
|
||||
if (res2 == Loader::ResultStatus::Success && program_id != 0) {
|
||||
|
||||
if (target == ScanTarget::FillManualContentProvider || target == ScanTarget::Both) {
|
||||
if (file_type == Loader::FileType::NCA) {
|
||||
provider->AddEntry(FileSys::TitleType::Application, FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()), program_id, file);
|
||||
} else if (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) {
|
||||
provider->AddEntry(entry.first.first, entry.first.second, title.first, entry.second->GetBaseFile());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (target == ScanTarget::PopulateGameList || target == ScanTarget::Both) {
|
||||
// 3. FILTER UPDATES: Only add to UI if it's a Base Game (ID ends in 000)
|
||||
if ((program_id & 0xFFF) == 0) {
|
||||
std::vector<u8> icon;
|
||||
std::string name = " ";
|
||||
loader->ReadIcon(icon);
|
||||
loader->ReadTitle(name);
|
||||
std::size_t file_size = Common::FS::GetSize(physical_name);
|
||||
|
||||
CacheGameMetadata(physical_name, program_id, file_type, file_size, name, icon);
|
||||
|
||||
const FileSys::PatchManager patch{program_id, system.GetFileSystemController(), system.GetContentProvider()};
|
||||
auto entry = MakeGameListEntry(physical_name, name, file_size, icon, *loader, program_id, compatibility_list, play_time_manager, patch, online_stats);
|
||||
RecordEvent([=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processed_files++;
|
||||
emit ProgressUpdated(std::min(100, (processed_files * 100) / total_files));
|
||||
return true;
|
||||
};
|
||||
|
||||
if (deep_scan) {
|
||||
Common::FS::IterateDirEntriesRecursively(dir_path, callback,
|
||||
Common::FS::DirEntryFilter::All);
|
||||
Common::FS::IterateDirEntriesRecursively(dir_path, callback, Common::FS::DirEntryFilter::File);
|
||||
} else {
|
||||
Common::FS::IterateDirEntries(dir_path, callback, Common::FS::DirEntryFilter::File);
|
||||
}
|
||||
}
|
||||
|
||||
void GameListWorker::run() {
|
||||
// Load cached game metadata at the start
|
||||
LoadGameMetadataCache();
|
||||
|
||||
std::map<u64, std::pair<int, int>> online_stats; // Game ID -> {player_count, server_count}
|
||||
std::map<u64, std::pair<int, int>> online_stats;
|
||||
if (session) {
|
||||
AnnounceMultiplayerRoom::RoomList room_list = session->GetRoomList();
|
||||
for (const auto& room : room_list) {
|
||||
u64 game_id = room.information.preferred_game.id;
|
||||
if (game_id != 0) {
|
||||
online_stats[game_id].first += room.members.size();
|
||||
online_stats[game_id].first += (int)room.members.size();
|
||||
online_stats[game_id].second++;
|
||||
}
|
||||
}
|
||||
@@ -698,42 +681,46 @@ void GameListWorker::run() {
|
||||
watch_list.clear();
|
||||
provider->ClearAllEntries();
|
||||
|
||||
int total_files = 0;
|
||||
int processed_files = 0;
|
||||
|
||||
for (const auto& game_dir : game_dirs) {
|
||||
if (game_dir.path == "SDMC" || game_dir.path == "UserNAND" || game_dir.path == "SysNAND") continue;
|
||||
|
||||
auto count_callback = [&](const std::filesystem::path& path) -> bool {
|
||||
const std::string physical_name = Common::FS::PathToUTF8String(path);
|
||||
if (HasSupportedFileExtension(physical_name)) {
|
||||
total_files++;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (game_dir.deep_scan) {
|
||||
Common::FS::IterateDirEntriesRecursively(game_dir.path, count_callback, Common::FS::DirEntryFilter::File);
|
||||
} else {
|
||||
Common::FS::IterateDirEntries(game_dir.path, count_callback, Common::FS::DirEntryFilter::File);
|
||||
}
|
||||
}
|
||||
|
||||
if (total_files <= 0) total_files = 1;
|
||||
|
||||
const auto DirEntryReady = [&](GameListDir* game_list_dir) {
|
||||
RecordEvent([=](GameList* game_list) { game_list->AddDirEntry(game_list_dir); });
|
||||
};
|
||||
|
||||
for (UISettings::GameDir& game_dir : game_dirs) {
|
||||
if (stop_requested) {
|
||||
break;
|
||||
}
|
||||
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, online_stats);
|
||||
} 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, online_stats);
|
||||
} 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, online_stats);
|
||||
} else {
|
||||
watch_list.append(QString::fromStdString(game_dir.path));
|
||||
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, online_stats);
|
||||
ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path, game_dir.deep_scan,
|
||||
game_list_dir, online_stats);
|
||||
}
|
||||
if (game_dir.path == "SDMC" || game_dir.path == "UserNAND" || game_dir.path == "SysNAND") continue;
|
||||
|
||||
watch_list.append(QString::fromStdString(game_dir.path));
|
||||
auto* const game_list_dir = new GameListDir(game_dir);
|
||||
DirEntryReady(game_list_dir);
|
||||
|
||||
ScanFileSystem(ScanTarget::Both, game_dir.path, game_dir.deep_scan, game_list_dir, online_stats, processed_files, total_files);
|
||||
}
|
||||
|
||||
RecordEvent([this](GameList* game_list) { game_list->DonePopulating(watch_list); });
|
||||
|
||||
// Save cached game metadata at the end
|
||||
SaveGameMetadataCache();
|
||||
|
||||
processing_completed.Set();
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ public:
|
||||
enum class ScanTarget {
|
||||
FillManualContentProvider,
|
||||
PopulateGameList,
|
||||
Both,
|
||||
};
|
||||
|
||||
explicit GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
|
||||
@@ -73,6 +74,7 @@ public:
|
||||
|
||||
signals:
|
||||
void DataAvailable();
|
||||
void ProgressUpdated(int percent);
|
||||
|
||||
private:
|
||||
template <typename F>
|
||||
@@ -84,7 +86,8 @@ private:
|
||||
|
||||
void ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan,
|
||||
GameListDir* parent_dir,
|
||||
const std::map<u64, std::pair<int, int>>& online_stats);
|
||||
const std::map<u64, std::pair<int, int>>& online_stats,
|
||||
int& processed_files, int total_files);
|
||||
|
||||
std::shared_ptr<FileSys::VfsFilesystem> vfs;
|
||||
FileSys::ManualContentProvider* provider;
|
||||
|
||||
@@ -2272,11 +2272,9 @@ void GMainWindow::OnEmulationStopped() {
|
||||
render_window->hide();
|
||||
loading_screen->hide();
|
||||
loading_screen->Clear();
|
||||
if (game_list->IsEmpty()) {
|
||||
game_list_placeholder->show();
|
||||
} else {
|
||||
game_list->show();
|
||||
}
|
||||
|
||||
game_list->show();
|
||||
game_list_placeholder->hide();
|
||||
game_list->SetFilterFocus();
|
||||
tas_label->clear();
|
||||
input_subsystem->GetTas()->Stop();
|
||||
|
||||
@@ -219,6 +219,7 @@ namespace UISettings {
|
||||
Setting<bool> game_list_grid_view{linkage, false, "game_list_grid_view", Category::UiGameList};
|
||||
std::atomic_bool is_game_list_reload_pending{false};
|
||||
Setting<bool> cache_game_list{linkage, true, "cache_game_list", Category::UiGameList};
|
||||
Setting<bool> scan_nca{linkage, false, "scan_nca", Category::UiGameList};
|
||||
Setting<bool> prompt_for_autoloader{linkage, true, "prompt_for_autoloader", Category::UiGameList};
|
||||
Setting<bool> favorites_expanded{linkage, true, "favorites_expanded", Category::UiGameList};
|
||||
QVector<u64> favorited_ids;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "common/fs/file.h"
|
||||
@@ -324,7 +325,6 @@ bool RemoveDirContentsRecursively(const fs::path& path) {
|
||||
|
||||
std::error_code ec;
|
||||
|
||||
// TODO (Morph): Replace this with recursive_directory_iterator once it's fixed in MSVC.
|
||||
for (const auto& entry : fs::directory_iterator(path, ec)) {
|
||||
if (ec) {
|
||||
LOG_ERROR(Common_Filesystem,
|
||||
@@ -342,10 +342,11 @@ bool RemoveDirContentsRecursively(const fs::path& path) {
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO (Morph): Remove this when MSVC fixes recursive_directory_iterator.
|
||||
// recursive_directory_iterator throws an exception despite passing in a std::error_code.
|
||||
if (entry.status().type() == fs::file_type::directory) {
|
||||
return RemoveDirContentsRecursively(entry.path());
|
||||
std::error_code status_ec;
|
||||
if (entry.status(status_ec).type() == fs::file_type::directory) {
|
||||
if (!RemoveDirContentsRecursively(entry.path())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,8 +435,12 @@ void IterateDirEntries(const std::filesystem::path& path, const DirEntryCallable
|
||||
break;
|
||||
}
|
||||
|
||||
std::error_code status_ec;
|
||||
const auto st = entry.status(status_ec);
|
||||
if (status_ec) continue;
|
||||
|
||||
if (True(filter & DirEntryFilter::File) &&
|
||||
entry.status().type() == fs::file_type::regular) {
|
||||
st.type() == fs::file_type::regular) {
|
||||
if (!callback(entry)) {
|
||||
callback_error = true;
|
||||
break;
|
||||
@@ -443,7 +448,7 @@ void IterateDirEntries(const std::filesystem::path& path, const DirEntryCallable
|
||||
}
|
||||
|
||||
if (True(filter & DirEntryFilter::Directory) &&
|
||||
entry.status().type() == fs::file_type::directory) {
|
||||
st.type() == fs::file_type::directory) {
|
||||
if (!callback(entry)) {
|
||||
callback_error = true;
|
||||
break;
|
||||
@@ -462,6 +467,42 @@ void IterateDirEntries(const std::filesystem::path& path, const DirEntryCallable
|
||||
PathToUTF8String(path));
|
||||
}
|
||||
|
||||
void IterateDirEntriesRecursivelyInternal(const std::filesystem::path& path,
|
||||
const DirEntryCallable& callback, DirEntryFilter filter,
|
||||
int depth) {
|
||||
if (depth > 12) return;
|
||||
|
||||
std::error_code ec;
|
||||
auto it = fs::directory_iterator(path, ec);
|
||||
if (ec) return;
|
||||
|
||||
while (it != fs::directory_iterator() && !ec) {
|
||||
const auto& entry = *it;
|
||||
|
||||
#ifndef _WIN32
|
||||
const std::string filename = entry.path().filename().string();
|
||||
if (filename[0] == '$' || filename == "Windows" || filename == "Program Files" ||
|
||||
filename == "Program Files (x86)" || filename == "System Volume Information" ||
|
||||
filename == "ProgramData" || filename == "Application Data" ||
|
||||
filename == "Users" || filename == "SteamLibrary") {
|
||||
it.increment(ec);
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
|
||||
std::error_code status_ec;
|
||||
if (entry.is_directory(status_ec)) {
|
||||
if (True(filter & DirEntryFilter::Directory)) { if (!callback(entry)) break; }
|
||||
IterateDirEntriesRecursivelyInternal(entry.path(), callback, filter, depth + 1);
|
||||
} else {
|
||||
if (True(filter & DirEntryFilter::File)) { if (!callback(entry)) break; }
|
||||
}
|
||||
|
||||
it.increment(ec);
|
||||
if (ec) { ec.clear(); break; }
|
||||
}
|
||||
}
|
||||
|
||||
void IterateDirEntriesRecursively(const std::filesystem::path& path,
|
||||
const DirEntryCallable& callback, DirEntryFilter filter) {
|
||||
if (!ValidatePath(path)) {
|
||||
@@ -469,59 +510,10 @@ void IterateDirEntriesRecursively(const std::filesystem::path& path,
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Exists(path)) {
|
||||
LOG_ERROR(Common_Filesystem, "Filesystem object at path={} does not exist",
|
||||
PathToUTF8String(path));
|
||||
return;
|
||||
}
|
||||
// Start the recursion at depth 0
|
||||
IterateDirEntriesRecursivelyInternal(path, callback, filter, 0);
|
||||
|
||||
if (!IsDir(path)) {
|
||||
LOG_ERROR(Common_Filesystem, "Filesystem object at path={} is not a directory",
|
||||
PathToUTF8String(path));
|
||||
return;
|
||||
}
|
||||
|
||||
bool callback_error = false;
|
||||
|
||||
std::error_code ec;
|
||||
|
||||
// TODO (Morph): Replace this with recursive_directory_iterator once it's fixed in MSVC.
|
||||
for (const auto& entry : fs::directory_iterator(path, ec)) {
|
||||
if (ec) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (True(filter & DirEntryFilter::File) &&
|
||||
entry.status().type() == fs::file_type::regular) {
|
||||
if (!callback(entry)) {
|
||||
callback_error = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (True(filter & DirEntryFilter::Directory) &&
|
||||
entry.status().type() == fs::file_type::directory) {
|
||||
if (!callback(entry)) {
|
||||
callback_error = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO (Morph): Remove this when MSVC fixes recursive_directory_iterator.
|
||||
// recursive_directory_iterator throws an exception despite passing in a std::error_code.
|
||||
if (entry.status().type() == fs::file_type::directory) {
|
||||
IterateDirEntriesRecursively(entry.path(), callback, filter);
|
||||
}
|
||||
}
|
||||
|
||||
if (callback_error || ec) {
|
||||
LOG_ERROR(Common_Filesystem,
|
||||
"Failed to visit all the directory entries of path={}, ec_message={}",
|
||||
PathToUTF8String(path), ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DEBUG(Common_Filesystem, "Successfully visited all the directory entries of path={}",
|
||||
LOG_DEBUG(Common_Filesystem, "Finished visiting directory entries of path={}",
|
||||
PathToUTF8String(path));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
@@ -399,6 +400,10 @@ void IterateDirEntriesRecursively(const std::filesystem::path& path,
|
||||
const DirEntryCallable& callback,
|
||||
DirEntryFilter filter = DirEntryFilter::All);
|
||||
|
||||
void IterateDirEntriesRecursivelyInternal(const std::filesystem::path& path,
|
||||
const DirEntryCallable& callback,
|
||||
DirEntryFilter filter, int depth);
|
||||
|
||||
#ifdef _WIN32
|
||||
template <typename Path>
|
||||
void IterateDirEntriesRecursively(const Path& path, const DirEntryCallable& callback,
|
||||
|
||||
Reference in New Issue
Block a user