UI: Add the functionality to choose/create profiles with being able to select avatars from frimware loosely based off how ryujinx works (#91)

This commit is contained in:
Stardust 2026-07-21 22:00:30 +09:30 committed by GitHub
parent 661a8a2dd8
commit 5e35c3305d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 3149 additions and 61 deletions

2
external/sciterui vendored

@ -1 +1 @@
Subproject commit db5a4d1840cd4fdda67cc8ef3f37597be26190ab
Subproject commit c2cdf8fa3a4c5807a6178a88799844e307af1101

View File

@ -250,6 +250,185 @@ label:disabled {
margin-bottom: 0.75rem;
}
.page_content.profiles-page
{
flow: vertical;
width: 100%%;
height: 100%%;
padding: 1em;
}
.active-profile-label
{
margin: 0 0 0.65rem 0;
color: #eff0f1;
font-size: 0.9rem;
}
.profile-grid
{
flow: horizontal-wrap;
border: 1px solid #55575a;
border-radius: 5px;
background: #1e1e1e;
padding: 0.5rem;
width: 100%%;
height: *;
overflow-y: auto;
vertical-align: top;
}
.profile-tile
{
position: relative;
width: 96px;
margin: 0.35rem;
padding: 0.25rem;
border: 2px solid transparent;
border-radius: 5px;
flow: vertical;
horizontal-align: center;
cursor: pointer;
behavior: clickable;
}
.profile-tile:hover
{
background: rgba(255, 255, 255, 0.06);
}
.profile-tile.current
{
background: rgba(61, 174, 233, 0.22);
border: 2px solid #3daee9;
}
.profile-tile.current > .profile-tile-name
{
color: #ffffff;
font-weight: 600;
}
.profile-tile-avatar
{
width: 96px;
height: 96px;
border-radius: 4px;
background: #111111;
overflow: hidden;
foreground-repeat: no-repeat;
foreground-position: 50% 50%;
foreground-size: 100% 100%;
}
.profile-tile-avatar img
{
display: block;
width: 96px;
height: 96px;
background: transparent;
foreground-repeat: no-repeat;
foreground-position: 50% 50%;
foreground-size: 100% 100%;
}
.profile-tile-name
{
width: 90px;
margin-top: 0.35rem;
text-align: center;
color: #eff0f1;
font-size: 0.85rem;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.profile-tile-edit
{
position: absolute;
top: 0.35rem;
right: 0.35rem;
width: 24px;
height: 24px;
border-radius: 12px;
background: #2b2f32;
border: none;
color: #ffffff;
padding: 0;
z-index: 2;
opacity: 0;
visibility: hidden;
behavior: clickable;
cursor: pointer;
overflow: hidden;
}
.profile-tile-edit svg
{
position: absolute;
left: 5px;
top: 5px;
width: 14px;
height: 14px;
}
.profile-tile-edit path
{
fill: none;
stroke: currentColor;
}
.profile-tile:hover > .profile-tile-edit
{
opacity: 1;
visibility: visible;
}
.profile-tile-edit:hover
{
background: #3a4045;
}
.profile-tile-add
{
position: relative;
width: 96px;
height: 118px;
margin: 0.35rem;
border-radius: 5px;
background: #2b2f32;
cursor: pointer;
behavior: clickable;
}
.profile-tile-add-button
{
position: absolute;
left: 27px;
top: 38px;
width: 42px;
height: 42px;
border-radius: 21px;
background: #2b2f32;
border: 1px solid #55575a;
color: #eff0f1;
font-size: 1.6rem;
font-family: var(--font-ui);
line-height: 40px;
text-align: center;
padding: 0;
overflow: hidden;
behavior: clickable;
cursor: pointer;
}
.profile-tile-add-button:hover
{
background: #3a4045;
border: 1px solid #3cafe9;
}
.hotkeys-table
{
width: 100%%;

View File

@ -0,0 +1,126 @@
.profile-editor-body
{
padding: 0.75rem 0.5rem 0.25rem;
min-width: 32rem;
color: #eff0f1;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 0.9rem;
}
.profile-detail-layout
{
flow: horizontal;
width: 100%;
}
.profile-detail-fields
{
width: *;
min-width: 22rem;
flow: vertical;
}
.profile-field
{
flow: vertical;
width: *;
margin: 0 0 0.85rem 0;
}
.profile-field > label
{
margin: 0 0 0.35rem 0;
color: #eff0f1;
}
.profile-field > input
{
display: block;
width: *;
height: 2rem;
min-height: 2rem;
max-height: 2rem;
box-sizing: border-box;
color: #ffffff;
background: #2f2f31;
border: 1px solid #6a6e73;
border-radius: 0.35rem;
padding: 0 0.6rem;
margin: 0;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 0.875rem;
line-height: 2rem;
vertical-align: middle;
font-rendering-mode: snap-pixel;
text-selection-caret-color: #ffffff;
text-selection-color: #ffffff;
text-selection-background-color: #3daee9;
}
.profile-field > input:focus,
.profile-field > input:active
{
border-color: #3daee9;
background: #3c3c3c;
text-selection-caret-color: #ffffff;
}
.profile-field > input:hover
{
background: #3c3c3c;
}
.profile-field > input:disabled,
.profile-field > input[readonly]
{
background: #2a2a2c;
color: #d0d0d0;
border-color: #55575a;
text-selection-caret-color: transparent;
}
.profile-field > input:disabled:hover,
.profile-field > input[readonly]:hover
{
background: #2a2a2c;
}
.profile-detail-image
{
margin-left: 1rem;
}
.profile-detail-avatar
{
width: 96px;
height: 96px;
border: 1px solid #55575a;
border-radius: 4px;
background: #111111;
overflow: hidden;
foreground-repeat: no-repeat;
foreground-position: 50% 50%;
foreground-size: 100% 100%;
}
.profile-detail-avatar img
{
display: block;
width: 96px;
height: 96px;
background: transparent;
foreground-repeat: no-repeat;
foreground-position: 50% 50%;
foreground-size: 100% 100%;
}
.profile-editor-footer
{
flow: horizontal;
horizontal-align: right;
}
.profile-editor-footer > button
{
margin-left: 0.4rem;
}

View File

@ -0,0 +1,119 @@
html
{
overflow: hidden;
}
body
{
flow: vertical;
}
.firmware-avatar-body
{
padding: 0.5rem 0.25rem;
min-width: 32rem;
height: 21rem;
color: #eff0f1;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 0.9rem;
}
.firmware-avatar-grid
{
flow: horizontal-wrap;
overflow-y: auto;
height: 100%;
width: 100%;
box-sizing: border-box;
padding: 0.25rem;
border: 1px solid #55575a;
background: #121212;
horizontal-align: center;
}
.firmware-avatar-tile
{
width: 72px;
height: 72px;
margin: 5px;
border: 2px solid transparent;
border-radius: 4px;
background: #ffffff;
overflow: hidden;
cursor: pointer;
}
.firmware-avatar-tile:hover
{
border-color: #3cafe9;
}
.firmware-avatar-tile.selected
{
border-color: #3cafe9;
}
.firmware-avatar-tile img
{
width: 72px;
height: 72px;
display: block;
foreground-repeat: no-repeat;
foreground-position: 50% 50%;
foreground-size: contain;
}
.firmware-avatar-empty
{
padding: 2rem 1rem;
text-align: center;
color: #b0b3b5;
}
.firmware-avatar-footer
{
flow: horizontal;
vertical-align: middle;
}
.firmware-avatar-footer > #firmwareAvatarBack
{
margin-right: *;
}
.firmware-avatar-actions
{
flow: horizontal;
vertical-align: middle;
}
.firmware-avatar-actions > *
{
margin-left: 0.4rem;
}
.firmware-avatar-colors
{
flow: horizontal;
vertical-align: middle;
}
.firmware-avatar-swatch
{
width: 1.35rem;
height: 1.35rem;
margin-right: 0.25rem;
border: 2px solid #55575a;
border-radius: 3px;
cursor: pointer;
}
.firmware-avatar-swatch:hover
{
border-color: #3cafe9;
}
.firmware-avatar-swatch.selected
{
border-color: #3cafe9;
}

View File

@ -0,0 +1,41 @@
.profile-image-selector-body
{
padding: 0.75rem 0.25rem 0.25rem;
min-width: 28rem;
color: #eff0f1;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 0.9rem;
}
.profile-image-selector-body p
{
margin: 0;
line-height: 1.4;
white-space: normal;
}
.profile-image-selector-footer
{
flow: horizontal;
vertical-align: middle;
}
.profile-image-selector-footer > #profileImageBack
{
margin-right: *;
}
.profile-image-selector-actions
{
flow: horizontal;
}
.profile-image-selector-actions > button
{
margin-left: 0.4rem;
}
#profileImageFirmware:disabled
{
opacity: 0.45;
}

View File

@ -0,0 +1,40 @@
<html window-frame="solid-with-shadow" window-icon="window-icon.png">
<head>
<link href="base.css" rel="stylesheet" type="text/css" />
<link href="dialog.css" rel="stylesheet" type="text/css" />
<link href="profile_editor_dialog.css" rel="stylesheet" type="text/css" />
<title>Edit Profile</title>
</head>
<header>
<window-caption role="window-caption"><img src="window-icon-32.png" /><span id="ProfileEditorTitle">Edit Profile</span></window-caption>
<window-buttons>
<window-button role="window-close"></window-button>
</window-buttons>
</header>
<body>
<div class="profile-editor-body">
<div class="profile-detail-layout">
<div class="profile-detail-fields">
<div class="profile-field">
<label for="ProfileUsername">Name:</label>
<input type="text" id="ProfileUsername" maxlength="32" placeholder="Choose a nickname" />
</div>
<div class="profile-field">
<label for="ProfileUserId">User ID:</label>
<input type="text" id="ProfileUserId" readonly />
</div>
</div>
<div class="profile-detail-image">
<div class="profile-detail-avatar">
<img id="ProfileDetailImage" />
</div>
</div>
</div>
</div>
<div class="footer profile-editor-footer">
<button id="profileRemove">Delete</button>
<button id="profileSetImage">Set Image</button>
<button id="profileSave" role="window-ok">Save</button>
</div>
</body>
</html>

View File

@ -0,0 +1,42 @@
<html window-frame="solid-with-shadow" window-icon="window-icon.png">
<head>
<link href="base.css" rel="stylesheet" type="text/css" />
<link href="dialog.css" rel="stylesheet" type="text/css" />
<link href="profile_firmware_avatar_dialog.css" rel="stylesheet" type="text/css" />
<title>Select Firmware Avatar</title>
</head>
<header>
<window-caption role="window-caption"><img src="window-icon-32.png" /><span>Select Firmware Avatar</span></window-caption>
<window-buttons>
<window-button role="window-close"></window-button>
</window-buttons>
</header>
<body>
<div class="firmware-avatar-body">
<div id="FirmwareAvatarGrid" class="firmware-avatar-grid"></div>
<div id="FirmwareAvatarEmpty" class="firmware-avatar-empty" style="display:none">
No firmware avatars were found. Ensure a complete system firmware install that includes AvatarImage (010000000000080A).
</div>
</div>
<div class="footer firmware-avatar-footer">
<button id="firmwareAvatarBack" role="window-close">Back</button>
<div class="firmware-avatar-actions">
<div id="FirmwareAvatarColors" class="firmware-avatar-colors" title="Background color">
<div class="firmware-avatar-swatch selected" role="firmware-color" data-color="#FFFFFF" style="background:#FFFFFF"></div>
<div class="firmware-avatar-swatch" role="firmware-color" data-color="#000000" style="background:#000000"></div>
<div class="firmware-avatar-swatch" role="firmware-color" data-color="#E60012" style="background:#E60012"></div>
<div class="firmware-avatar-swatch" role="firmware-color" data-color="#F4A60A" style="background:#F4A60A"></div>
<div class="firmware-avatar-swatch" role="firmware-color" data-color="#F5E53F" style="background:#F5E53F"></div>
<div class="firmware-avatar-swatch" role="firmware-color" data-color="#00A651" style="background:#00A651"></div>
<div class="firmware-avatar-swatch" role="firmware-color" data-color="#00A0E9" style="background:#00A0E9"></div>
<div class="firmware-avatar-swatch" role="firmware-color" data-color="#1D2088" style="background:#1D2088"></div>
<div class="firmware-avatar-swatch" role="firmware-color" data-color="#920783" style="background:#920783"></div>
<div class="firmware-avatar-swatch" role="firmware-color" data-color="#E4007F" style="background:#E4007F"></div>
<div class="firmware-avatar-swatch" role="firmware-color" data-color="#8C8C8C" style="background:#8C8C8C"></div>
<div class="firmware-avatar-swatch" role="firmware-color" data-color="#C9A26B" style="background:#C9A26B"></div>
</div>
<button id="firmwareAvatarChoose">Choose</button>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,26 @@
<html window-frame="solid-with-shadow" window-icon="window-icon.png">
<head>
<link href="base.css" rel="stylesheet" type="text/css" />
<link href="dialog.css" rel="stylesheet" type="text/css" />
<link href="profile_image_selector_dialog.css" rel="stylesheet" type="text/css" />
<title>Profile Image Selection</title>
</head>
<header>
<window-caption role="window-caption"><img src="window-icon-32.png" /><span>Choose a profile Image</span></window-caption>
<window-buttons>
<window-button role="window-close"></window-button>
</window-buttons>
</header>
<body>
<div class="profile-image-selector-body">
<p id="ProfileImageSelectionNote">You may import a custom profile image, or select an avatar from system firmware</p>
</div>
<div class="footer profile-image-selector-footer">
<button id="profileImageBack" role="window-close">Back</button>
<div class="profile-image-selector-actions">
<button id="profileImageImport">Import Image File</button>
<button id="profileImageFirmware">Select Firmware Avatar</button>
</div>
</div>
</body>
</html>

View File

@ -146,6 +146,7 @@ PageNav.page_tabs page.active {
<Page contents="system_config_general.html" label="General" Name="General" />
<Page contents="system_config_graphics.html" label="Graphics" Name="Graphics" />
<Page contents="system_config_audio.html" label="Audio" Name="Audio" />
<Page contents="system_config_profiles.html" label="Profiles" Name="Profiles" />
</PageNav>
<div id="ConfigContents" />
</div>

View File

@ -0,0 +1,7 @@
<PageNav target="ProfilesContents" id="ProfilesTabNav" class="page_tabs">
<Page label="Manage Profiles" Name="ManageProfiles">
<div id="ActiveProfileLabel" class="active-profile-label">Active profile: </div>
<div id="profileGrid" class="profile-grid"></div>
</Page>
</PageNav>
<div id="ProfilesContents" class="page_content profiles-page" />

View File

@ -29,7 +29,7 @@ enum
MODULE_LOADER_SPECS_VERSION = 0x0123,
MODULE_VIDEO_SPECS_VERSION = 0x011A,
MODULE_CPU_SPECS_VERSION = 0x0111,
MODULE_OPERATING_SYSTEM_SPECS_VERSION = 0x0118,
MODULE_OPERATING_SYSTEM_SPECS_VERSION = 0x0119,
};
enum MODULE_TYPE : uint16_t

View File

@ -646,6 +646,19 @@ nxinterface IButtonMappingList
virtual void Release() = 0;
};
enum
{
HOST_PROFILE_UUID_SIZE = 16,
HOST_PROFILE_USERNAME_SIZE = 32,
HOST_PROFILE_MAX_USERS = 8,
};
typedef struct HostProfileInfo
{
uint8_t uuid[HOST_PROFILE_UUID_SIZE];
char username[HOST_PROFILE_USERNAME_SIZE + 1];
} HostProfileInfo;
nxinterface IOperatingSystem
{
virtual bool Initialize() = 0;
@ -685,6 +698,13 @@ nxinterface IOperatingSystem
virtual void SetFrontendApplets(ICabinetFrontendApplet * cabinet, IControllerFrontendApplet * controller, IErrorFrontendApplet * error, IMiiEditFrontendApplet * mii_edit, IParentalControlsFrontendApplet * parental_controls, IPhotoViewerFrontendApplet * photo_viewer, IProfileSelectFrontendApplet * profile_select, ISoftwareKeyboardFrontendApplet * software_keyboard, IWebBrowserFrontendApplet * web_browser) = 0;
virtual void SetPlayerButtonState(uint32_t player_index, uint32_t button_ordinal, bool pressed) = 0;
virtual void SetPlayerAnalogState(uint32_t player_index, uint32_t stick_index, float x, float y) = 0;
virtual uint32_t GetProfileCount() const = 0;
virtual bool GetProfile(uint32_t index, HostProfileInfo * profile) const = 0;
virtual bool CreateProfile(const uint8_t uuid[HOST_PROFILE_UUID_SIZE], const char * username_utf8, HostProfileInfo * out_profile) = 0;
virtual bool RenameProfile(const uint8_t uuid[HOST_PROFILE_UUID_SIZE], const char * username_utf8) = 0;
virtual bool RemoveProfile(const uint8_t uuid[HOST_PROFILE_UUID_SIZE]) = 0;
virtual bool SetProfileImage(const uint8_t uuid[HOST_PROFILE_UUID_SIZE], const uint8_t * image_data, uint32_t image_size) = 0;
virtual bool GetProfileImagePath(const uint8_t uuid[HOST_PROFILE_UUID_SIZE], char * out_path, uint32_t out_path_size) const = 0;
};
EXPORT IOperatingSystem * CALL CreateOperatingSystem(ISystemModules & modules);

View File

@ -482,6 +482,7 @@
<ClCompile Include="os_time_zone.cpp" />
<ClCompile Include="os_manager.cpp" />
<ClCompile Include="os_settings.cpp" />
<ClCompile Include="profile_image_writer.cpp" />
</ItemGroup>
<ItemGroup>
<None Include="version.h.in" />
@ -992,6 +993,7 @@
<ClInclude Include="os_settings.h" />
<ClInclude Include="player_input.h" />
<ClInclude Include="os_settings_identifiers.h" />
<ClInclude Include="profile_image_writer.h" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\external\cubeb.vcxproj">

View File

@ -2043,6 +2043,9 @@
<ClInclude Include="core\hle\service\ldn\ldn_results.h">
<Filter>Header Files\core\hle\service\ldn</Filter>
</ClInclude>
<ClInclude Include="profile_image_writer.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<None Include="version.h.in">
@ -3409,5 +3412,8 @@
<ClCompile Include="core\hle\service\ldn\user_local_communication_service.cpp">
<Filter>Source Files\core\hle\service\ldn</Filter>
</ClCompile>
<ClCompile Include="profile_image_writer.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>

View File

@ -1,7 +1,9 @@
#include "os_manager.h"
#include "profile_image_writer.h"
#include "core/core_timing.h"
#include "core/cpu_manager.h"
#include "core/hle/kernel/k_process.h"
#include "core/hle/service/acc/profile_manager.h"
#include "core/hle/service/am/applet_manager.h"
#include "core/hle/service/am/frontend/applets.h"
#include "core/hle/service/filesystem/filesystem.h"
@ -9,16 +11,21 @@
#include "os_settings.h"
#include "os_settings_identifiers.h"
#include "yuzu_audio_core/sink/sink_details.h"
#include "yuzu_common/fs/path_util.h"
#include "yuzu_common/settings.h"
#include "yuzu_common/string_util.h"
#include "yuzu_hid_core/frontend/emulated_controller.h"
#include "yuzu_hid_core/hid_core.h"
#include "yuzu_input_common/drivers/keyboard.h"
#include "yuzu_input_common/drivers/virtual_gamepad.h"
#include "yuzu_input_common/main.h"
#include <nxemu-core/settings/identifiers.h>
#include <filesystem>
namespace
{
constexpr char ACC_SAVE_AVATORS_BASE_PATH[] = "system/save/8000000000000010/su/avators";
class IButtonMappingListImpl : public IButtonMappingList
{
public:
@ -88,6 +95,37 @@ namespace
std::vector<uint32_t> m_indices;
std::vector<IParamPackageImpl*> m_params;
};
bool FillHostProfileInfo(const Service::Account::ProfileManager & manager, std::size_t index, HostProfileInfo * out_profile)
{
if (out_profile == nullptr)
{
return false;
}
const auto uuid = manager.GetUser(index);
if (!uuid)
{
return false;
}
Service::Account::ProfileBase profile{};
if (!manager.GetProfileBase(*uuid, profile))
{
return false;
}
std::memcpy(out_profile->uuid, profile.user_uuid.uuid.data(), HOST_PROFILE_UUID_SIZE);
const std::string username = Common::StringFromFixedZeroTerminatedBuffer((const char *)profile.username.data(), profile.username.size());
std::memset(out_profile->username, 0, sizeof(out_profile->username));
std::strncpy(out_profile->username, username.c_str(), HOST_PROFILE_USERNAME_SIZE);
return true;
}
std::filesystem::path ProfileImageFilesystemPath(const Common::UUID & uuid)
{
return Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir) / ACC_SAVE_AVATORS_BASE_PATH / (uuid.FormattedString() + ".jpg");
}
}
extern IModuleSettings * g_settings;
@ -416,4 +454,159 @@ void OSManager::SetPlayerAnalogState(uint32_t player_index, uint32_t stick_index
return;
}
virtual_gamepad->SetStickPosition(player_index, static_cast<int>(stick_index), x, y);
}
}
uint32_t OSManager::GetProfileCount() const
{
Service::Account::ProfileManager manager;
return (uint32_t)manager.GetUserCount();
}
bool OSManager::GetProfile(uint32_t index, HostProfileInfo * out_profile) const
{
Service::Account::ProfileManager manager;
return FillHostProfileInfo(manager, index, out_profile);
}
bool OSManager::CreateProfile(const uint8_t uuid_bytes[HOST_PROFILE_UUID_SIZE], const char * username_utf8, HostProfileInfo * out_profile)
{
if (uuid_bytes == nullptr || username_utf8 == nullptr || username_utf8[0] == '\0')
{
return false;
}
Service::Account::ProfileManager manager;
if (manager.GetUserCount() >= Service::Account::MAX_USERS)
{
return false;
}
std::array<uint8_t, HOST_PROFILE_UUID_SIZE> uuid_array{};
std::memcpy(uuid_array.data(), uuid_bytes, HOST_PROFILE_UUID_SIZE);
const Common::UUID uuid{uuid_array};
if (uuid.IsInvalid() || manager.UserExists(uuid))
{
return false;
}
if (manager.CreateNewUser(uuid, std::string(username_utf8)).IsError())
{
return false;
}
manager.WriteUserSaveFile();
if (out_profile == nullptr)
{
return true;
}
const auto index = manager.GetUserIndex(uuid);
return index && FillHostProfileInfo(manager, *index, out_profile);
}
bool OSManager::RenameProfile(const uint8_t uuid_bytes[HOST_PROFILE_UUID_SIZE], const char * username_utf8)
{
if (uuid_bytes == nullptr || username_utf8 == nullptr || username_utf8[0] == '\0')
{
return false;
}
std::array<uint8_t, HOST_PROFILE_UUID_SIZE> uuid_array{};
std::memcpy(uuid_array.data(), uuid_bytes, HOST_PROFILE_UUID_SIZE);
const Common::UUID uuid{uuid_array};
Service::Account::ProfileManager manager;
Service::Account::ProfileBase profile{};
if (!manager.GetProfileBase(uuid, profile))
{
return false;
}
const std::string username(username_utf8);
profile.username.fill(0);
std::copy_n(username.begin(), std::min(username.size(), profile.username.size()), profile.username.begin());
if (!manager.SetProfileBase(uuid, profile))
{
return false;
}
manager.WriteUserSaveFile();
return true;
}
bool OSManager::RemoveProfile(const uint8_t uuid_bytes[HOST_PROFILE_UUID_SIZE])
{
if (uuid_bytes == nullptr)
{
return false;
}
std::array<uint8_t, HOST_PROFILE_UUID_SIZE> uuid_array{};
std::memcpy(uuid_array.data(), uuid_bytes, HOST_PROFILE_UUID_SIZE);
const Common::UUID uuid{uuid_array};
Service::Account::ProfileManager manager;
if (manager.GetUserCount() < 2 || !manager.RemoveUser(uuid))
{
return false;
}
manager.WriteUserSaveFile();
return true;
}
bool OSManager::SetProfileImage(const uint8_t uuid_bytes[HOST_PROFILE_UUID_SIZE], const uint8_t * image_data, uint32_t image_size)
{
if (uuid_bytes == nullptr || image_data == nullptr || image_size == 0)
{
return false;
}
std::array<uint8_t, HOST_PROFILE_UUID_SIZE> uuid_array{};
std::memcpy(uuid_array.data(), uuid_bytes, HOST_PROFILE_UUID_SIZE);
const Common::UUID uuid{uuid_array};
Service::Account::ProfileManager manager;
if (!manager.UserExists(uuid))
{
return false;
}
return WriteProfileJpegFromMemory(image_data, image_size, ProfileImageFilesystemPath(uuid));
}
bool OSManager::GetProfileImagePath(const uint8_t uuid_bytes[HOST_PROFILE_UUID_SIZE], char * out_path, uint32_t out_path_size) const
{
if (uuid_bytes == nullptr || out_path == nullptr || out_path_size == 0)
{
return false;
}
std::array<uint8_t, HOST_PROFILE_UUID_SIZE> uuid_array{};
std::memcpy(uuid_array.data(), uuid_bytes, HOST_PROFILE_UUID_SIZE);
const Common::UUID uuid{uuid_array};
Service::Account::ProfileManager manager;
if (!manager.UserExists(uuid))
{
return false;
}
const std::filesystem::path imagePath = ProfileImageFilesystemPath(uuid);
std::error_code ec;
if (!std::filesystem::exists(imagePath, ec) || ec)
{
out_path[0] = '\0';
return true;
}
const std::string path = Common::FS::PathToUTF8String(imagePath);
if (path.size() + 1 > out_path_size)
{
return false;
}
std::memcpy(out_path, path.c_str(), path.size() + 1);
return true;
}

