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 @@
+
+ +
-
- Loading -
-
-
-
-
+
+
Loading +
+
+
+
+
+
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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +enum +{ + EVENT_UPDATE_LIST = 0x1000, + TIMER_UPDATE_UI = 1, +}; + +std::string Base64Encode(const uint8_t * data, size_t len) +{ + static const char * base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + std::string ret; + int32_t i = 0; + int32_t j = 0; + uint8_t char_array_3[3]; + uint8_t char_array_4[4]; + + while (len--) + { + char_array_3[i++] = *(data++); + if (i == 3) + { + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (i = 0; i < 4; i++) + { + ret += base64_chars[char_array_4[i]]; + } + i = 0; + } + } + + if (i) + { + for (j = i; j < 3; j++) + { + char_array_3[j] = '\0'; + } + + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + + for (j = 0; j < i + 1; j++) + { + ret += base64_chars[char_array_4[j]]; + } + + while ((i++ < 3)) + { + ret += '='; + } + } + return ret; +} + +using IRomInfoPtr = InterfacePtr; + +class WidgetRomBrowser; + +struct RomEntry +{ + SciterElement romCard; + std::string title; + std::vector icon; + bool newItem; + bool found; +}; +using RomEntrys = std::unordered_map; + +class RomListWorker +{ +public: + RomListWorker(WidgetRomBrowser & romBrowser, ISystemModules & modules); + ~RomListWorker(); + + void Start(); + void Stop(); + +private: + void Run(); + void ScanFileSystem(const std::string & dir_path); + bool HasSupportedFileExtension(const std::string & file_name); + + ISystemModules & m_modules; + WidgetRomBrowser & m_romBrowser; + std::mutex & m_romsMutex; + RomEntrys & m_roms; + SciterElement & m_rootElement; + std::thread m_thread; + bool m_stop; +}; + +class WidgetRomBrowser : + public std::enable_shared_from_this, + public IWidget, + public IEventSink, + public ITimerSink, + public IClickSink, + public IDoubleClickSink, + public IRomBrowser +{ + friend class RomListWorker; + + typedef std::map> RomBrowsers; + +public: + static bool Register(ISciterUI & sciterUI); + + // IEventSink + bool OnEvent(SCITER_ELEMENT element, SCITER_ELEMENT source, uint32_t event_code, uint64_t reason) override; + + // ITimerSink + bool OnTimer(SCITER_ELEMENT Element, uint32_t * TimerId) override; + + // IClickSink + bool OnClick(SCITER_ELEMENT element, SCITER_ELEMENT source, uint32_t reason) override; + + // IDoubleClickSink + bool OnDoubleClick(SCITER_ELEMENT element, SCITER_ELEMENT source) override; + +private: + WidgetRomBrowser() = delete; + WidgetRomBrowser(const WidgetRomBrowser &) = delete; + WidgetRomBrowser & operator=(const WidgetRomBrowser &) = delete; + + WidgetRomBrowser(ISciterUI & sciterUI); + bool RenderUI(); + + // IWidget + void Attached(SCITER_ELEMENT element, IBaseElement * baseElement) override; + void Detached(SCITER_ELEMENT element) override; + std::shared_ptr GetInterface(const char * riid) override; + + // IRomBrowser + void PopulateAsync() override; + void SetMainWindow(SciterMainWindow * window, ISystemModules * modules) override; + void ClearItems() override; + + static void SettingChanged(const char * setting, void * userData); + static IWidget * __stdcall CreateWidget(ISciterUI & sciterUI); + static void __stdcall ReleaseWidget(IWidget * widget); + + ISciterUI & m_sciterUI; + IBaseElement * m_baseElement; + SciterMainWindow * m_window; + ISystemModules * m_modules; + SciterElement m_rootElement; + SciterElement m_gameTitle; + SciterElement m_gameGrid; + SciterElement m_configButton; + SciterElement m_currentGame; + std::mutex m_romsMutex; + RomEntrys m_roms; + std::unique_ptr m_currentWorker; + bool m_updatingUI; + static RomBrowsers m_instances; +}; + +WidgetRomBrowser::RomBrowsers WidgetRomBrowser::m_instances; + +WidgetRomBrowser::WidgetRomBrowser(ISciterUI & sciterUI) : + m_sciterUI(sciterUI), + m_updatingUI(false), + m_baseElement(nullptr), + m_window(nullptr), + m_modules(nullptr) +{ +} + +bool WidgetRomBrowser::Register(ISciterUI & sciterUI) +{ + const char * WidgetCss = + "rombrowser {" + " display: block;" + " behavior: rombrowser;" + "}"; + return sciterUI.RegisterWidgetType("rombrowser", WidgetRomBrowser::CreateWidget, WidgetRomBrowser::ReleaseWidget, WidgetCss); +} + +bool WidgetRomBrowser::RenderUI() +{ + if (!m_gameTitle.IsValid()) + { + m_gameTitle.Create("div", ""); + m_gameTitle.SetAttribute("class", "games-title"); + SciterElement title; + title.Create("h1", "My Games"); + m_gameTitle.Insert(title, m_gameTitle.GetChildCount()); + + SciterElement buttonsContainer; + buttonsContainer.Create("div", ""); + buttonsContainer.SetAttribute("class", "buttons-container"); + + m_configButton.Create("div", ""); + m_configButton.SetAttribute("class", "config-button"); + buttonsContainer.Insert(m_configButton, buttonsContainer.GetChildCount()); + m_gameTitle.Insert(buttonsContainer, m_gameTitle.GetChildCount()); + m_rootElement.Insert(m_gameTitle, m_rootElement.GetChildCount()); + + std::string ConfigSvg = R"( + + +)"; + m_configButton.SetHTML((uint8_t *)ConfigSvg.c_str(), ConfigSvg.size()); + m_sciterUI.AttachHandler(m_configButton, IID_ICLICKSINK, (IClickSink *)this); + m_sciterUI.AttachHandler(m_configButton, IID_IDBLCLICKSINK, (IDoubleClickSink *)this); + } + if (!m_gameGrid.IsValid()) + { + SettingsStore & settings = SettingsStore::GetInstance(); + m_gameGrid.Create("div", ""); + m_gameGrid.SetAttribute("class", "games-grid"); + m_gameGrid.SetAttribute("data-size", stdstr_f("%d", settings.GetInt(NXUISetting::MyGameIconSize)).c_str()); + m_rootElement.Insert(m_gameGrid, m_rootElement.GetChildCount()); + } + + if (m_roms.size() > 0) + { + std::lock_guard lock(m_romsMutex); + for (std::pair & entry : m_roms) + { + RomEntry & romEntry = entry.second; + SciterElement & romCard = romEntry.romCard; + if (romCard.IsValid()) + { + continue; + } + romCard.Create("div", ""); + romCard.SetAttribute("class", "rom-card"); + romCard.SetAttribute("data-path", entry.first.c_str()); + m_gameGrid.Insert(romCard, m_gameGrid.GetChildCount()); + m_sciterUI.AttachHandler(romCard, IID_ICLICKSINK, (IClickSink *)this); + m_sciterUI.AttachHandler(romCard, IID_IDBLCLICKSINK, (IDoubleClickSink *)this); + + SciterElement romCardContents; + romCardContents.Create("div", ""); + romCardContents.SetAttribute("class", "rom-card-content"); + romCard.Insert(romCardContents, romCard.GetChildCount()); + + if (romEntry.icon.size() > 0) + { + SciterElement iconElement; + iconElement.Create("img", ""); + iconElement.SetAttribute("class", "rom-icon"); + iconElement.SetAttribute("src", stdstr_f("data:image/jpeg;base64,%s", Base64Encode(romEntry.icon.data(), romEntry.icon.size()).c_str()).c_str()); + romCardContents.Insert(iconElement, romCardContents.GetChildCount()); + romEntry.icon.clear(); + } + } + } + m_updatingUI = false; + return false; +} + +bool WidgetRomBrowser::OnEvent(SCITER_ELEMENT /*element*/, SCITER_ELEMENT /*source*/, uint32_t event_code, uint64_t /*reason*/) +{ + if (event_code == EVENT_UPDATE_LIST && !m_updatingUI) + { + m_updatingUI = true; + m_rootElement.SetTimer(250, (uint32_t *)TIMER_UPDATE_UI); + } + return false; +} + +bool WidgetRomBrowser::OnTimer(SCITER_ELEMENT /*element*/, uint32_t * timerId) +{ + if (timerId == (uint32_t *)TIMER_UPDATE_UI) + { + return RenderUI(); + }; + return true; +} + +bool WidgetRomBrowser::OnClick(SCITER_ELEMENT element, SCITER_ELEMENT /*source*/, uint32_t /*reason*/) +{ + SciterElement el(element); + std::string className = el.GetAttribute("class"); + if (strcmp(className.c_str(), "config-button") == 0 && m_window != nullptr) + { + m_window->ShowConfig("GameBrowser"); + return true; + } + else if (strcmp(className.c_str(), "rom-card") == 0) + { + if (m_currentGame.IsValid()) + { + m_currentGame.SetState(0, SciterElement::STATE_CURRENT | SciterElement::STATE_VISITED, true); + } + m_currentGame = el; + m_currentGame.SetState(SciterElement::STATE_CURRENT | SciterElement::STATE_VISITED, 0, true); + } + return false; +} + +bool WidgetRomBrowser::OnDoubleClick(SCITER_ELEMENT element, SCITER_ELEMENT /*source*/) +{ + SciterElement el(element); + if (strcmp(el.GetAttribute("class").c_str(), "rom-card") == 0 && m_window != nullptr) + { + std::string path = el.GetAttribute("data-path"); + m_window->LoadGame(path.c_str()); + } + return false; +} + +void WidgetRomBrowser::SettingChanged(const char * setting, void * userData) +{ + WidgetRomBrowser * impl = (WidgetRomBrowser *)userData; + SettingsStore & settings = SettingsStore::GetInstance(); + if (strcmp(setting, NXUISetting::MyGameIconSize) == 0 && impl->m_gameGrid) + { + impl->m_gameGrid.SetAttribute("data-size", stdstr_f("%d", settings.GetInt(NXUISetting::MyGameIconSize)).c_str()); + } + else if (strcmp(setting, NXUISetting::GameDirectories) == 0) + { + impl->PopulateAsync(); + } +} + +IWidget * __stdcall WidgetRomBrowser::CreateWidget(ISciterUI & sciterUI) +{ + std::shared_ptr instance(new WidgetRomBrowser(sciterUI)); + IWidget * widget = (IWidget *)instance.get(); + WidgetRomBrowser * rombrowser = instance.get(); + m_instances.insert(RomBrowsers::value_type(rombrowser, std::move(instance))); + return widget; +} + +void __stdcall WidgetRomBrowser::ReleaseWidget(IWidget * widget) +{ + RomBrowsers::iterator it = m_instances.find(widget); + if (it != m_instances.end()) + { + m_instances.erase(it); + } +} + +void WidgetRomBrowser::Attached(SCITER_ELEMENT element, IBaseElement * baseElement) +{ + m_baseElement = baseElement; + m_rootElement = element; + m_sciterUI.AttachHandler(m_rootElement, IID_EVENTSINK, (IEventSink *)this); + m_sciterUI.AttachHandler(m_rootElement, IID_ITIMERSINK, (ITimerSink *)this); + + SettingsStore & settings = SettingsStore::GetInstance(); + settings.RegisterCallback(NXUISetting::MyGameIconSize, WidgetRomBrowser::SettingChanged, this); + settings.RegisterCallback(NXUISetting::GameDirectories, WidgetRomBrowser::SettingChanged, this); + + RenderUI(); +} + +void WidgetRomBrowser::Detached(SCITER_ELEMENT /*element*/) +{ + m_baseElement = nullptr; + m_rootElement = nullptr; +} + +std::shared_ptr WidgetRomBrowser::GetInterface(const char * riid) +{ + if (strcmp(riid, IID_ROMBROWSER) == 0) + { + return std::static_pointer_cast(shared_from_this()); + } + return nullptr; +} + +void WidgetRomBrowser::PopulateAsync() +{ + if (m_modules != nullptr) + { + m_currentWorker.reset(); + m_currentWorker = std::make_unique(*this, *m_modules); + m_currentWorker->Start(); + } +} + +void WidgetRomBrowser::SetMainWindow(SciterMainWindow * window, ISystemModules * modules) +{ + m_modules = modules; + m_window = window; +} + +void WidgetRomBrowser::ClearItems() +{ + m_currentWorker->Stop(); + std::lock_guard lock(m_romsMutex); + m_roms.clear(); +} + +RomListWorker::RomListWorker(WidgetRomBrowser & romBrowser, ISystemModules & modules) : + m_modules(modules), + m_romBrowser(romBrowser), + m_romsMutex(romBrowser.m_romsMutex), + m_roms(romBrowser.m_roms), + m_rootElement(romBrowser.m_rootElement), + m_stop(false) +{ +} + +RomListWorker::~RomListWorker() +{ + Stop(); +} + +void RomListWorker::Start() +{ + m_thread = std::thread(&RomListWorker::Run, this); +} + +void RomListWorker::Stop() +{ + m_stop = true; + if (m_thread.joinable()) + { + m_thread.join(); + } +} + +void RomListWorker::Run() +{ + { + std::lock_guard lock(m_romsMutex); + for (std::pair & entry : m_roms) + { + RomEntry & romEntry = entry.second; + romEntry.found = false; + } + } + + for (const std::string & game_dir : uiSettings.gameDirectories) + { + if (m_stop) + { + return; + } + ScanFileSystem(game_dir); + } + + if (!m_stop) + { + std::lock_guard lock(m_romsMutex); + + for (RomEntrys::iterator itr = m_roms.begin(); itr != m_roms.end();) + { + if (!itr->second.found) + { + SciterElement & romCard = itr->second.romCard; + if (romCard.IsValid()) + { + romCard.Destroy(); + } + itr = m_roms.erase(itr); + } + else + { + itr++; + } + } + } +} + +void RomListWorker::ScanFileSystem(const std::string & dir_path) +{ + const auto callback = [this](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))) + { + { + std::lock_guard lock(m_romsMutex); + RomEntrys::iterator iter = m_roms.find(physical_name); + if (iter != m_roms.end()) + { + iter->second.found = true; + return true; + } + } + + ISystemloader & systemloader = m_modules.Systemloader(); + RomEntry entry; + IRomInfoPtr romInfo = systemloader.RomInfo(physical_name.c_str(), 0, 0); + if (!romInfo) + { + return true; + } + entry.newItem = true; + entry.found = true; + + uint32_t size = 0; + LoaderResultStatus res = romInfo->ReadIcon(nullptr, &size); + if (res == LoaderResultStatus::Success && size > 0) + { + entry.icon.resize(size); + romInfo->ReadIcon(entry.icon.data(), &size); + } + + res = romInfo->ReadTitle(nullptr, &size); + if (res == LoaderResultStatus::Success) + { + entry.title.resize(size); + res = romInfo->ReadTitle(entry.title.data(), &size); + } + + { + std::lock_guard lock(m_romsMutex); + m_roms.emplace(physical_name, std::move(entry)); + } + m_rootElement.PostEvent(EVENT_UPDATE_LIST); + } + return true; + }; + Common::FS::IterateDirEntries(dir_path, callback, Common::FS::DirEntryFilter::File); +} + +bool RomListWorker::HasSupportedFileExtension(const std::string & file_name) +{ + static const std::unordered_set supported_extensions = { + "nso", "nro", "nca", + "dxci", "dnsp", "kip"}; + return supported_extensions.contains(stdstr(Path(file_name).GetExtension()).ToLower()); +} +} // namespace + +bool Register_WidgetRomBrowser(ISciterUI & sciterUI) +{ + return WidgetRomBrowser::Register(sciterUI); +} \ No newline at end of file diff --git a/src/nxemu/user_interface/widgets/rom_browser.h b/src/nxemu/user_interface/widgets/rom_browser.h new file mode 100644 index 00000000..47c7a2a8 --- /dev/null +++ b/src/nxemu/user_interface/widgets/rom_browser.h @@ -0,0 +1,17 @@ +#pragma once + +__interface ISciterUI; +__interface ISystemModules; +class SciterMainWindow; +class SystemModules; + +static const char * IID_ROMBROWSER = "F68DFC0D-C86D-4810-97C6-48289FA650ED"; + +__interface IRomBrowser +{ + void PopulateAsync() = 0; + void SetMainWindow(SciterMainWindow * window, ISystemModules * modules) = 0; + void ClearItems() = 0; +}; + +bool Register_WidgetRomBrowser(ISciterUI & SciterUI); \ No newline at end of file