Add rom browser

This commit is contained in:
N3xoX1 2026-02-02 14:39:21 +10:30
parent ff9dbd8179
commit 0919cd79ed
13 changed files with 1053 additions and 15 deletions

View File

@ -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;

View File

@ -14,14 +14,18 @@
</header>
<mainmenu id="MainMenu" />
<div id="MainContents">
<div id="RomBrowserPanel">
<rombrowser />
</div>
<div id="LoadingPanel">
<div class="loading-text">
<span class="text">Loading</span>
<div class="dot-pulse-container">
<div class="dot-pulse"></div>
<div class="dot-pulse"></div>
<div class="dot-pulse"></div>
</div>
<div class="loading-container">
<div class="loading-text"><span class="text">Loading</span>
<div class="dot-pulse-container">
<div class="dot-pulse"></div>
<div class="dot-pulse"></div>
<div class="dot-pulse"></div>
</div>
</div>
</div>
</div>
</div>

View File

@ -3259,9 +3259,6 @@
<ClCompile Include="core\reporter.cpp">
<Filter>Source Files\core</Filter>
</ClCompile>
<ClCompile Include="core\arm\debug.cpp">
<Filter>Source Files\core\arm</Filter>
</ClCompile>
<ClCompile Include="emu_thread.cpp">
<Filter>Source Files</Filter>
</ClCompile>

View File

@ -12,6 +12,7 @@
#include <widgets/menubar.h>
#include <widgets/page_nav.h>
#include <widgets/tooltip_host.h>
#include "user_interface/widgets/rom_browser.h"
#include <windows.h>
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" {

View File

@ -40,6 +40,7 @@
<ClCompile Include="main.cpp" />
<ClCompile Include="settings\ui_settings.cpp" />
<ClCompile Include="startup_checks.cpp" />
<ClCompile Include="user_interface\game_list_worker.cpp" />
<ClCompile Include="user_interface\key_mappings.cpp" />
<ClCompile Include="user_interface\notification.cpp" />
<ClCompile Include="user_interface\sciter_main_window.cpp" />
@ -51,12 +52,14 @@
<ClCompile Include="user_interface\settings\system_config_debug.cpp" />
<ClCompile Include="user_interface\settings\system_config_game_browser.cpp" />
<ClCompile Include="user_interface\settings\system_config_graphics.cpp" />
<ClCompile Include="user_interface\widgets\rom_browser.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="settings\ui_identifiers.h" />
<ClInclude Include="settings\ui_settings.h" />
<ClInclude Include="settings\ui_settings_identifiers.h" />
<ClInclude Include="startup_checks.h" />
<ClInclude Include="user_interface\game_list_worker.h" />
<ClInclude Include="user_interface\key_mappings.h" />
<ClInclude Include="user_interface\notification.h" />
<ClInclude Include="user_interface\resource.h" />
@ -69,6 +72,7 @@
<ClInclude Include="user_interface\settings\system_config_debug.h" />
<ClInclude Include="user_interface\settings\system_config_game_browser.h" />
<ClInclude Include="user_interface\settings\system_config_graphics.h" />
<ClInclude Include="user_interface\widgets\rom_browser.h" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\external\sciterui\src\sciterui\sciterui.vcxproj">

View File

@ -31,6 +31,12 @@
<Filter Include="Source Files\settings">
<UniqueIdentifier>{3bf11314-5055-4185-95ae-653ebe2f8e8e}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files\user_interface\widgets">
<UniqueIdentifier>{465d5a27-da20-4e68-b138-2d8b5b359780}</UniqueIdentifier>
</Filter>
<Filter Include="Source Files\user_interface\widgets">
<UniqueIdentifier>{a4091b11-2249-4f6f-8fd2-49ae4e48ea92}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="main.cpp">
@ -75,6 +81,12 @@
<ClCompile Include="user_interface\key_mappings.cpp">
<Filter>Source Files\user_interface</Filter>
</ClCompile>
<ClCompile Include="user_interface\game_list_worker.cpp">
<Filter>Source Files\user_interface</Filter>
</ClCompile>
<ClCompile Include="user_interface\widgets\rom_browser.cpp">
<Filter>Source Files\user_interface\widgets</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="user_interface\resource.h">
@ -122,6 +134,12 @@
<ClInclude Include="user_interface\key_mappings.h">
<Filter>Header Files\user_interface</Filter>
</ClInclude>
<ClInclude Include="user_interface\game_list_worker.h">
<Filter>Header Files\user_interface</Filter>
</ClInclude>
<ClInclude Include="user_interface\widgets\rom_browser.h">
<Filter>Header Files\user_interface\widgets</Filter>
</ClInclude>
<ClInclude Include="settings\ui_identifiers.h">
<Filter>Header Files\settings</Filter>
</ClInclude>

View File