View File

@ -51,6 +51,13 @@ public:
void SetFrontendApplets(ICabinetFrontendApplet * cabinet, IControllerFrontendApplet * controller, IErrorFrontendApplet * error, IMiiEditFrontendApplet * mii_edit, IParentalControlsFrontendApplet * parental_controls, IPhotoViewerFrontendApplet * photo_viewer, IProfileSelectFrontendApplet * profile_select, ISoftwareKeyboardFrontendApplet * software_keyboard, IWebBrowserFrontendApplet * web_browser) override;
void SetPlayerButtonState(uint32_t player_index, uint32_t button_ordinal, bool pressed) override;
void SetPlayerAnalogState(uint32_t player_index, uint32_t stick_index, float x, float y) override;
uint32_t GetProfileCount() const override;
bool GetProfile(uint32_t index, HostProfileInfo * out_profile) const override;
bool CreateProfile(const uint8_t uuid[HOST_PROFILE_UUID_SIZE], const char * username_utf8, HostProfileInfo * out_profile) override;
bool RenameProfile(const uint8_t uuid[HOST_PROFILE_UUID_SIZE], const char * username_utf8) override;
bool RemoveProfile(const uint8_t uuid[HOST_PROFILE_UUID_SIZE]) override;
bool SetProfileImage(const uint8_t uuid[HOST_PROFILE_UUID_SIZE], const uint8_t * image_data, uint32_t image_size) override;
bool GetProfileImagePath(const uint8_t uuid[HOST_PROFILE_UUID_SIZE], char * out_path, uint32_t out_path_size) const override;
private:
OSManager() = delete;

