From 8ee84ee519a385b9edc2ad1e00cf8881d8004beb Mon Sep 17 00:00:00 2001 From: Collecting Date: Fri, 19 Dec 2025 18:40:19 +0000 Subject: [PATCH 01/19] fs: Fix Directory Scanning w/ NTFS Signed-off-by: Collecting --- src/common/fs/fs.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/common/fs/fs.h b/src/common/fs/fs.h index ce3eb309a..058cd498b 100644 --- a/src/common/fs/fs.h +++ b/src/common/fs/fs.h @@ -399,6 +399,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, From 9d68c1b1f6730fe545d7d381ba1696322b0320c1 Mon Sep 17 00:00:00 2001 From: Collecting Date: Fri, 19 Dec 2025 18:41:20 +0000 Subject: [PATCH 02/19] fix: Directory Scanning w/ Linux (add Copyright) Signed-off-by: Collecting --- src/common/fs/fs.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/fs/fs.h b/src/common/fs/fs.h index 058cd498b..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 From 3b5c2e86ae712af1afb0dd63c98558a1aea07bde Mon Sep 17 00:00:00 2001 From: Collecting Date: Fri, 19 Dec 2025 18:43:51 +0000 Subject: [PATCH 03/19] fix: NTFS Directory Scanning on Linux Signed-off-by: Collecting --- src/common/fs/fs.cpp | 110 ++++++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 59 deletions(-) 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)); } From c4cf225d12c113fee83bac71f53997ad6093498c Mon Sep 17 00:00:00 2001 From: Collecting Date: Fri, 19 Dec 2025 18:45:28 +0000 Subject: [PATCH 04/19] fix: NTFS Directory Scanning w/ Linux Signed-off-by: Collecting --- src/citron/game_list.cpp | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/citron/game_list.cpp b/src/citron/game_list.cpp index e5ad53c22..2f3320593 100644 --- a/src/citron/game_list.cpp +++ b/src/citron/game_list.cpp @@ -749,16 +749,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); @@ -1312,15 +1317,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)) { From 0ff757b7a47db7a60dd610ff6000a212f875bf2f Mon Sep 17 00:00:00 2001 From: Collecting Date: Fri, 19 Dec 2025 18:46:26 +0000 Subject: [PATCH 05/19] fix: NTFS Directory Scanning w/ Linux Signed-off-by: Collecting --- src/citron/game_list_worker.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/citron/game_list_worker.h b/src/citron/game_list_worker.h index 723bbeb51..becf8e5bf 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_, From 0fd1e81f5f9cae0ad648a019889caf273212ec17 Mon Sep 17 00:00:00 2001 From: Collecting Date: Fri, 19 Dec 2025 18:48:17 +0000 Subject: [PATCH 06/19] fix: NTFS Directory Scanning w/ Linux Signed-off-by: Collecting --- src/citron/game_list_worker.cpp | 233 ++++++++++++++------------------ 1 file changed, 103 insertions(+), 130 deletions(-) diff --git a/src/citron/game_list_worker.cpp b/src/citron/game_list_worker.cpp index 51a7085f4..c6786d9d6 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) { @@ -553,127 +565,98 @@ 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; - } + 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)); + // Do not touch the disk if the extension isn't a game + if (!HasSupportedFileExtension(physical_name) && !IsExtractedNCAMain(physical_name)) { + return true; } + // Only now do we do "Heavy" disk I/O + 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); + + // Handle Content Provider AND Game List in one pass + if (target == ScanTarget::FillManualContentProvider || target == ScanTarget::Both) { + 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()); + } + } + } + } + + if (target == ScanTarget::PopulateGameList || target == ScanTarget::Both) { + 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) { + auto sub_loader = Loader::GetLoader(system, file, id); + if (!sub_loader) continue; + + std::vector icon; + sub_loader->ReadIcon(icon); + std::string name = " "; + sub_loader->ReadTitle(name); + + const FileSys::PatchManager patch{id, system.GetFileSystemController(), system.GetContentProvider()}; + auto entry = MakeGameListEntry(physical_name, name, Common::FS::GetSize(physical_name), icon, *sub_loader, id, compatibility_list, play_time_manager, patch, online_stats); + RecordEvent([=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); }); + } + } else { + const auto* cached_metadata = GetCachedGameMetadata(physical_name); + std::vector icon; + std::string name = " "; + std::size_t file_size = 0; + + if (cached_metadata && cached_metadata->program_id == program_id) { + icon = cached_metadata->icon; + name = cached_metadata->title; + file_size = cached_metadata->file_size; + } else { + loader->ReadIcon(icon); + loader->ReadTitle(name); + file_size = Common::FS::GetSize(physical_name); + 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); }); + } + } 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); } @@ -703,30 +686,20 @@ void GameListWorker::run() { }; 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); + if (game_dir.path == std::string("SDMC") || game_dir.path == std::string("UserNAND") || game_dir.path == std::string("SysNAND")) { + auto type = (game_dir.path == "SDMC") ? GameListItemType::SdmcDir : (game_dir.path == "UserNAND" ? GameListItemType::UserNandDir : GameListItemType::SysNandDir); + auto* const game_list_dir = new GameListDir(game_dir, type); 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); + + // ONE PASS SCAN: Replaces the two separate ScanFileSystem calls + ScanFileSystem(ScanTarget::Both, game_dir.path, game_dir.deep_scan, game_list_dir, online_stats); } } From 55e737e22f444aa58f7d45fe3dd70e12e3d60737 Mon Sep 17 00:00:00 2001 From: Collecting Date: Fri, 19 Dec 2025 18:49:30 +0000 Subject: [PATCH 07/19] fix: NTFS Directory Scanning w/ Linux (add .nca toggle) Signed-off-by: Collecting --- src/citron/uisettings.h | 1 + 1 file changed, 1 insertion(+) 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; From e347fb5c3a3b0e738c7955730bf708ae4e5af38a Mon Sep 17 00:00:00 2001 From: Collecting Date: Fri, 19 Dec 2025 18:50:59 +0000 Subject: [PATCH 08/19] fix: Add NCA Scanning Toggle for Game List Signed-off-by: Collecting --- src/citron/configuration/configure_filesystem.cpp | 6 ++++++ 1 file changed, 6 insertions(+) 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(); From 340d3ecb263ed0f2c50f9bb5f7137a0560d42a13 Mon Sep 17 00:00:00 2001 From: Collecting Date: Fri, 19 Dec 2025 18:51:56 +0000 Subject: [PATCH 09/19] fix: UI File w/ Added .nca Toggle Logic (file clean-up) Signed-off-by: Collecting --- .../configuration/configure_filesystem.ui | 352 ++++-------------- 1 file changed, 74 insertions(+), 278 deletions(-) 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 - - From c93a43f8727e9f9bb2d519c55539bd76ef35f79c Mon Sep 17 00:00:00 2001 From: Collecting Date: Fri, 19 Dec 2025 20:50:43 +0000 Subject: [PATCH 10/19] fs(ui): Include Progress Bar Signed-off-by: Collecting --- src/citron/game_list_worker.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/citron/game_list_worker.h b/src/citron/game_list_worker.h index becf8e5bf..8ad673eeb 100644 --- a/src/citron/game_list_worker.h +++ b/src/citron/game_list_worker.h @@ -74,6 +74,7 @@ public: signals: void DataAvailable(); + void ProgressUpdated(int percent); private: template @@ -85,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; From 9650077ade368ae4d839fba500558758fc88a61a Mon Sep 17 00:00:00 2001 From: Collecting Date: Fri, 19 Dec 2025 20:51:55 +0000 Subject: [PATCH 11/19] fs(ui): Include Progress Bar Signed-off-by: Collecting --- src/citron/game_list_worker.cpp | 38 ++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/citron/game_list_worker.cpp b/src/citron/game_list_worker.cpp index c6786d9d6..e02f9d4ac 100644 --- a/src/citron/game_list_worker.cpp +++ b/src/citron/game_list_worker.cpp @@ -563,8 +563,9 @@ 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 { + 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); @@ -574,6 +575,12 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa return true; } + // Update progress before heavy I/O + processed_files++; + if (total_files > 0) { + emit ProgressUpdated((processed_files * 100) / total_files); + } + // Only now do we do "Heavy" disk I/O const auto file = vfs->OpenFile(physical_name, FileSys::OpenMode::Read); if (!file) return true; @@ -672,7 +679,7 @@ void GameListWorker::run() { 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++; } } @@ -681,6 +688,27 @@ void GameListWorker::run() { watch_list.clear(); provider->ClearAllEntries(); + // 1. Pre-scan to count total games for accurate progress + 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 { + if (HasSupportedFileExtension(Common::FS::PathToUTF8String(path)) || IsExtractedNCAMain(Common::FS::PathToUTF8String(path))) { + 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); + } + } + const auto DirEntryReady = [&](GameListDir* game_list_dir) { RecordEvent([=](GameList* game_list) { game_list->AddDirEntry(game_list_dir); }); }; @@ -688,7 +716,7 @@ void GameListWorker::run() { for (UISettings::GameDir& game_dir : game_dirs) { if (stop_requested) break; - if (game_dir.path == std::string("SDMC") || game_dir.path == std::string("UserNAND") || game_dir.path == std::string("SysNAND")) { + if (game_dir.path == "SDMC" || game_dir.path == "UserNAND" || game_dir.path == "SysNAND") { auto type = (game_dir.path == "SDMC") ? GameListItemType::SdmcDir : (game_dir.path == "UserNAND" ? GameListItemType::UserNandDir : GameListItemType::SysNandDir); auto* const game_list_dir = new GameListDir(game_dir, type); DirEntryReady(game_list_dir); @@ -699,7 +727,7 @@ void GameListWorker::run() { DirEntryReady(game_list_dir); // ONE PASS SCAN: Replaces the two separate ScanFileSystem calls - ScanFileSystem(ScanTarget::Both, game_dir.path, game_dir.deep_scan, game_list_dir, online_stats); + ScanFileSystem(ScanTarget::Both, game_dir.path, game_dir.deep_scan, game_list_dir, online_stats, processed_files, total_files); } } From b7a1e23bb997269bf1f707324fa7bf80a9b1e6d4 Mon Sep 17 00:00:00 2001 From: Collecting Date: Fri, 19 Dec 2025 20:52:50 +0000 Subject: [PATCH 12/19] fs(ui): Include Progress Bar Signed-off-by: Collecting --- src/citron/game_list.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/citron/game_list.h b/src/citron/game_list.h index 06554d939..e5d255cba 100644 --- a/src/citron/game_list.h +++ b/src/citron/game_list.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -201,6 +202,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; From 68328293bbe5b06e96e46c437d35745123f42a43 Mon Sep 17 00:00:00 2001 From: Collecting Date: Fri, 19 Dec 2025 20:54:00 +0000 Subject: [PATCH 13/19] fs(ui): Include Progress Bar Signed-off-by: Collecting --- src/citron/game_list.cpp | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/citron/game_list.cpp b/src/citron/game_list.cpp index 2f3320593..085e75fdc 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); @@ -864,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()); @@ -1298,8 +1313,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()); } From 62f2d322b1e01ba0fcdcbb649dda94280838cf70 Mon Sep 17 00:00:00 2001 From: Collecting Date: Sun, 21 Dec 2025 00:37:20 +0000 Subject: [PATCH 14/19] fix: Get rid of placeholder Signed-off-by: Collecting --- src/citron/main.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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(); From fea9455ee71b55dbb7eb7f9f16b2cdb53b6b4a35 Mon Sep 17 00:00:00 2001 From: Collecting Date: Sun, 21 Dec 2025 00:38:09 +0000 Subject: [PATCH 15/19] fix: Show Gamelist Repopulation when Entering Main Citron Menu Signed-off-by: Collecting --- src/citron/game_list.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/citron/game_list.cpp b/src/citron/game_list.cpp index 085e75fdc..cb2e1ae7e 100644 --- a/src/citron/game_list.cpp +++ b/src/citron/game_list.cpp @@ -1304,6 +1304,7 @@ QStandardItemModel* GameList::GetModel() const { void GameList::PopulateAsync(QVector& game_dirs) { 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); From e6872aadb52b7f407f75f713f9f17fb7c2024913 Mon Sep 17 00:00:00 2001 From: Collecting Date: Sun, 21 Dec 2025 01:10:15 +0000 Subject: [PATCH 16/19] fix: Optimize JSON Signed-off-by: Collecting --- src/citron/game_list_worker.cpp | 123 ++++++++++++++------------------ 1 file changed, 53 insertions(+), 70 deletions(-) diff --git a/src/citron/game_list_worker.cpp b/src/citron/game_list_worker.cpp index e02f9d4ac..86c245e73 100644 --- a/src/citron/game_list_worker.cpp +++ b/src/citron/game_list_worker.cpp @@ -570,95 +570,77 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa const auto physical_name = Common::FS::PathToUTF8String(path); - // Do not touch the disk if the extension isn't a game if (!HasSupportedFileExtension(physical_name) && !IsExtractedNCAMain(physical_name)) { return true; } - // Update progress before heavy I/O - processed_files++; - if (total_files > 0) { - emit ProgressUpdated((processed_files * 100) / total_files); + // Cache Check + const auto* cached = GetCachedGameMetadata(physical_name); + if (cached && cached->IsValid() && (target == ScanTarget::PopulateGameList || target == ScanTarget::Both)) { + 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; + } + } } - // Only now do we do "Heavy" disk I/O + // Full Scan const auto file = vfs->OpenFile(physical_name, FileSys::OpenMode::Read); - if (!file) return true; + if (!file) { + processed_files++; + 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) { + if (!loader) { + processed_files++; return true; } u64 program_id = 0; const auto res2 = loader->ReadProgramId(program_id); + const auto file_type = loader->GetFileType(); - // Handle Content Provider AND Game List in one pass if (target == ScanTarget::FillManualContentProvider || target == ScanTarget::Both) { 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(); + 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()); + provider->AddEntry(entry.first.first, entry.first.second, title.first, entry.second->GetBaseFile()); } } } } if (target == ScanTarget::PopulateGameList || target == ScanTarget::Both) { - std::vector program_ids; - loader->ReadProgramIds(program_ids); + std::vector icon; + std::string name = " "; + loader->ReadIcon(icon); + loader->ReadTitle(name); + std::size_t file_size = Common::FS::GetSize(physical_name); - 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) { - auto sub_loader = Loader::GetLoader(system, file, id); - if (!sub_loader) continue; - - std::vector icon; - sub_loader->ReadIcon(icon); - std::string name = " "; - sub_loader->ReadTitle(name); - - const FileSys::PatchManager patch{id, system.GetFileSystemController(), system.GetContentProvider()}; - auto entry = MakeGameListEntry(physical_name, name, Common::FS::GetSize(physical_name), icon, *sub_loader, id, compatibility_list, play_time_manager, patch, online_stats); - RecordEvent([=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); }); - } - } else { - const auto* cached_metadata = GetCachedGameMetadata(physical_name); - std::vector icon; - std::string name = " "; - std::size_t file_size = 0; - - if (cached_metadata && cached_metadata->program_id == program_id) { - icon = cached_metadata->icon; - name = cached_metadata->title; - file_size = cached_metadata->file_size; - } else { - loader->ReadIcon(icon); - loader->ReadTitle(name); - file_size = Common::FS::GetSize(physical_name); - 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); }); + 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); }); } + + processed_files++; + emit ProgressUpdated(std::min(100, (processed_files * 100) / total_files)); return true; }; @@ -670,10 +652,9 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa } 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) { @@ -688,15 +669,18 @@ void GameListWorker::run() { watch_list.clear(); provider->ClearAllEntries(); - // 1. Pre-scan to count total games for accurate progress 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; + if (game_dir.path == "SDMC" || game_dir.path == "UserNAND" || game_dir.path == "SysNAND") { + total_files += 5; + continue; + } auto count_callback = [&](const std::filesystem::path& path) -> bool { - if (HasSupportedFileExtension(Common::FS::PathToUTF8String(path)) || IsExtractedNCAMain(Common::FS::PathToUTF8String(path))) { + const std::string physical_name = Common::FS::PathToUTF8String(path); + if (HasSupportedFileExtension(physical_name) || IsExtractedNCAMain(physical_name)) { total_files++; } return true; @@ -709,6 +693,8 @@ void GameListWorker::run() { } } + if (total_files <= 0) total_files = 1; + const auto DirEntryReady = [&](GameListDir* game_list_dir) { RecordEvent([=](GameList* game_list) { game_list->AddDirEntry(game_list_dir); }); }; @@ -721,20 +707,17 @@ void GameListWorker::run() { auto* const game_list_dir = new GameListDir(game_dir, type); DirEntryReady(game_list_dir); AddTitlesToGameList(game_list_dir, online_stats); + processed_files += 5; // Match our placeholder count } else { watch_list.append(QString::fromStdString(game_dir.path)); auto* const game_list_dir = new GameListDir(game_dir); DirEntryReady(game_list_dir); - // ONE PASS SCAN: Replaces the two separate ScanFileSystem calls 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(); } From ac5cb98c82332391180fa1b93c134dc0a964e5cd Mon Sep 17 00:00:00 2001 From: Collecting Date: Sun, 21 Dec 2025 01:20:15 +0000 Subject: [PATCH 17/19] feat: Add Accent Color for Game List Progress Bar Signed-off-by: Collecting --- src/citron/game_list.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/citron/game_list.h b/src/citron/game_list.h index e5d255cba..7438a6e78 100644 --- a/src/citron/game_list.h +++ b/src/citron/game_list.h @@ -147,6 +147,7 @@ public slots: void OnConfigurationChanged(); private slots: + void UpdateProgressBarColor(); void OnItemExpanded(const QModelIndex& item); void OnTextChanged(const QString& new_text); void OnFilterCloseClicked(); From e1a879489b29d0ab3e1e9b20012d5b3e245342b6 Mon Sep 17 00:00:00 2001 From: Collecting Date: Sun, 21 Dec 2025 01:21:01 +0000 Subject: [PATCH 18/19] feat: Add Accent Color to Gamelist Progress Bar Signed-off-by: Collecting --- src/citron/game_list.cpp | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/citron/game_list.cpp b/src/citron/game_list.cpp index cb2e1ae7e..7deb9c445 100644 --- a/src/citron/game_list.cpp +++ b/src/citron/game_list.cpp @@ -1303,6 +1303,7 @@ 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); @@ -1621,3 +1622,26 @@ void GameList::RefreshGameDirectory() { } 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())); + } +} From 2f16de156099d318d3ad24b341c8f317c6bf0e6a Mon Sep 17 00:00:00 2001 From: Collecting Date: Sun, 21 Dec 2025 02:50:43 +0000 Subject: [PATCH 19/19] fix: NAND & SDMC scanning Signed-off-by: Collecting --- src/citron/game_list_worker.cpp | 103 ++++++++++++++++---------------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/src/citron/game_list_worker.cpp b/src/citron/game_list_worker.cpp index 86c245e73..b97225258 100644 --- a/src/citron/game_list_worker.cpp +++ b/src/citron/game_list_worker.cpp @@ -570,6 +570,13 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa const auto physical_name = Common::FS::PathToUTF8String(path); + 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; } @@ -577,20 +584,21 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa // Cache Check const auto* cached = GetCachedGameMetadata(physical_name); if (cached && cached->IsValid() && (target == ScanTarget::PopulateGameList || target == ScanTarget::Both)) { - 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; + 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 @@ -610,33 +618,37 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa const auto res2 = loader->ReadProgramId(program_id); const auto file_type = loader->GetFileType(); - if (target == ScanTarget::FillManualContentProvider || target == ScanTarget::Both) { - 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()); + 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) { - std::vector icon; - std::string name = " "; - loader->ReadIcon(icon); - loader->ReadTitle(name); - std::size_t file_size = Common::FS::GetSize(physical_name); + 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); - if (res2 == Loader::ResultStatus::Success) { - CacheGameMetadata(physical_name, program_id, file_type, file_size, name, icon); + 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); }); + } } - - 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++; @@ -673,14 +685,11 @@ void GameListWorker::run() { int processed_files = 0; for (const auto& game_dir : game_dirs) { - if (game_dir.path == "SDMC" || game_dir.path == "UserNAND" || game_dir.path == "SysNAND") { - total_files += 5; - continue; - } + 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) || IsExtractedNCAMain(physical_name)) { + if (HasSupportedFileExtension(physical_name)) { total_files++; } return true; @@ -702,19 +711,13 @@ void GameListWorker::run() { for (UISettings::GameDir& game_dir : game_dirs) { if (stop_requested) break; - if (game_dir.path == "SDMC" || game_dir.path == "UserNAND" || game_dir.path == "SysNAND") { - auto type = (game_dir.path == "SDMC") ? GameListItemType::SdmcDir : (game_dir.path == "UserNAND" ? GameListItemType::UserNandDir : GameListItemType::SysNandDir); - auto* const game_list_dir = new GameListDir(game_dir, type); - DirEntryReady(game_list_dir); - AddTitlesToGameList(game_list_dir, online_stats); - processed_files += 5; // Match our placeholder count - } else { - watch_list.append(QString::fromStdString(game_dir.path)); - auto* const game_list_dir = new GameListDir(game_dir); - DirEntryReady(game_list_dir); + if (game_dir.path == "SDMC" || game_dir.path == "UserNAND" || game_dir.path == "SysNAND") continue; - ScanFileSystem(ScanTarget::Both, game_dir.path, game_dir.deep_scan, game_list_dir, online_stats, processed_files, total_files); - } + 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); });