diff --git a/src/citron/configuration/configure_filesystem.cpp b/src/citron/configuration/configure_filesystem.cpp
index f2d61681b..bab26c910 100644
--- a/src/citron/configuration/configure_filesystem.cpp
+++ b/src/citron/configuration/configure_filesystem.cpp
@@ -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();
diff --git a/src/citron/configuration/configure_filesystem.ui b/src/citron/configuration/configure_filesystem.ui
index 3fe6b8fcf..18d085c9a 100644
--- a/src/citron/configuration/configure_filesystem.ui
+++ b/src/citron/configuration/configure_filesystem.ui
@@ -7,294 +7,90 @@
0
0
453
- 561
+ 650
Form
-
- Filesystem
-
-
-
-
-
-
-
- Storage Directories
-
-
-
-
-
-
- NAND
-
-
-
- -
-
-
- ...
-
-
-
- -
-
-
- -
-
-
- -
-
-
- SD Card
-
-
-
- -
-
-
- ...
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Maximum
-
-
-
- 60
- 20
-
-
-
-
-
-
-
- -
-
-
- Gamecard
-
-
-
-
-
-
- Path
-
-
-
- -
-
-
- -
-
-
- Inserted
-
-
-
- -
-
-
- Current Game
-
-
-
- -
-
-
- ...
-
-
-
-
-
-
- -
-
-
- Patch Manager
-
-
-
-
-
-
- -
-
-
- -
-
-
- ...
-
-
-
- -
-
-
- ...
-
-
-
- -
-
-
-
-
-
- Dump Decompressed NSOs
-
-
-
- -
-
-
- Dump ExeFS
-
-
-
-
-
- -
-
-
- Mod Load Root
-
-
-
- -
-
-
- Dump Root
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 40
- 20
-
-
-
-
-
-
-
- -
-
-
- Autoloader
-
-
-
-
-
-
- Prompt to run Autoloader when a new game directory is added
-
-
-
- -
-
-
- Run Autoloader Now
-
-
-
-
-
-
- -
-
-
- Updater
-
-
-
-
-
-
- Enable AppImage Backups
-
-
-
- -
-
-
- Use Custom Backup Location for AppImage Updates
-
-
-
- -
-
-
- -
-
-
- ...
-
-
-
-
-
-
- -
-
-
- Caching
-
-
-
-
-
-
-
-
-
- Cache Game List Metadata
-
-
-
- -
-
-
- Reset Metadata Cache
-
-
-
-
-
-
-
-
-
+
+
+ Storage Directories
+
+
+ - NAND
+
+ - ...
+ - SD Card
+
+ - ...
+
+
-
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
+
+ Gamecard
+
+
- Inserted
+ - Current Game
+ - Path
+
+ - ...
+
+
+ -
+
+ Patch Manager
+
+
- Dump Root
+
+ - ...
+ - Mod Load Root
+
+ - ...
+ -
+
+
- Dump Decompressed NSOs
+ - Dump ExeFS
+
+
+
+
+
+ -
+
+ Autoloader
+
+
- Prompt to run Autoloader when a new game directory is added
+ - Run Autoloader Now
+
+
+
+ -
+
+ Updater
+
+
- Enable AppImage Backups
+ - Use Custom Backup Location
+
+ - ...
+
+
+
+ -
+
+ Caching & Scanning
+
+
- Cache Game List Metadata
+ - Reset Metadata Cache
+ - Scan for .nca files (Advanced: Significantly slows down scanning)
+
+
+
+ - Qt::Vertical
-
-
diff --git a/src/citron/game_list.cpp b/src/citron/game_list.cpp
index e5ad53c22..7deb9c445 100644
--- a/src/citron/game_list.cpp
+++ b/src/citron/game_list.cpp
@@ -21,6 +21,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -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>> future = QtConcurrent::run([session]() {
- std::map> 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> 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>{};
}
- 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& 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& 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(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()));
+ }
+}
diff --git a/src/citron/game_list.h b/src/citron/game_list.h
index 06554d939..7438a6e78 100644
--- a/src/citron/game_list.h
+++ b/src/citron/game_list.h
@@ -14,6 +14,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -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 current_worker;
+ QProgressBar* progress_bar = nullptr;
QFileSystemWatcher* watcher = nullptr;
ControllerNavigation* controller_navigation = nullptr;
CompatibilityList compatibility_list;
diff --git a/src/citron/game_list_worker.cpp b/src/citron/game_list_worker.cpp
index 51a7085f4..b97225258 100644
--- a/src/citron/game_list_worker.cpp
+++ b/src/citron/game_list_worker.cpp
@@ -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>& 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>& 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(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 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 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 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(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 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> online_stats; // Game ID -> {player_count, server_count}
+ std::map> 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();
}
diff --git a/src/citron/game_list_worker.h b/src/citron/game_list_worker.h
index 723bbeb51..8ad673eeb 100644
--- a/src/citron/game_list_worker.h
+++ b/src/citron/game_list_worker.h
@@ -47,6 +47,7 @@ public:
enum class ScanTarget {
FillManualContentProvider,
PopulateGameList,
+ Both,
};
explicit GameListWorker(std::shared_ptr vfs_,
@@ -73,6 +74,7 @@ public:
signals:
void DataAvailable();
+ void ProgressUpdated(int percent);
private:
template
@@ -84,7 +86,8 @@ private:
void ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan,
GameListDir* parent_dir,
- const std::map>& online_stats);
+ const std::map>& online_stats,
+ int& processed_files, int total_files);
std::shared_ptr vfs;
FileSys::ManualContentProvider* provider;
diff --git a/src/citron/main.cpp b/src/citron/main.cpp
index b0692c9b7..e1458fe59 100644
--- a/src/citron/main.cpp
+++ b/src/citron/main.cpp
@@ -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();
diff --git a/src/citron/uisettings.h b/src/citron/uisettings.h
index 36b615ebe..6aece0855 100644
--- a/src/citron/uisettings.h
+++ b/src/citron/uisettings.h
@@ -219,6 +219,7 @@ namespace UISettings {
Setting game_list_grid_view{linkage, false, "game_list_grid_view", Category::UiGameList};
std::atomic_bool is_game_list_reload_pending{false};
Setting cache_game_list{linkage, true, "cache_game_list", Category::UiGameList};
+ Setting scan_nca{linkage, false, "scan_nca", Category::UiGameList};
Setting prompt_for_autoloader{linkage, true, "prompt_for_autoloader", Category::UiGameList};
Setting favorites_expanded{linkage, true, "favorites_expanded", Category::UiGameList};
QVector favorited_ids;
diff --git a/src/common/fs/fs.cpp b/src/common/fs/fs.cpp
index 174aed49b..71ac8c27a 100644
--- a/src/common/fs/fs.cpp
+++ b/src/common/fs/fs.cpp
@@ -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));
}
diff --git a/src/common/fs/fs.h b/src/common/fs/fs.h
index ce3eb309a..42b66ea8d 100644
--- a/src/common/fs/fs.h
+++ b/src/common/fs/fs.h
@@ -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
void IterateDirEntriesRecursively(const Path& path, const DirEntryCallable& callback,