View File

@ -0,0 +1,86 @@
#include "profile_image_writer.h"
#include "yuzu_common/fs/file.h"
#include "yuzu_common/fs/fs.h"
#include "yuzu_common/fs/path_util.h"
#include "yuzu_common/stb.h"
namespace
{
constexpr int ProfileDimension = 256;
constexpr std::size_t ProfilePixelCount = static_cast<std::size_t>(ProfileDimension) * ProfileDimension;
void JpgToMemory(void * context, void * data, int len)
{
std::vector<uint8_t> * jpg = (std::vector<uint8_t> *)context;
const unsigned char * bytes = (const unsigned char *)data;
jpg->insert(jpg->end(), bytes, bytes + len);
}
bool WriteJpegRgb(const uint8_t * rgb, const std::filesystem::path & destination)
{
if (!Common::FS::CreateParentDirs(destination))
{
return false;
}
std::error_code ec;
if (std::filesystem::exists(destination, ec))
{
if (!Common::FS::RemoveFile(destination))
{
return false;
}
}
std::vector<uint8_t> jpeg;
if (!stbi_write_jpg_to_func(JpgToMemory, &jpeg, ProfileDimension, ProfileDimension, STBI_rgb, rgb, 100))
{
return false;
}
Common::FS::IOFile file(destination, Common::FS::FileAccessMode::Write, Common::FS::FileType::BinaryFile);
if (!file.IsOpen() || !file.SetSize(jpeg.size()) || file.Write(jpeg) != jpeg.size())
{
return false;
}
return true;
}
} // namespace
bool WriteProfileJpegFromMemory(const uint8_t * data, size_t size, const std::filesystem::path & destination)
{
if (data == nullptr || size == 0)
{
return false;
}
int32_t width = 0, height = 0, channels = 0;
unsigned char * pixels = stbi_load_from_memory(data, static_cast<int>(size), &width, &height, &channels, STBI_rgb);
if (pixels == nullptr || width <= 0 || height <= 0)
{
if (pixels != nullptr)
{
stbi_image_free(pixels);
}
return false;
}
std::vector<uint8_t> resized(ProfilePixelCount * 3);
if (width != ProfileDimension || height != ProfileDimension)
{
if (!stbir_resize_uint8(pixels, width, height, 0, resized.data(), ProfileDimension, ProfileDimension, 0, STBI_rgb))
{
stbi_image_free(pixels);
return false;
}
stbi_image_free(pixels);
pixels = nullptr;
}
else
{
std::memcpy(resized.data(), pixels, resized.size());
stbi_image_free(pixels);
pixels = nullptr;
}
return WriteJpegRgb(resized.data(), destination);
}

View File

@ -0,0 +1,6 @@
#pragma once
#include <stdint.h>
#include <filesystem>
bool WriteProfileJpegFromMemory(const uint8_t * data, size_t size, const std::filesystem::path & destination);

View File

@ -24,7 +24,7 @@
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>$(SolutionDir)external\fmt\include;$(SolutionDir)external\vulkan-headers\include;$(SolutionDir)external\sciterui\include;$(SolutionDir)src\3rd_party\discord-rpc\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>$(SolutionDir)src\3rd_party\stb;$(SolutionDir)external\fmt\include;$(SolutionDir)external\vulkan-headers\include;$(SolutionDir)external\sciterui\include;$(SolutionDir)src\3rd_party\discord-rpc\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<UseStandardPreprocessor>true</UseStandardPreprocessor>
<PreprocessorDefinitions>NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
@ -43,6 +43,7 @@
<ClCompile Include="startup_checks.cpp" />
<ClCompile Include="user_interface\game_list_worker.cpp" />
<ClCompile Include="user_interface\hotkey_map.cpp" />
<ClCompile Include="user_interface\html_utils.cpp" />
<ClCompile Include="user_interface\key_mappings.cpp" />
<ClCompile Include="user_interface\about_dialog.cpp" />
<ClCompile Include="user_interface\discord_presence.cpp" />
@ -50,12 +51,16 @@
<ClCompile Include="user_interface\sciter_main_window.cpp" />
<ClCompile Include="user_interface\settings\config_setting .cpp" />
<ClCompile Include="user_interface\settings\input_config_player.cpp" />
<ClCompile Include="user_interface\settings\profile_editor_dialog.cpp" />
<ClCompile Include="user_interface\settings\profile_firmware_avatar_dialog.cpp" />
<ClCompile Include="user_interface\settings\profile_image_selector_dialog.cpp" />
<ClCompile Include="user_interface\settings\system_config.cpp" />
<ClCompile Include="user_interface\settings\input_config.cpp" />
<ClCompile Include="user_interface\settings\system_config_audio.cpp" />
<ClCompile Include="user_interface\settings\system_config_game_browser.cpp" />
<ClCompile Include="user_interface\settings\system_config_general.cpp" />
<ClCompile Include="user_interface\settings\system_config_graphics.cpp" />
<ClCompile Include="user_interface\settings\system_config_profiles.cpp" />
<ClCompile Include="user_interface\settings\system_config_system.cpp" />
<ClCompile Include="user_interface\settings\system_config_hotkeys.cpp" />
<ClCompile Include="user_interface\settings\system_config_logging.cpp" />
@ -69,6 +74,7 @@
<ClInclude Include="startup_checks.h" />
<ClInclude Include="user_interface\game_list_worker.h" />
<ClInclude Include="user_interface\hotkey_map.h" />
<ClInclude Include="user_interface\html_utils.h" />
<ClInclude Include="user_interface\key_mappings.h" />
<ClInclude Include="user_interface\about_dialog.h" />
<ClInclude Include="user_interface\discord_presence.h" />
@ -77,6 +83,9 @@
<ClInclude Include="user_interface\sciter_main_window.h" />
<ClInclude Include="user_interface\settings\config_setting.h" />
<ClInclude Include="user_interface\settings\input_config_player.h" />
<ClInclude Include="user_interface\settings\profile_editor_dialog.h" />
<ClInclude Include="user_interface\settings\profile_firmware_avatar_dialog.h" />
<ClInclude Include="user_interface\settings\profile_image_selector_dialog.h" />
<ClInclude Include="user_interface\settings\system_config.h" />
<ClInclude Include="user_interface\settings\input_config.h" />
<ClInclude Include="user_interface\settings\system_config_audio.h" />
@ -84,6 +93,7 @@
<ClInclude Include="user_interface\settings\system_config_game_browser.h" />
<ClInclude Include="user_interface\settings\system_config_general.h" />
<ClInclude Include="user_interface\settings\system_config_graphics.h" />
<ClInclude Include="user_interface\settings\system_config_profiles.h" />
<ClInclude Include="user_interface\settings\system_config_system.h" />
<ClInclude Include="user_interface\settings\system_config_hotkeys.h" />
<ClInclude Include="user_interface\settings\system_config_logging.h" />

View File

@ -111,6 +111,21 @@
<ClCompile Include="applets\web_browser.cpp">
<Filter>Source Files\applets</Filter>
</ClCompile>
<ClCompile Include="user_interface\settings\system_config_profiles.cpp">
<Filter>Source Files\user_interface\settings</Filter>
</ClCompile>
<ClCompile Include="user_interface\html_utils.cpp">
<Filter>Source Files\user_interface</Filter>
</ClCompile>
<ClCompile Include="user_interface\settings\profile_editor_dialog.cpp">
<Filter>Source Files\user_interface\settings</Filter>
</ClCompile>
<ClCompile Include="user_interface\settings\profile_image_selector_dialog.cpp">
<Filter>Source Files\user_interface\settings</Filter>
</ClCompile>
<ClCompile Include="user_interface\settings\profile_firmware_avatar_dialog.cpp">
<Filter>Source Files\user_interface\settings</Filter>
</ClCompile>
<ClCompile Include="user_interface\discord_presence.cpp">
<Filter>Source Files\user_interface</Filter>
</ClCompile>
@ -191,6 +206,21 @@
<ClInclude Include="applets\web_browser.h">
<Filter>Header Files\applets</Filter>
</ClInclude>
<ClInclude Include="user_interface\settings\system_config_profiles.h">
<Filter>Header Files\user_interface\settings</Filter>
</ClInclude>
<ClInclude Include="user_interface\html_utils.h">
<Filter>Header Files\user_interface</Filter>
</ClInclude>
<ClInclude Include="user_interface\settings\profile_editor_dialog.h">
<Filter>Header Files\user_interface\settings</Filter>
</ClInclude>
<ClInclude Include="user_interface\settings\profile_image_selector_dialog.h">
<Filter>Header Files\user_interface\settings</Filter>
</ClInclude>
<ClInclude Include="user_interface\settings\profile_firmware_avatar_dialog.h">
<Filter>Header Files\user_interface\settings</Filter>
</ClInclude>
<ClInclude Include="user_interface\discord_presence.h">
<Filter>Header Files\user_interface</Filter>
</ClInclude>

View File

@ -1,4 +1,5 @@
#include "about_dialog.h"
#include "user_interface/html_utils.h"
#include <common/shell_open.h>
#include <nxemu-core/version.h>
@ -8,19 +9,6 @@
#include <string>
namespace
{
void AttachClickHandler(ISciterUI & sciterUI, const SciterElement & element, IClickSink * sink)
{
if (element.IsValid())
{
sciterUI.AttachHandler(element, IID_ICLICKSINK, sink);
}
}
} // namespace
AboutDialog::AboutDialog(ISciterUI & sciterUI) :
m_sciterUI(sciterUI),
m_window(nullptr)

View File

@ -0,0 +1,108 @@
#include "html_utils.h"
#include <common/base64.h>
#include <common/std_string.h>
#include <sciter_element.h>
#include <sciter_handler.h>
#include <fstream>
#include <vector>
namespace
{
const char * SniffImageMime(const uint8_t * data, size_t size)
{
if (size >= 8 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47)
{
return "image/png";
}
if (size >= 2 && data[0] == 0xff && data[1] == 0xd8)
{
return "image/jpeg";
}
if (size >= 3 && data[0] == 'G' && data[1] == 'I' && data[2] == 'F')
{
return "image/gif";
}
return "image/jpeg";
}
} // namespace
std::string HtmlEscape(const std::string & text)
{
std::string out;
out.reserve(text.size());
for (const char ch : text)
{
switch (ch)
{
case '&': out += "&amp;"; break;
case '<': out += "&lt;"; break;
case '>': out += "&gt;"; break;
case '"': out += "&quot;"; break;
case '\'': out += "&#39;"; break;
default: out.push_back(ch); break;
}
}
return out;
}
std::string ImageDataUri(const uint8_t * data, size_t size)
{
if (data == nullptr || size == 0)
{
return {};
}
return stdstr_f("data:%s;base64,%s", SniffImageMime(data, size), base64_encode(data, size).c_str());
}
std::string ImageDataUriFromFile(const char * path)
{
if (path == nullptr || path[0] == '\0')
{
return {};
}
std::ifstream file(path, std::ios::binary);
if (!file)
{
return {};
}
const std::vector<uint8_t> data((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
return ImageDataUri(data.data(), data.size());
}
void AttachClickHandler(ISciterUI & sciterUI, const SciterElement & element, IClickSink * sink)
{
if (element.IsValid())
{
sciterUI.AttachHandler(element, IID_ICLICKSINK, sink);
}
}
void SetElementEnabled(const SciterElement & root, const char * id, bool enabled)
{
SciterElement el(root.GetElementByID(id));
if (!el.IsValid())
{
return;
}
if (enabled)
{
el.SetState(0, SciterElement::STATE_DISABLED, true);
}
else
{
el.SetState(SciterElement::STATE_DISABLED, 0, true);
}
}
void SetElementVisible(const SciterElement & root, const char * id, bool visible)
{
SciterElement el(root.GetElementByID(id));
if (!el.IsValid())
{
return;
}
el.SetStyleAttribute("display", visible ? "" : "none");
}

View File

@ -0,0 +1,14 @@
#pragma once
#include <string>
__interface ISciterUI;
__interface IClickSink;
class SciterElement;
std::string HtmlEscape(const std::string & text);
std::string ImageDataUri(const uint8_t * data, size_t size);
std::string ImageDataUriFromFile(const char * path);
void AttachClickHandler(ISciterUI & sciterUI, const SciterElement & element, IClickSink * sink);
void SetElementEnabled(const SciterElement & root, const char * id, bool enabled);
void SetElementVisible(const SciterElement & root, const char * id, bool visible);

View File

@ -3,9 +3,9 @@
#include "settings/system_config.h"
#include "settings/ui_settings.h"
#include "user_interface/about_dialog.h"
#include "user_interface/html_utils.h"
#include "user_interface/key_mappings.h"
#include "user_interface/notification.h"
#include <common/base64.h>
#include <common/shell_open.h>
#include <common/std_string.h>
#include <nxemu-core/notification.h>
@ -111,17 +111,7 @@ void LoadImageToElement(SciterElement elem, const std::vector<uint8_t> & data)
}
if (!data.empty())
{
const char * mime = "image/png";
if (data.size() >= 2 && data[0] == 0xff && data[1] == 0xd8)
{
mime = "image/jpeg";
}
else if (data.size() >= 4 && data[0] == 'G' && data[1] == 'I' && data[2] == 'F')
{
mime = "image/gif";
}
const std::string uri = stdstr_f("data:%s;base64,%s", mime, base64_encode(data.data(), data.size()).c_str());
elem.SetAttribute("src", uri.c_str());
elem.SetAttribute("src", ImageDataUri(data.data(), data.size()).c_str());
}
elem.SetStyleAttribute("display", data.empty() ? "none" : "block");
}
@ -165,34 +155,6 @@ void UpdateLoadingProgressBar(SciterElement & fillEl, bool indeterminate, int wi
}
}
std::string HtmlEscapeForHtmlContent(const std::string & s)
{
std::string out;
out.reserve(s.size());
for (const char c : s)
{
switch (c)
{
case '&':
out += "&amp;";
break;
case '<':
out += "&lt;";
break;
case '>':
out += "&gt;";
break;
case '"':
out += "&quot;";
break;
default:
out += c;
break;
}
}
return out;
}
std::string GetGameTitleLoadingHtml(ISystemloader & loader, const char * verb)
{
IRomInfoPtr info(loader.LoadedRomInfo());
@ -210,7 +172,7 @@ std::string GetGameTitleLoadingHtml(ISystemloader & loader, const char * verb)
{
return stdstr_f("<span class=\"loading-verb\">%s</span> <span class=\"loading-game-name\">...</span>", verb);
}
const std::string titleEsc = HtmlEscapeForHtmlContent(std::string(buf.data()));
const std::string titleEsc = HtmlEscape(std::string(buf.data()));
return stdstr_f("<span class=\"loading-verb\">%s</span> <span class=\"loading-game-name\">%s</span>", verb, titleEsc.c_str());
}
@ -1055,7 +1017,7 @@ void SciterMainWindow::RefreshFirmwareInstallLoading()
if (status.IsValid())
{
const std::string text = stdstr_f("<span class=\"loading-verb\">Installing firmware</span> <span class=\"loading-game-name\">%s</span>", HtmlEscapeForHtmlContent(detail).c_str());
const std::string text = stdstr_f("<span class=\"loading-verb\">Installing firmware</span> <span class=\"loading-game-name\">%s</span>", HtmlEscape(detail).c_str());
status.SetHTML(reinterpret_cast<const uint8_t *>(text.c_str()), text.size());
}
m_sciterUI.UpdateWindow(m_rootElement.GetElementHwnd(true));

View File

@ -0,0 +1,361 @@
#include "profile_editor_dialog.h"
#include "profile_image_selector_dialog.h"
#include "user_interface/html_utils.h"
#include "user_interface/notification.h"
#include <common/std_string.h>
#include <nxemu-core/modules/system_modules.h>
#include <nxemu-core/settings/settings.h>
#include <nxemu-os/os_settings_identifiers.h>
#include <sciter_element.h>
#include <sciter_ui.h>
#include <yuzu_common/uuid.h>
namespace
{
std::string FormatUuid(const HostProfileInfo & profile)
{
std::string uuid_text;
uuid_text.reserve(36);
static const char * hex = "0123456789ABCDEF";
for (uint32_t i = 0; i < HOST_PROFILE_UUID_SIZE; ++i)
{
if (i == 4 || i == 6 || i == 8 || i == 10)
{
uuid_text.push_back('-');
}
uuid_text.push_back(hex[(profile.uuid[i] >> 4) & 0xF]);
uuid_text.push_back(hex[profile.uuid[i] & 0xF]);
}
return uuid_text;
}
}
ProfileEditorDialog::ProfileEditorDialog(ISciterUI & sciterUI, SystemModules & modules) :
m_sciterUI(sciterUI),
m_modules(modules),
m_window(nullptr),
m_mode(ProfileEditorMode::EditExistingProfile),
m_editingIndex(-1),
m_editingProfile{},
m_pendingImage{},
m_changed(false)
{
}
ProfileEditorDialog::~ProfileEditorDialog() = default;
bool ProfileEditorDialog::Display(void * parentWindow, ProfileEditorMode mode, int32_t profileIndex)
{
enum
{
WINDOW_WIDTH = 520,
};
m_mode = mode;
m_editingIndex = profileIndex;
m_editingProfile = {};
m_pendingImage = {};
m_changed = false;
m_window = nullptr;
if (!m_modules.IsValid())
{
return false;
}
IOperatingSystem & os = m_modules.Modules().OperatingSystem();
if (m_mode == ProfileEditorMode::CreateNewProfile)
{
if (os.GetProfileCount() >= HOST_PROFILE_MAX_USERS)
{
Notification::GetInstance().DisplayError("Unable to create profile. A maximum of 8 users is supported.", "Profiles");
return false;
}
const Common::UUID uuid = Common::UUID::MakeRandom();
std::memcpy(m_editingProfile.uuid, uuid.uuid.data(), HOST_PROFILE_UUID_SIZE);
m_editingProfile.username[0] = '\0';
m_editingIndex = -1;
}
else if (m_mode == ProfileEditorMode::EditExistingProfile && !os.GetProfile(static_cast<uint32_t>(profileIndex), &m_editingProfile))
{
return false;
}
if (!m_sciterUI.WindowCreate(parentWindow, "profile_editor_dialog.html", 0, 0, WINDOW_WIDTH, 0, SUIW_CHILD, m_window))
{
return false;
}
SciterElement root(m_window->GetRootElement());
if (root.IsValid())
{
SciterElement titleEl(root.GetElementByID("ProfileEditorTitle"));
if (titleEl.IsValid())
{
titleEl.SetText(m_mode == ProfileEditorMode::CreateNewProfile ? "Create Profile" : "Edit Profile");
}
SciterElement usernameInput(root.GetElementByID("ProfileUsername"));
SciterElement userIdInput(root.GetElementByID("ProfileUserId"));
if (usernameInput.IsValid())
{
usernameInput.SetValue(SciterValue(std::string(m_editingProfile.username)));
}
if (userIdInput.IsValid())
{
userIdInput.SetValue(SciterValue(FormatUuid(m_editingProfile)));
}
SetDetailImage(m_mode == ProfileEditorMode::CreateNewProfile ? std::string() : GetImageFileUrl(m_editingProfile));
UpdateControls();
AttachClickHandler(m_sciterUI, root.GetElementByID("profileRemove"), this);
AttachClickHandler(m_sciterUI, root.GetElementByID("profileSetImage"), this);
AttachClickHandler(m_sciterUI, root.GetElementByID("profileSave"), this);
}
m_window->FixMinSize();
m_window->CenterWindow();
m_window->RunModal();
return m_changed;
}
bool ProfileEditorDialog::OnClick(SCITER_ELEMENT element, SCITER_ELEMENT /*source*/, uint32_t /*reason*/)
{
SciterElement clickElem(element);
const std::string elementID = clickElem.GetAttributeByName("id");
if (!m_modules.IsValid())
{
return true;
}
IOperatingSystem & os = m_modules.Modules().OperatingSystem();
if (elementID == "profileSave")
{
const std::string username = GetUsernameInput();
if (username.empty())
{
Notification::GetInstance().DisplayError("Enter a username for the profile.", "Profiles");
return true;
}
if (m_mode == ProfileEditorMode::CreateNewProfile)
{
HostProfileInfo created{};
if (!os.CreateProfile(m_editingProfile.uuid, username.c_str(), &created))
{
Notification::GetInstance().DisplayError("Unable to create profile. A maximum of 8 users is supported.", "Profiles");
return true;
}
m_editingProfile = created;
m_editingIndex = (int32_t)(os.GetProfileCount() - 1);
if (m_pendingImage.HasImage() && !CommitPendingImage())
{
return true;
}
m_mode = ProfileEditorMode::EditExistingProfile;
m_changed = true;
Close();
return true;
}
if (!os.RenameProfile(m_editingProfile.uuid, username.c_str()))
{
Notification::GetInstance().DisplayError("Unable to rename profile.", "Profiles");
return true;
}
if (m_pendingImage.HasImage() && !CommitPendingImage())
{
return true;
}
m_changed = true;
Close();
return true;
}
if (elementID == "profileRemove")
{
if (m_mode == ProfileEditorMode::CreateNewProfile)
{
return true;
}
const std::string message = stdstr_f("Delete user \"%s\"?\n\nSave data for this user will remain on disk.", m_editingProfile.username);
if (Notification::GetInstance().Query(message.c_str(), "Confirm Delete") != NotificationResponse::Yes)
{
return true;
}
if (!os.RemoveProfile(m_editingProfile.uuid))
{
Notification::GetInstance().DisplayError("Unable to delete profile. At least one user must remain.", "Profiles");
return true;
}
SettingsStore & settings = SettingsStore::GetInstance();
if (settings.GetInt(NXOsSetting::CurrentUser) == m_editingIndex)
{
settings.SetInt(NXOsSetting::CurrentUser, 0);
}
else if (settings.GetInt(NXOsSetting::CurrentUser) > m_editingIndex)
{
settings.SetInt(NXOsSetting::CurrentUser, settings.GetInt(NXOsSetting::CurrentUser) - 1);
}
m_pendingImage = {};
m_changed = true;
Close();
return true;
}
if (elementID == "profileSetImage")
{
ProfileImageSelectorDialog selector(m_sciterUI, m_modules);
PendingProfileImage selection{};
if (selector.Display((void *)(m_window != nullptr ? m_window->GetHandle() : nullptr), selection))
{
m_pendingImage = selection;
SetDetailImage(m_pendingImage.HasImage() ? ImageDataUri(m_pendingImage.data.data(), m_pendingImage.data.size()) : std::string{});
}
return true;
}
return true;
}
void ProfileEditorDialog::Close()
{
if (m_window == nullptr || m_window->IsClosed())
{
return;
}
m_window->Destroy();
}
void ProfileEditorDialog::UpdateControls()
{
if (m_window == nullptr)
{
return;
}
SciterElement root(m_window->GetRootElement());
const uint32_t count = m_modules.IsValid() ? m_modules.Modules().OperatingSystem().GetProfileCount() : 0;
const bool editing = m_mode == ProfileEditorMode::EditExistingProfile;
SetElementEnabled(root, "profileSave", true);
SetElementEnabled(root, "profileSetImage", true);
SetElementVisible(root, "profileRemove", editing);
SetElementEnabled(root, "profileRemove", editing && count >= 2);
}
void ProfileEditorDialog::SetDetailImage(const std::string & imageUri)
{
if (m_window == nullptr)
{
return;
}
SciterElement image(SciterElement(m_window->GetRootElement()).GetElementByID("ProfileDetailImage"));
if (!image.IsValid())
{
return;
}
image.SetAttribute("src", "");
image.SetStyleAttribute("foreground-image", "none");
SciterElement avatar = image.GetParent();
if (avatar.IsValid())
{
avatar.SetStyleAttribute("foreground-image", "none");
avatar.SetStyleAttribute("background", "");
}
if (imageUri.empty())
{
image.SetStyleAttribute("display", "none");
}
else
{
const std::string foreground = "url(" + imageUri + ")";
image.SetAttribute("src", imageUri.c_str());
image.SetStyleAttribute("foreground-image", foreground.c_str());
image.SetStyleAttribute("foreground-size", "100% 100%");
image.SetStyleAttribute("background", "transparent");
image.SetStyleAttribute("display", "block");
if (avatar.IsValid())
{
avatar.SetStyleAttribute("foreground-image", foreground.c_str());
avatar.SetStyleAttribute("foreground-size", "100% 100%");
}
}
}
bool ProfileEditorDialog::CommitPendingImage()
{
if (!m_modules.IsValid() || !m_pendingImage.HasImage())
{
return true;
}
IOperatingSystem & os = m_modules.Modules().OperatingSystem();
if (!os.SetProfileImage(m_editingProfile.uuid, m_pendingImage.data.data(), (uint32_t)m_pendingImage.data.size()))
{
Notification::GetInstance().DisplayError("Unable to set profile image.", "Profiles");
return false;
}
m_pendingImage = {};
return true;
}
std::string ProfileEditorDialog::GetUsernameInput() const
{
if (m_window == nullptr)
{
return {};
}
SciterElement input(SciterElement(m_window->GetRootElement()).GetElementByID("ProfileUsername"));
if (!input.IsValid())
{
return {};
}
SciterValue value = input.GetValue();
if (!value.isString())
{
return {};
}
std::string username = value.GetValueStr();
if (username.size() > HOST_PROFILE_USERNAME_SIZE)
{
username.resize(HOST_PROFILE_USERNAME_SIZE);
}
return username;
}
std::string ProfileEditorDialog::GetImageFileUrl(const HostProfileInfo & profile) const
{
if (!m_modules.IsValid())
{
return {};
}
char path[1024] = {};
if (!m_modules.Modules().OperatingSystem().GetProfileImagePath(profile.uuid, path, sizeof(path)) || path[0] == '\0')
{
return {};
}
return ImageDataUriFromFile(path);
}

View File

@ -0,0 +1,49 @@
#pragma once
#include "profile_image_selector_dialog.h"
#include <sciter_handler.h>
#include <nxemu-module-spec/operating_system.h>
#include <stdint.h>
__interface ISciterUI;
__interface ISciterWindow;
class SystemModules;
enum class ProfileEditorMode
{
CreateNewProfile,
EditExistingProfile,
};
class ProfileEditorDialog :
public IClickSink
{
public:
ProfileEditorDialog(ISciterUI & sciterUI, SystemModules & modules);
~ProfileEditorDialog();
bool Display(void * parentWindow, ProfileEditorMode mode, int32_t profileIndex = -1);
// IClickSink
bool OnClick(SCITER_ELEMENT element, SCITER_ELEMENT source, uint32_t reason) override;
private:
ProfileEditorDialog() = delete;
ProfileEditorDialog(const ProfileEditorDialog &) = delete;
ProfileEditorDialog & operator=(const ProfileEditorDialog &) = delete;
void Close();
void UpdateControls();
void SetDetailImage(const std::string & imageUri);
bool CommitPendingImage();
std::string GetUsernameInput() const;
std::string GetImageFileUrl(const HostProfileInfo & profile) const;
ISciterUI & m_sciterUI;
SystemModules & m_modules;
ISciterWindow * m_window;
ProfileEditorMode m_mode;
int32_t m_editingIndex;
HostProfileInfo m_editingProfile;
PendingProfileImage m_pendingImage;
bool m_changed;
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,72 @@
#pragma once
#include "profile_image_selector_dialog.h"
#include <sciter_handler.h>
#include <atomic>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
__interface ISciterUI;
class SystemModules;
struct FirmwareProfileAvatar
{
std::string thumb_path;
std::string full_path;
};
struct FirmwareAvatarLoadState
{
std::atomic_bool done{false};
std::mutex mutex;
std::vector<FirmwareProfileAvatar> avatars;
};
class ProfileFirmwareAvatarDialog :
public IClickSink,
public ITimerSink
{
public:
ProfileFirmwareAvatarDialog(ISciterUI & sciterUI, SystemModules & modules);
~ProfileFirmwareAvatarDialog();
bool Display(void * parentWindow, PendingProfileImage & outSelection);
// IClickSink
bool OnClick(SCITER_ELEMENT element, SCITER_ELEMENT source, uint32_t reason) override;
// ITimerSink
bool OnTimer(SCITER_ELEMENT element, uint32_t * timerId) override;
private:
ProfileFirmwareAvatarDialog() = delete;
ProfileFirmwareAvatarDialog(const ProfileFirmwareAvatarDialog &) = delete;
ProfileFirmwareAvatarDialog & operator=(const ProfileFirmwareAvatarDialog &) = delete;
void Close();
void StartAvatarLoad();
void PopulateGridStructure();
void SyncAvatarsFromLoad();
void ShowEmptyAvatarsMessage();
void ApplyNextImageBatch();
void UpdateSelectionHighlight();
void ApplyBackgroundColorPreview();
void SelectBackgroundColor(const std::string & colorCss);
bool ParseSelectedColor(uint8_t & r, uint8_t & g, uint8_t & b) const;
bool ChooseSelected();
ISciterUI & m_sciterUI;
SystemModules & m_modules;
ISciterWindow * m_window;
int32_t m_selectedIndex;
std::string m_backgroundColor;
PendingProfileImage m_selection;
bool m_changed;
std::vector<FirmwareProfileAvatar> m_avatars;
std::vector<std::string> m_pngPaths;
uint32_t m_nextImageIndex;
std::shared_ptr<FirmwareAvatarLoadState> m_loadState;
};

View File

@ -0,0 +1,156 @@
#include "profile_image_selector_dialog.h"
#include "profile_firmware_avatar_dialog.h"
#include "user_interface/html_utils.h"
#include <common/path.h>
#include <nxemu-core/modules/system_modules.h>
#include <nxemu-module-spec/system_loader.h>
#include <sciter_element.h>
#include <sciter_ui.h>
#include <fstream>
ProfileImageSelectorDialog::ProfileImageSelectorDialog(ISciterUI & sciterUI, SystemModules & modules) :
m_sciterUI(sciterUI),
m_modules(modules),
m_window(nullptr),
m_selection{},
m_changed(false)
{
}
ProfileImageSelectorDialog::~ProfileImageSelectorDialog() = default;
bool ProfileImageSelectorDialog::Display(void * parentWindow, PendingProfileImage & outSelection)
{
enum
{
WINDOW_WIDTH = 500,
};
m_selection = {};
m_changed = false;
m_window = nullptr;
outSelection = {};
if (!m_sciterUI.WindowCreate(parentWindow, "profile_image_selector_dialog.html", 0, 0, WINDOW_WIDTH, 0, SUIW_CHILD, m_window))
{
return false;
}
SciterElement root(m_window->GetRootElement());
if (root.IsValid())
{
SciterElement firmwareButton(root.GetElementByID("profileImageFirmware"));
if (firmwareButton.IsValid())
{
if (HasInstalledFirmware())
{
firmwareButton.SetState(0, SciterElement::STATE_DISABLED, true);
}
else
{
firmwareButton.SetState(SciterElement::STATE_DISABLED, 0, true);
}
}
AttachClickHandler(m_sciterUI, root.GetElementByID("profileImageImport"), this);
AttachClickHandler(m_sciterUI, root.GetElementByID("profileImageFirmware"), this);
}
m_window->FixMinSize();
m_window->CenterWindow();
m_window->RunModal();
if (m_changed)
{
outSelection = m_selection;
}
return m_changed;
}
bool ProfileImageSelectorDialog::OnClick(SCITER_ELEMENT element, SCITER_ELEMENT /*source*/, uint32_t /*reason*/)
{
SciterElement clickElem(element);
const std::string elementID = clickElem.GetAttributeByName("id");
if (elementID == "profileImageImport")
{
if (ImportImageFile())
{
m_changed = true;
Close();
}
return true;
}
if (elementID == "profileImageFirmware")
{
if (SelectFirmwareAvatar())
{
m_changed = true;
Close();
}
return true;
}
return true;
}
void ProfileImageSelectorDialog::Close()
{
if (m_window == nullptr || m_window->IsClosed())
{
return;
}
m_window->Destroy();
}
bool ProfileImageSelectorDialog::HasInstalledFirmware() const
{
if (!m_modules.IsValid())
{
return false;
}
char buffer[32]{};
return m_modules.Modules().Systemloader().GetInstalledFirmwareDisplayVersion(buffer, sizeof(buffer)) > 0;
}
bool ProfileImageSelectorDialog::ImportImageFile()
{
Path file;
const char * filter =
"Image Files (*.jpg;*.jpeg;*.png;*.bmp)\0*.jpg;*.jpeg;*.png;*.bmp\0"
"All files (*.*)\0*.*\0";
if (!file.FileSelect((void *)(m_window != nullptr ? m_window->GetHandle() : nullptr), Path(Path::CURRENT_DIRECTORY), filter, true))
{
return false;
}
std::ifstream stream(static_cast<const char *>(file), std::ios::binary);
if (!stream)
{
return false;
}
m_selection = {};
m_selection.data.assign(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
return m_selection.HasImage();
}
bool ProfileImageSelectorDialog::SelectFirmwareAvatar()
{
if (!HasInstalledFirmware())
{
return false;
}
ProfileFirmwareAvatarDialog dialog(m_sciterUI, m_modules);
PendingProfileImage selection{};
if (!dialog.Display((void *)(m_window != nullptr ? m_window->GetHandle() : nullptr), selection))
{
return false;
}
m_selection = selection;
return true;
}

View File

@ -0,0 +1,43 @@
#pragma once
#include <sciter_handler.h>
#include <vector>
__interface ISciterUI;
__interface ISciterWindow;
class SystemModules;
struct PendingProfileImage
{
std::vector<uint8_t> data;
bool HasImage() const { return !data.empty(); }
};
class ProfileImageSelectorDialog :
public IClickSink
{
public:
ProfileImageSelectorDialog(ISciterUI & sciterUI, SystemModules & modules);
~ProfileImageSelectorDialog();
bool Display(void * parentWindow, PendingProfileImage & outSelection);
// IClickSink
bool OnClick(SCITER_ELEMENT element, SCITER_ELEMENT source, uint32_t reason) override;
private:
ProfileImageSelectorDialog() = delete;
ProfileImageSelectorDialog(const ProfileImageSelectorDialog &) = delete;
ProfileImageSelectorDialog & operator=(const ProfileImageSelectorDialog &) = delete;
void Close();
bool ImportImageFile();
bool SelectFirmwareAvatar();
bool HasInstalledFirmware() const;
ISciterUI & m_sciterUI;
SystemModules & m_modules;
ISciterWindow * m_window;
PendingProfileImage m_selection;
bool m_changed;
};

View File

@ -4,6 +4,7 @@
#include "system_config_audio.h"
#include "system_config_general.h"
#include "system_config_graphics.h"
#include "system_config_profiles.h"
#include "system_config_system.h"
#include <common/std_string.h>
#include <nxemu-core/settings/identifiers.h>
@ -330,6 +331,10 @@ void SystemConfig::PageNavCreatedPage(const std::string & pageName, SCITER_ELEME
{
m_systemConfigGeneral.reset(new SystemConfigGeneral(m_sciterUI, *this, *m_window, page));
}
else if (pageName == "Profiles")
{
m_systemConfigProfiles.reset(new SystemConfigProfiles(m_sciterUI, *this, m_modules, *m_window, page));
}
}
void SystemConfig::PageNavPageChanged(const std::string & /*pageName*/, SCITER_ELEMENT /*pageNav*/)
@ -353,6 +358,10 @@ bool SystemConfig::OnClick(SCITER_ELEMENT element, SCITER_ELEMENT /*source*/, ui
{
m_systemConfigGeneral->SaveSetting();
}
if (m_systemConfigProfiles)
{
m_systemConfigProfiles->SaveSetting();
}
if (m_modules.IsValid())
{
m_modules.FlushSettings();

View File

@ -13,6 +13,7 @@ class SciterElement;
class SystemConfigAudio;
class SystemConfigGraphics;
class SystemConfigGeneral;
class SystemConfigProfiles;
class SystemConfigSystem;
class ConfigSetting;
class SystemModules;
@ -86,4 +87,5 @@ private:
std::unique_ptr<SystemConfigGraphics> m_systemConfigGraphics;
std::unique_ptr<SystemConfigAudio> m_systemConfigAudio;
std::unique_ptr<SystemConfigGeneral> m_systemConfigGeneral;
std::unique_ptr<SystemConfigProfiles> m_systemConfigProfiles;
};

View File

@ -0,0 +1,291 @@
#include "system_config_profiles.h"
#include "profile_editor_dialog.h"
#include "user_interface/html_utils.h"
#include "user_interface/notification.h"
#include <common/std_string.h>
#include <nxemu-core/modules/system_modules.h>
#include <nxemu-core/settings/identifiers.h>
#include <nxemu-core/settings/settings.h>
#include <nxemu-os/os_settings_identifiers.h>
SystemConfigProfiles::SystemConfigProfiles(ISciterUI & sciterUI, SystemConfig & config, SystemModules & modules, ISciterWindow & window, SciterElement page) :
m_sciterUI(sciterUI),
m_config(config),
m_modules(modules),
m_window(window),
m_page(page),
m_enabled(!SettingsStore::GetInstance().GetBool(NXCoreSetting::EmulationRunning)),
m_currentUser(SettingsStore::GetInstance().GetInt(NXOsSetting::CurrentUser))
{
SciterElement pageNav = page.GetElementByID("ProfilesTabNav");
std::shared_ptr<void> interfacePtr = pageNav.IsValid() ? m_sciterUI.GetElementInterface(pageNav, IID_IPAGENAV) : nullptr;
if (interfacePtr)
{
m_pageNav = std::static_pointer_cast<IPageNav>(interfacePtr);
m_pageNav->AddSink(this);
}
}
void SystemConfigProfiles::SaveSetting(void)
{
SettingsStore::GetInstance().SetInt(NXOsSetting::CurrentUser, m_currentUser);
}
bool SystemConfigProfiles::PageNavChangeFrom(const std::string & /*pageName*/, SCITER_ELEMENT /*pageNav*/)
{
return true;
}
bool SystemConfigProfiles::PageNavChangeTo(const std::string & /*pageName*/, SCITER_ELEMENT /*pageNav*/)
{
return true;
}
void SystemConfigProfiles::PageNavCreatedPage(const std::string & pageName, SCITER_ELEMENT page)
{
if (pageName == "ManageProfiles")
{
SetupProfilesPage(page);
}
}
void SystemConfigProfiles::PageNavPageChanged(const std::string & /*pageName*/, SCITER_ELEMENT /*pageNav*/)
{
}
void SystemConfigProfiles::SetupProfilesPage(SciterElement page)
{
m_profilesPage = page;
PopulateProfiles();
}
bool SystemConfigProfiles::OnClick(SCITER_ELEMENT /*element*/, SCITER_ELEMENT source, uint32_t /*reason*/)
{
SciterElement clickElem(source);
std::string role;
std::string indexAttr;
SciterElement walk = clickElem;
while (walk.IsValid() && walk != m_page)
{
const std::string walkRole = walk.GetAttribute("role");
const std::string walkIndex = walk.GetAttribute("data-index");
if (walkRole == "profile-edit" || walkRole == "profile-add" || walkRole == "profile-tile")
{
role = walkRole;
if (!walkIndex.empty())
{
indexAttr = walkIndex;
}
break;
}
if (indexAttr.empty() && !walkIndex.empty())
{
indexAttr = walkIndex;
}
walk = walk.GetParent();
}
if (role == "profile-add")
{
if (m_enabled)
{
AddProfile();
}
return true;
}
if (role == "profile-edit")
{
if (m_enabled && !indexAttr.empty())
{
OpenEditor(ProfileEditorMode::EditExistingProfile, std::stoi(indexAttr));
}
return true;
}
if (role == "profile-tile")
{
if (!indexAttr.empty())
{
SelectCurrentUser(std::stoi(indexAttr));
PopulateProfiles();
}
return true;
}
return true;
}
void SystemConfigProfiles::AddProfile()
{
if (!m_modules.IsValid())
{
return;
}
if (m_modules.Modules().OperatingSystem().GetProfileCount() >= HOST_PROFILE_MAX_USERS)
{
Notification::GetInstance().DisplayError("Unable to create profile. A maximum of 8 users is supported.", "Profiles");
return;
}
OpenEditor(ProfileEditorMode::CreateNewProfile);
}
void SystemConfigProfiles::OpenEditor(ProfileEditorMode mode, int32_t index)
{
if (!m_modules.IsValid())
{
return;
}
IOperatingSystem & os = m_modules.Modules().OperatingSystem();
const uint32_t countBefore = os.GetProfileCount();
ProfileEditorDialog dialog(m_sciterUI, m_modules);
if (!dialog.Display((void *)m_window.GetHandle(), mode, index))
{
return;
}
const uint32_t count = os.GetProfileCount();
if (mode == ProfileEditorMode::CreateNewProfile && count > countBefore)
{
m_currentUser = static_cast<int32_t>(count - 1);
}
else if (count < countBefore)
{
m_currentUser = SettingsStore::GetInstance().GetInt(NXOsSetting::CurrentUser);
}
PopulateProfiles();
}
void SystemConfigProfiles::PopulateProfiles()
{
SciterElement grid = m_profilesPage.IsValid() ? m_profilesPage.GetElementByID("profileGrid") : nullptr;
if (!m_profilesPage.IsValid() || !grid.IsValid() || !m_modules.IsValid())
{
return;
}
grid.Clear();
IOperatingSystem & os = m_modules.Modules().OperatingSystem();
const uint32_t count = os.GetProfileCount();
if (count == 0)
{
m_currentUser = 0;
}
else if (m_currentUser < 0 || (uint32_t)m_currentUser >= count)
{
m_currentUser = 0;
}
const int32_t current = m_currentUser;
std::string activeName;
std::string html;
std::vector<std::string> imageUris;
imageUris.reserve(count);
for (uint32_t i = 0; i < count; ++i)
{
HostProfileInfo profile{};
if (!os.GetProfile(i, &profile))
{
continue;
}
const bool isCurrent = (int32_t)i == current;
const std::string imageUri = GetImageFileUrl(profile);
const std::string name = HtmlEscape(profile.username);
imageUris.push_back(imageUri);
if (isCurrent)
{
activeName = profile.username;
}
html += stdstr_f(
"<div class=\"profile-tile%s\" role=\"profile-tile\" data-index=\"%u\">"
"<div class=\"profile-tile-avatar\"></div>"
"<div class=\"profile-tile-name\">%s</div>"
"<div class=\"profile-tile-edit\" role=\"profile-edit\" data-index=\"%u\" %s>"
"<svg viewBox=\"0 0 24 24\">"
"<path fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m18 10 3-3-4-4-3 3m4 4L8 20H4v-4L14 6m4 4-4-4\"/>"
"</svg>"
"</div>"
"</div>",
isCurrent ? " current" : "",
i,
name.c_str(),
i,
m_enabled ? "" : "disabled");
}
SciterElement activeLabel = m_profilesPage.GetElementByID("ActiveProfileLabel");
if (activeLabel.IsValid())
{
activeLabel.SetText(stdstr_f("Active profile: %s", !activeName.empty() ? activeName.c_str() : "").c_str());
}
if (m_enabled && count < HOST_PROFILE_MAX_USERS)
{
html +=
"<div class=\"profile-tile-add\" role=\"profile-add\">"
"<div class=\"profile-tile-add-button\" role=\"profile-add\">+</div>"
"</div>";
}
grid.SetHTML((const uint8_t *)html.c_str(), html.size());
m_sciterUI.AttachHandler(grid, IID_ICLICKSINK, (IClickSink *)this);
for (uint32_t i = 0; i < grid.GetChildCount() && i < imageUris.size(); ++i)
{
if (imageUris[i].empty())
{
continue;
}
SciterElement tile = grid.GetChild(i);
if (!tile.IsValid())
{
continue;
}
SciterElement avatar = tile.FindFirst(".profile-tile-avatar");
if (avatar.IsValid())
{
avatar.SetStyleAttribute("foreground-image", ("url(" + imageUris[i] + ")").c_str());
avatar.SetStyleAttribute("foreground-size", "100% 100%");
avatar.SetStyleAttribute("foreground-repeat", "no-repeat");
avatar.SetStyleAttribute("foreground-position", "50% 50%");
}
}
}
void SystemConfigProfiles::SelectCurrentUser(int32_t index)
{
if (index < 0)
{
return;
}
m_currentUser = index;
}
std::string SystemConfigProfiles::GetImageFileUrl(const HostProfileInfo & profile) const
{
if (!m_modules.IsValid())
{
return {};
}
char path[1024] = {};
if (!m_modules.Modules().OperatingSystem().GetProfileImagePath(profile.uuid, path, sizeof(path)) || path[0] == '\0')
{
return {};
}
return ImageDataUriFromFile(path);
}

View File

@ -0,0 +1,53 @@
#pragma once
#include <sciter_ui.h>
#include <sciter_element.h>
#include <sciter_handler.h>
#include <widgets/page_nav.h>
#include <nxemu-module-spec/operating_system.h>
#include <string>
class SystemConfig;
class SystemModules;
enum class ProfileEditorMode;
class SystemConfigProfiles :
public IPagesSink,
public IClickSink
{
public:
SystemConfigProfiles(ISciterUI & sciterUI, SystemConfig & config, SystemModules & modules, ISciterWindow & window, SciterElement page);
~SystemConfigProfiles() = default;
void SaveSetting(void);
// IPagesSink
bool PageNavChangeFrom(const std::string & pageName, SCITER_ELEMENT pageNav) override;
bool PageNavChangeTo(const std::string & pageName, SCITER_ELEMENT pageNav) override;
void PageNavCreatedPage(const std::string & pageName, SCITER_ELEMENT page) override;
void PageNavPageChanged(const std::string & pageName, SCITER_ELEMENT pageNav) override;
// IClickSink
bool OnClick(SCITER_ELEMENT element, SCITER_ELEMENT source, uint32_t reason) override;
private:
SystemConfigProfiles() = delete;
SystemConfigProfiles(const SystemConfigProfiles &) = delete;
SystemConfigProfiles & operator=(const SystemConfigProfiles &) = delete;
void SetupProfilesPage(SciterElement page);
void PopulateProfiles();
void SelectCurrentUser(int32_t index);
void AddProfile();
void OpenEditor(ProfileEditorMode mode, int32_t index = -1);
std::string GetImageFileUrl(const HostProfileInfo & profile) const;
ISciterUI & m_sciterUI;
SystemConfig & m_config;
SystemModules & m_modules;
ISciterWindow & m_window;
SciterElement m_page;
SciterElement m_profilesPage;
std::shared_ptr<IPageNav> m_pageNav;
bool m_enabled;
int32_t m_currentUser;
};

View File

@ -1,8 +1,8 @@
#include "rom_browser.h"
#include "user_interface/html_utils.h"
#include "user_interface/sciter_main_window.h"
#include "settings/ui_settings.h"
#include <map>
#include <common/base64.h>
#include <common/path.h>
#include <common/std_string.h>
#include <nxemu-module-spec/system_loader.h>
@ -213,7 +213,7 @@ bool WidgetRomBrowser::RenderUI()
SciterElement iconElement;
iconElement.Create("img", "");
iconElement.SetAttribute("class", "rom-icon");
iconElement.SetAttribute("src", stdstr_f("data:image/jpeg;base64,%s", base64_encode(romEntry.icon.data(), romEntry.icon.size()).c_str()).c_str());
iconElement.SetAttribute("src", ImageDataUri(romEntry.icon.data(), romEntry.icon.size()).c_str());
romCardContents.Insert(iconElement, romCardContents.GetChildCount());
romEntry.icon.clear();
}