@ -0,0 +1,232 @@
#include "game_list_worker.h"
#include "settings/ui_settings.h"
#include <yuzu_common/fs/fs.h>
#include <common/path.h>
#include <common/std_string.h>
#include <nxemu-module-spec/system_loader.h>
#include <nxemu-core/modules/system_modules.h>
#include <yuzu_common/interface_pointer.h>
#include <yuzu_common/interface_pointer_def.h>
#include <unordered_set>
namespace
{
bool HasSupportedFileExtension(const std::string & file_name)
{
const std::unordered_set<std::string> 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<IRomInfo>;
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<uint64_t> 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<u8> 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<u8> 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<IRomInfo>;

View File

@ -0,0 +1,31 @@
#pragma once
#include <thread>
#include <string>
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;
};

View File

@ -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<IRomBrowser>(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)
{

View File

@ -7,6 +7,7 @@
#include <sciter_element.h>
#include <widgets/menubar.h>
#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<VkDeviceRecord> m_vkDeviceRecords;
std::shared_ptr<IMenuBar> m_menuBar;
std::shared_ptr<IRomBrowser> m_romBrowser;
void * m_renderWindow;
std::string m_windowTitle;
std::unique_ptr<SystemConfig> m_systemConfig;

View File

@ -1581,7 +1581,7 @@ void InputConfigPlayer::UpdateControllerButtonNames()
}
NpadStyleIndex layout = GetControllerTypeFromIndex(m_comboControllerType->CurrentIndex());
SciterElement el;
switch (layout) {
case NpadStyleIndex::Fullkey:
case NpadStyleIndex::JoyconDual:

View File

@ -0,0 +1,557 @@
#include "rom_browser.h"
#include "user_interface/sciter_main_window.h"
#include "settings/ui_settings.h"
#include <map>
#include <common/path.h>
#include <common/std_string.h>
#include <nxemu-module-spec/system_loader.h>
#include <nxemu-core/modules/system_modules.h>
#include <nxemu-core/settings/settings.h>
#include <nxemu/settings/ui_identifiers.h>
#include <sciter_handler.h>
#include <sciter_element.h>
#include <yuzu_common/fs/fs.h>
#include <yuzu_common/interface_pointer.h>
#include <yuzu_common/interface_pointer_def.h>
#include <mutex>
#include <thread>
#include <unordered_set>
#include <unordered_map>
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<IRomInfo>;
class WidgetRomBrowser;
struct RomEntry
{
SciterElement romCard;
std::string title;
std::vector<uint8_t> icon;
bool newItem;
bool found;
};
using RomEntrys = std::unordered_map<std::string, RomEntry>;
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<WidgetRomBrowser>,
public IWidget,
public IEventSink,
public ITimerSink,
public IClickSink,
public IDoubleClickSink,
public IRomBrowser
{
friend class RomListWorker;
typedef std::map<IWidget *, std::shared_ptr<WidgetRomBrowser>> 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<void> 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<RomListWorker> 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"(<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 36 36">
<path d="M18.1 11c-3.9 0-7 3.1-7 7s3.1 7 7 7 7-3.1 7-7-3.1-7-7-7zm0 12c-2.8 0-5-2.2-5-5s2.2-5 5-5 5 2.2 5 5-2.2 5-5 5z" class="outline"/>
<path d="m32.8 14.7-2.8-.9-.6-1.5 1.4-2.6c.3-.6.2-1.4-.3-1.9l-2.4-2.4c-.5-.5-1.3-.6-1.9-.3l-2.6 1.4-1.5-.6-.9-2.8C21 2.5 20.4 2 19.7 2h-3.4c-.7 0-1.3.5-1.4 1.2L14 6c-.6.1-1.1.3-1.6.6L9.8 5.2c-.6-.3-1.4-.2-1.9.3L5.5 7.9c-.5.5-.6 1.3-.3 1.9l1.3 2.5c-.2.5-.4 1.1-.6 1.6l-2.8.9c-.6.2-1.1.8-1.1 1.5v3.4c0 .7.5 1.3 1.2 1.5l2.8.9.6 1.5-1.4 2.6c-.3.6-.2 1.4.3 1.9l2.4 2.4c.5.5 1.3.6 1.9.3l2.6-1.4 1.5.6.9 2.9c.2.6.8 1.1 1.5 1.1h3.4c.7 0 1.3-.5 1.5-1.1l.9-2.9 1.5-.6 2.6 1.4c.6.3 1.4.2 1.9-.3l2.4-2.4c.5-.5.6-1.3.3-1.9l-1.4-2.6.6-1.5 2.9-.9c.6-.2 1.1-.8 1.1-1.5v-3.4c0-.7-.5-1.4-1.2-1.6zm-.8 4.7-3.6 1.1-.1.5-.9 2.1-.3.5 1.8 3.3-2 2-3.3-1.8-.5.3c-.7.4-1.4.7-2.1.9l-.5.1-1.1 3.6h-2.8l-1.1-3.6-.5-.1-2.1-.9-.5-.3-3.3 1.8-2-2 1.8-3.3-.3-.5c-.4-.7-.7-1.4-.9-2.1l-.1-.5L4 19.4v-2.8l3.4-1 .2-.5c.2-.8.5-1.5.9-2.2l.3-.5-1.7-3.3 2-2 3.2 1.8.5-.3c.7-.4 1.4-.7 2.2-.9l.5-.2L16.6 4h2.8l1.1 3.5.5.2c.7.2 1.4.5 2.1.9l.5.3 3.3-1.8 2 2-1.8 3.3.3.5c.4.7.7 1.4.9 2.1l.1.5 3.6 1.1v2.8z" class="outline"/>
</svg>)";
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<std::mutex> lock(m_romsMutex);
for (std::pair<const std::string, RomEntry> & 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<WidgetRomBrowser> 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<void> WidgetRomBrowser::GetInterface(const char * riid)
{
if (strcmp(riid, IID_ROMBROWSER) == 0)
{
return std::static_pointer_cast<IRomBrowser>(shared_from_this());
}
return nullptr;
}
void WidgetRomBrowser::PopulateAsync()
{
if (m_modules != nullptr)
{
m_currentWorker.reset();
m_currentWorker = std::make_unique<RomListWorker>(*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<std::mutex> 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<std::mutex> lock(m_romsMutex);
for (std::pair<const std::string, RomEntry> & 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::string> 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);
}

View File

@ -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);