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:
parent
661a8a2dd8
commit
5e35c3305d
|
|
@ -1 +1 @@
|
|||
Subproject commit db5a4d1840cd4fdda67cc8ef3f37597be26190ab
|
||||
Subproject commit c2cdf8fa3a4c5807a6178a88799844e307af1101
|
||||
|
|
@ -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%%;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 += "&"; break;
|
||||
case '<': out += "<"; break;
|
||||
case '>': out += ">"; break;
|
||||
case '"': out += """; break;
|
||||
case '\'': out += "'"; 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");
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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 += "&";
|
||||
break;
|
||||
case '<':
|
||||
out += "<";
|
||||
break;
|
||||
case '>':
|
||||
out += ">";
|
||||
break;
|
||||
case '"':
|
||||
out += """;
|
||||
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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue