diff --git a/lang/english/css/main_window.css b/lang/english/css/main_window.css
index 65cccd93..352748c6 100644
--- a/lang/english/css/main_window.css
+++ b/lang/english/css/main_window.css
@@ -37,12 +37,144 @@
border: 1px solid #434242;
padding: 1em;
}
-
+
+#RomBrowserPanel,
#LoadingPanel
{
- display: none;
width: 100%%;
height: 100%%;
+ display: none;
+}
+
+rombrowser
+{
+ display: block;
+ width: 100%%;
+ height: 100%%;
+ padding: 0.5rem 1rem;
+ margin: 0;
+}
+
+rombrowser .games-title
+{
+ flow: horizontal;
+}
+
+rombrowser .buttons-container
+{
+ padding: 0.2rem 0 0;
+}
+
+rombrowser .config-button svg
+{
+ width: 1.5rem;
+ height: 1.5rem;
+ cursor: pointer;
+}
+
+rombrowser .config-button .outline
+{
+ fill: #eff0f1;
+}
+
+rombrowser .config-button:hover .outline
+{
+ fill: #232629;
+}
+
+rombrowser .config-button
+{
+ padding: 0.2rem;
+}
+
+rombrowser .config-button:hover
+{
+ background-color: #b3d9f6;
+ border-radius: 4px;
+}
+
+rombrowser .games-title h1
+{
+ font-family: 'Segoe UI', Arial, sans-serif;
+ font-size: 1.5rem;
+ font-weight: 400;
+ color: #eff0f1;
+ padding: 0 0 0.6rem;
+ margin: 0;
+ width:*
+}
+
+rombrowser .games-grid
+{
+ height: 100%%;
+ flow:horizontal-wrap;
+ border-spacing: 1rem;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+rombrowser .games-grid[data-size="1"]
+{
+ border-spacing: 0.5rem;
+}
+
+rombrowser .rom-icon
+{
+ width: 100%;
+ height: 100%;
+ display: block;
+ border-radius: 5px;
+}
+
+rombrowser .rom-card-content
+{
+ width: 100%;
+ height: 100%;
+}
+
+rombrowser .rom-card
+{
+ cursor: pointer;
+ border-radius: 8px;
+ border: 3px solid transparent;
+}
+
+rombrowser .rom-card:visited
+{
+ border: 3px solid #76B9ED;
+ cursor: pointer;
+}
+
+rombrowser .games-grid[data-size="1"] .rom-card
+{
+ width: 6rem;
+ height: 6rem;
+}
+
+rombrowser .games-grid[data-size="2"] .rom-card
+{
+ width: 8rem;
+ height: 8rem;
+}
+
+rombrowser .games-grid[data-size="3"] .rom-card
+{
+ width: 10rem;
+ height: 10rem;
+}
+
+rombrowser .games-grid[data-size="4"] .rom-card
+{
+ width: 13rem;
+ height: 13rem;
+}
+
+#LoadingPanel .loading-container
+{
+ width: 100%%;
+ height: 100%%;
+ display: flex;
+ flex-direction: column;
align-items: center;
justify-content: center;
background-color: black;
diff --git a/lang/english/html/main_window.html b/lang/english/html/main_window.html
index 541cc2a2..afa5c0e6 100644
--- a/lang/english/html/main_window.html
+++ b/lang/english/html/main_window.html
@@ -14,14 +14,18 @@
+
+
+
diff --git a/src/nxemu-os/nxemu-os.vcxproj.filters b/src/nxemu-os/nxemu-os.vcxproj.filters
index 382a8f09..73540935 100644
--- a/src/nxemu-os/nxemu-os.vcxproj.filters
+++ b/src/nxemu-os/nxemu-os.vcxproj.filters
@@ -3259,9 +3259,6 @@
Source Files\core
-
- Source Files\core\arm
-
Source Files
diff --git a/src/nxemu/main.cpp b/src/nxemu/main.cpp
index 99141f30..c767b134 100644
--- a/src/nxemu/main.cpp
+++ b/src/nxemu/main.cpp
@@ -12,6 +12,7 @@
#include
#include
#include
+#include "user_interface/widgets/rom_browser.h"
#include
void RegisterWidgets(ISciterUI & sciterUI)
@@ -21,6 +22,7 @@ void RegisterWidgets(ISciterUI & sciterUI)
Register_WidgetMenuBar(sciterUI);
Register_WidgetPageNav(sciterUI);
Register_WidgetToolTipHost(sciterUI);
+ Register_WidgetRomBrowser(sciterUI);
}
extern "C" {
diff --git a/src/nxemu/nxemu.vcxproj b/src/nxemu/nxemu.vcxproj
index d0ae5009..9c02192e 100644
--- a/src/nxemu/nxemu.vcxproj
+++ b/src/nxemu/nxemu.vcxproj
@@ -40,6 +40,7 @@
+
@@ -51,12 +52,14 @@
+
+
@@ -69,6 +72,7 @@
+
diff --git a/src/nxemu/nxemu.vcxproj.filters b/src/nxemu/nxemu.vcxproj.filters
index 67de2197..508ae5d8 100644
--- a/src/nxemu/nxemu.vcxproj.filters
+++ b/src/nxemu/nxemu.vcxproj.filters
@@ -31,6 +31,12 @@
{3bf11314-5055-4185-95ae-653ebe2f8e8e}
+
+ {465d5a27-da20-4e68-b138-2d8b5b359780}
+
+
+ {a4091b11-2249-4f6f-8fd2-49ae4e48ea92}
+
@@ -75,6 +81,12 @@
Source Files\user_interface
+
+ Source Files\user_interface
+
+
+ Source Files\user_interface\widgets
+
@@ -122,6 +134,12 @@
Header Files\user_interface
+
+ Header Files\user_interface
+
+
+ Header Files\user_interface\widgets
+
Header Files\settings
diff --git a/src/nxemu/user_interface/game_list_worker.cpp b/src/nxemu/user_interface/game_list_worker.cpp
new file mode 100644
index 00000000..4e0fed64
--- /dev/null
+++ b/src/nxemu/user_interface/game_list_worker.cpp
@@ -0,0 +1,232 @@
+#include "game_list_worker.h"
+#include "settings/ui_settings.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace
+{
+bool HasSupportedFileExtension(const std::string & file_name)
+{
+ const std::unordered_set supported_extensions = {
+ "nso", "nro", "nca",
+ "dxci", "dnsp", "kip"
+ };
+ return supported_extensions.contains(stdstr(Path(file_name).GetExtension()).ToLower());
+}
+
+bool IsExtractedNCAMain(const std::string & file_name)
+{
+ return (stdstr(Path(file_name).GetNameExtension()).ToLower() == "main");
+}
+
+}
+
+GameListWorker::GameListWorker(IManualContentProvider & provider_, SystemModules & modules) :
+ m_provider(provider_),
+ m_modules(modules),
+ m_stop(false)
+{
+}
+
+GameListWorker::~GameListWorker()
+{
+ Stop();
+}
+
+void GameListWorker::Start()
+{
+ m_thread = std::thread(&GameListWorker::Run, this);
+}
+
+void GameListWorker::Stop()
+{
+ m_stop = true;
+ if (m_thread.joinable())
+ {
+ m_thread.join();
+ }
+}
+
+void GameListWorker::Run()
+{
+#ifdef tofix
+ watch_list.clear();
+ provider.ClearAllEntries();
+
+ const auto DirEntryReady = [&](GameListDir * game_list_dir) {
+ RecordEvent([=](GameList * game_list) { game_list->AddDirEntry(game_list_dir); });
+ };
+#endif
+ for (const std::string & game_dir : uiSettings.gameDirectories)
+ {
+ if (m_stop)
+ {
+ break;
+ }
+
+#ifdef tofix
+ if (game_dir.path == std::string("SDMC"))
+ {
+ auto * const game_list_dir = new GameListDir(game_dir, GameListItemType::SdmcDir);
+ DirEntryReady(game_list_dir);
+ AddTitlesToGameList(game_list_dir);
+ }
+ else if (game_dir.path == std::string("UserNAND"))
+ {
+ auto * const game_list_dir = new GameListDir(game_dir, GameListItemType::UserNandDir);
+ DirEntryReady(game_list_dir);
+ AddTitlesToGameList(game_list_dir);
+ }
+ else if (game_dir.path == std::string("SysNAND"))
+ {
+ auto * const game_list_dir = new GameListDir(game_dir, GameListItemType::SysNandDir);
+ DirEntryReady(game_list_dir);
+ AddTitlesToGameList(game_list_dir);
+ }
+ else
+ {
+ watch_list.append(QString::fromStdString(game_dir.path));
+ auto * const game_list_dir = new GameListDir(game_dir);
+ DirEntryReady(game_list_dir);
+#endif
+ ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir, false /*game_dir.deep_scan, game_list_dir*/);
+ ScanFileSystem(ScanTarget::PopulateGameList, game_dir, false /*game_dir.deep_scan, game_list_dir*/);
+#ifdef tofix
+ }
+#endif
+ }
+#ifdef tofix
+ RecordEvent([this](GameList * game_list) { game_list->DonePopulating(watch_list); });
+ processing_completed.Set();
+#endif
+}
+
+void GameListWorker::ScanFileSystem(ScanTarget target, const std::string & dir_path, bool deep_scan/*, GameListDir * parent_dir*/)
+{
+ using IRomInfoPtr = InterfacePtr;
+
+ const auto callback = [this, target/*, parent_dir*/](const std::filesystem::path & path) -> bool
+ {
+ if (m_stop)
+ {
+ // Breaks the callback loop.
+ return false;
+ }
+ const std::string physical_name = Common::FS::PathToUTF8String(path);
+ const bool is_dir = Common::FS::IsDir(path);
+ if (!is_dir && (HasSupportedFileExtension(physical_name) || IsExtractedNCAMain(physical_name)))
+ {
+ ISystemloader & systemloader = m_modules.Modules().Systemloader();
+ IRomInfoPtr info(systemloader.RomInfo(physical_name.c_str(), 0 ,0));
+ if (!info)
+ {
+ return true;
+ }
+ const LoaderFileType file_type = info->GetFileType();
+ if (file_type == LoaderFileType::Unknown || file_type == LoaderFileType::Error)
+ {
+ return true;
+ }
+ u64 program_id = 0;
+ const LoaderResultStatus res2 = info->ReadProgramId(program_id);
+
+ if (target == ScanTarget::FillManualContentProvider)
+ {
+ if (res2 == LoaderResultStatus::Success)
+ {
+ info->AddToManualContentProvider(m_provider);
+ }
+ }
+ else
+ {
+ uint32_t count = 0;
+ std::vector program_ids;
+
+ if (info->ReadProgramIds(nullptr, &count) == LoaderResultStatus::Success)
+ {
+ program_ids.resize(count);
+ info->ReadProgramIds(program_ids.data(), &count);
+ }
+
+ if (res2 == LoaderResultStatus::Success && program_ids.size() > 1 && (file_type == LoaderFileType::XCI || file_type == LoaderFileType::NSP))
+ {
+ __debugbreak();
+#ifdef tofix
+ for (const auto id : program_ids)
+ {
+ loader = Loader::GetLoader(systemModules.GetSystemloader(), file, id);
+ if (!loader)
+ {
+ continue;
+ }
+
+ std::vector icon;
+ [[maybe_unused]] const auto res1 = info->ReadIcon(icon);
+
+ std::string name = " ";
+ [[maybe_unused]] const auto res3 = info->ReadTitle(name);
+
+ const FileSys::PatchManager patch{id, system.GetFileSystemController(), system.GetContentProvider()};
+
+ auto entry = MakeGameListEntry(physical_name, name, Common::FS::GetSize(physical_name), icon, *loader, id, compatibility_list, play_time_manager, patch);
+
+ RecordEvent(
+ [=](GameList * game_list) { game_list->AddEntry(entry, parent_dir); });
+ }
+#endif
+ }
+ else
+ {
+ uint32_t size = 0;
+ std::vector icon;
+ LoaderResultStatus res1 = info->ReadIcon(nullptr, &size);
+ if (res1 == LoaderResultStatus::Success)
+ {
+ icon.resize(size);
+ res1 = info->ReadIcon(icon.data(), &size);
+ }
+
+ std::string name = " ";
+ LoaderResultStatus res3 = info->ReadTitle(nullptr, &size);
+ if (res3 == LoaderResultStatus::Success)
+ {
+ name.resize(size);
+ res3 = info->ReadTitle(name.data(), &size);
+ }
+
+ //__debugbreak();
+#ifdef tofix
+ const FileSys::PatchManager patch{program_id, system.GetFileSystemController(), system.GetContentProvider()};
+ auto entry = MakeGameListEntry(physical_name, name, Common::FS::GetSize(physical_name), icon, *info, program_id, compatibility_list, play_time_manager, patch);
+ RecordEvent([=](GameList * game_list) { game_list->AddEntry(entry, parent_dir); });
+#endif
+ }
+ }
+ }
+ else if (is_dir)
+ {
+ __debugbreak();
+#ifdef tofix
+ watch_list.append(QString::fromStdString(physical_name));
+#endif
+ }
+ return true;
+ };
+
+ if (deep_scan)
+ {
+ Common::FS::IterateDirEntriesRecursively(dir_path, callback, Common::FS::DirEntryFilter::All);
+ }
+ else
+ {
+ Common::FS::IterateDirEntries(dir_path, callback, Common::FS::DirEntryFilter::File);
+ }
+}
+
+template class InterfacePtr;
\ No newline at end of file
diff --git a/src/nxemu/user_interface/game_list_worker.h b/src/nxemu/user_interface/game_list_worker.h
new file mode 100644
index 00000000..d8c6a628
--- /dev/null
+++ b/src/nxemu/user_interface/game_list_worker.h
@@ -0,0 +1,31 @@
+#pragma once
+#include
+#include
+
+class SystemModules;
+__interface IManualContentProvider;
+
+class GameListWorker
+{
+ enum class ScanTarget
+ {
+ FillManualContentProvider,
+ PopulateGameList,
+ };
+
+public:
+ GameListWorker(IManualContentProvider & provider_, SystemModules & modules);
+ ~GameListWorker();
+
+ void Start();
+ void Stop();
+
+private:
+ void Run();
+ void ScanFileSystem(ScanTarget target, const std::string & game_dir, bool deep_scan);
+
+ IManualContentProvider & m_provider;
+ SystemModules & m_modules;
+ std::thread m_thread;
+ bool m_stop;
+};
\ No newline at end of file
diff --git a/src/nxemu/user_interface/sciter_main_window.cpp b/src/nxemu/user_interface/sciter_main_window.cpp
index d49bbdf7..8c1e6630 100644
--- a/src/nxemu/user_interface/sciter_main_window.cpp
+++ b/src/nxemu/user_interface/sciter_main_window.cpp
@@ -59,6 +59,10 @@ SciterMainWindow::~SciterMainWindow()
settings.UnregisterCallback(NXOsSetting::AudioVolume, SciterMainWindow::SettingChanged, this);
settings.SetBool(NXCoreSetting::ShuttingDown, true);
+ if (m_romBrowser)
+ {
+ m_romBrowser->ClearItems();
+ }
m_modules.ShutDown();
}
@@ -153,6 +157,21 @@ bool SciterMainWindow::Show()
m_sciterUI.AttachHandler(m_rootElement, IID_ITIMERSINK, (ITimerSink *)this);
m_rootElement.SetTimer(25, (uint32_t *)TIMER_UPDATE_INPUT);
+ SciterElement romBrowserPanel = m_rootElement.GetElementByID("RomBrowserPanel");
+ if (romBrowserPanel.IsValid())
+ {
+ romBrowserPanel.SetStyleAttribute("display", "block");
+ }
+
+ SciterElement rombrowser = m_rootElement.FindFirst("rombrowser");
+ interfacePtr = rombrowser.IsValid() ? m_sciterUI.GetElementInterface(rombrowser, IID_ROMBROWSER) : nullptr;
+ if (interfacePtr)
+ {
+ m_romBrowser = std::static_pointer_cast(interfacePtr);
+ m_romBrowser->SetMainWindow(this, &m_modules.Modules());
+ m_romBrowser->PopulateAsync();
+ }
+
if (!uiSettings.hasBrokenVulkan)
{
PopulateVulkanRecords(m_vkDeviceRecords, RenderSurface());
@@ -565,7 +584,17 @@ void SciterMainWindow::SettingChanged(const char * setting, void * userData)
bool SciterMainWindow::OnTimer(SCITER_ELEMENT /*element*/, uint32_t * timerId)
{
- if (timerId == (uint32_t*)TIMER_UPDATE_INPUT)
+ if (timerId == (uint32_t *)TIMER_UPDATE_UI)
+ {
+ SciterElement mainContents(m_rootElement.GetElementByID("MainContents"));
+ if (mainContents.IsValid())
+ {
+ mainContents.Update(false);
+ m_sciterUI.UpdateWindow(mainContents.GetElementHwnd(true));
+ }
+ return false;
+ }
+ else if (timerId == (uint32_t *)TIMER_UPDATE_INPUT)
{
UpdateInputDrivers();
}
@@ -576,6 +605,12 @@ bool SciterMainWindow::OnEvent(SCITER_ELEMENT /*element*/, SCITER_ELEMENT /*sour
{
if (event_code == EVENT_EMULATION_LOADING)
{
+ SciterElement romBrowserPanel(m_rootElement.GetElementByID("RomBrowserPanel"));
+ if (romBrowserPanel.IsValid())
+ {
+ romBrowserPanel.SetStyleAttribute("display", "none");
+ }
+
SciterElement LoadingPanel(m_rootElement.GetElementByID("LoadingPanel"));
if (LoadingPanel.IsValid())
{
@@ -594,6 +629,12 @@ bool SciterMainWindow::OnEvent(SCITER_ELEMENT /*element*/, SCITER_ELEMENT /*sour
{
LoadingPanel.SetStyleAttribute("display", "none");
}
+
+ SciterElement romBrowserPanel(m_rootElement.GetElementByID("RomBrowserPanel"));
+ if (romBrowserPanel.IsValid())
+ {
+ romBrowserPanel.SetStyleAttribute("display", "block");
+ }
}
else if (event_code == EVENT_EMULATION_FIRST_FRAME)
{
diff --git a/src/nxemu/user_interface/sciter_main_window.h b/src/nxemu/user_interface/sciter_main_window.h
index d1bf08ae..02c8a703 100644
--- a/src/nxemu/user_interface/sciter_main_window.h
+++ b/src/nxemu/user_interface/sciter_main_window.h
@@ -7,6 +7,7 @@
#include
#include
#include "startup_checks.h"
+#include "user_interface/widgets/rom_browser.h"
class SystemConfig;
class InputConfig;
@@ -43,7 +44,8 @@ class SciterMainWindow :
enum
{
- TIMER_UPDATE_INPUT = 5000,
+ TIMER_UPDATE_UI = 5000,
+ TIMER_UPDATE_INPUT = 5001,
};
public:
@@ -122,6 +124,7 @@ private:
SystemModules m_modules;
std::vector m_vkDeviceRecords;
std::shared_ptr m_menuBar;
+ std::shared_ptr m_romBrowser;
void * m_renderWindow;
std::string m_windowTitle;
std::unique_ptr m_systemConfig;
diff --git a/src/nxemu/user_interface/settings/input_config_player.cpp b/src/nxemu/user_interface/settings/input_config_player.cpp
index 32e2b8b0..fcebba5b 100644
--- a/src/nxemu/user_interface/settings/input_config_player.cpp
+++ b/src/nxemu/user_interface/settings/input_config_player.cpp
@@ -1581,7 +1581,7 @@ void InputConfigPlayer::UpdateControllerButtonNames()
}
NpadStyleIndex layout = GetControllerTypeFromIndex(m_comboControllerType->CurrentIndex());
SciterElement el;
-
+
switch (layout) {
case NpadStyleIndex::Fullkey:
case NpadStyleIndex::JoyconDual:
diff --git a/src/nxemu/user_interface/widgets/rom_browser.cpp b/src/nxemu/user_interface/widgets/rom_browser.cpp
new file mode 100644
index 00000000..ad4c0203
--- /dev/null
+++ b/src/nxemu/user_interface/widgets/rom_browser.cpp
@@ -0,0 +1,557 @@
+#include "rom_browser.h"
+#include "user_interface/sciter_main_window.h"
+#include "settings/ui_settings.h"
+#include