Add dynamic UI theme for server browser
Introduce runtime-driven UI theming for the server browser - Expose PushUITheme() in F4TServerBrowserBridge and implement ColorToJson + PushUIThemeInternal to serialize HUD colors and invoke a JS hook. - Call PushUITheme on UI initialization and when the browser is shown so the webview receives current HUD colors. - Add frontend handler for "uiTheme" in app.js and a new components/theme.js that implements window.applyCoTheme to set CSS vars and tint the cursor SVG. - Update styles.css to use RGB component CSS variables (--co-r/--co-g/--co-b and background equivalents) so colors can be overridden at runtime. - Update index.html to include the new theme script and tweak cursor SVG stroke width; cursor asset updated as well. This enables the plugin to synchronize the in-game HUD colors with the browser UI dynamically.
This commit is contained in:
@@ -15,5 +15,6 @@ namespace F4T::ServerBrowser
|
|||||||
static void HandleAction(const nlohmann::json& a_action);
|
static void HandleAction(const nlohmann::json& a_action);
|
||||||
static void ForwardControllerInput(const char* a_button, const char* a_phase);
|
static void ForwardControllerInput(const char* a_button, const char* a_phase);
|
||||||
static void PushConnectionStatus();
|
static void PushConnectionStatus();
|
||||||
|
static void PushUITheme();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ namespace F4T::PrismaUI
|
|||||||
});
|
});
|
||||||
|
|
||||||
F4T::ServerBrowser::ServerBrowserBridge::Initialize(g_api, a_view);
|
F4T::ServerBrowser::ServerBrowserBridge::Initialize(g_api, a_view);
|
||||||
|
F4T::ServerBrowser::ServerBrowserBridge::PushUITheme();
|
||||||
|
|
||||||
g_api->BindUIEvent(a_view, "coAction", [](const char* a_json) {
|
g_api->BindUIEvent(a_view, "coAction", [](const char* a_json) {
|
||||||
if (!a_json) {
|
if (!a_json) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "F4TServerBrowserBridge.h"
|
#include "F4TServerBrowserBridge.h"
|
||||||
#include "F4TServerBrowserData.h"
|
#include "F4TServerBrowserData.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <format>
|
#include <format>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
@@ -58,6 +59,38 @@ namespace F4T::ServerBrowser
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nlohmann::json ColorToJson(const RE::NiColor& a_color)
|
||||||
|
{
|
||||||
|
const auto toByte = [](float a_channel) {
|
||||||
|
return static_cast<int>(std::clamp(a_channel * 255.0f, 0.0f, 255.0f));
|
||||||
|
};
|
||||||
|
|
||||||
|
return nlohmann::json{
|
||||||
|
{ "r", toByte(a_color.r) },
|
||||||
|
{ "g", toByte(a_color.g) },
|
||||||
|
{ "b", toByte(a_color.b) }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void PushUIThemeInternal()
|
||||||
|
{
|
||||||
|
if (!g_api || g_view == 0 || !g_api->IsValid(g_view)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto hudColor = RE::HUDMenuUtils::GetGameplayHUDColor();
|
||||||
|
const auto hudBgColor = RE::HUDMenuUtils::GetGameplayHUDBackgroundColor();
|
||||||
|
const auto themePayload = nlohmann::json{
|
||||||
|
{ "hud", ColorToJson(hudColor) },
|
||||||
|
{ "hudBackground", ColorToJson(hudBgColor) }
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto script = std::format(
|
||||||
|
"if(window.applyCoTheme){{window.applyCoTheme({});}}",
|
||||||
|
themePayload.dump());
|
||||||
|
g_api->Invoke(g_view, script.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
void PushConnectionStatusEvent()
|
void PushConnectionStatusEvent()
|
||||||
{
|
{
|
||||||
const auto connected = F4T::Networking::IsConnectedToServer();
|
const auto connected = F4T::Networking::IsConnectedToServer();
|
||||||
@@ -136,6 +169,7 @@ namespace F4T::ServerBrowser
|
|||||||
|
|
||||||
void ServerBrowserBridge::OnBrowserShown()
|
void ServerBrowserBridge::OnBrowserShown()
|
||||||
{
|
{
|
||||||
|
PushUIThemeInternal();
|
||||||
DispatchEvent(nlohmann::json{ { "type", "menuOpened" } });
|
DispatchEvent(nlohmann::json{ { "type", "menuOpened" } });
|
||||||
RefreshServerList();
|
RefreshServerList();
|
||||||
}
|
}
|
||||||
@@ -289,4 +323,9 @@ namespace F4T::ServerBrowser
|
|||||||
{
|
{
|
||||||
PushConnectionStatusEvent();
|
PushConnectionStatusEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ServerBrowserBridge::PushUITheme()
|
||||||
|
{
|
||||||
|
PushUIThemeInternal();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -482,6 +482,11 @@
|
|||||||
case "connectionStatusChanged":
|
case "connectionStatusChanged":
|
||||||
state.connectionStatus = event.status || "Online";
|
state.connectionStatus = event.status || "Online";
|
||||||
break;
|
break;
|
||||||
|
case "uiTheme":
|
||||||
|
if (typeof window.applyCoTheme === "function") {
|
||||||
|
window.applyCoTheme(event);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case "menuOpened":
|
case "menuOpened":
|
||||||
break;
|
break;
|
||||||
case "menuClosed":
|
case "menuClosed":
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 911 B |
@@ -6,6 +6,7 @@
|
|||||||
.st0 {
|
.st0 {
|
||||||
fill: #fff;
|
fill: #fff;
|
||||||
stroke: #000;
|
stroke: #000;
|
||||||
|
stroke-width: 3.2px;
|
||||||
stroke-miterlimit: 10;
|
stroke-miterlimit: 10;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 479 B |
@@ -0,0 +1,55 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function clampByte(value) {
|
||||||
|
return Math.max(0, Math.min(255, Math.round(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyHudRgb(root, prefix, color) {
|
||||||
|
if (!color) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.style.setProperty(prefix + "-r", String(clampByte(color.r)));
|
||||||
|
root.style.setProperty(prefix + "-g", String(clampByte(color.g)));
|
||||||
|
root.style.setProperty(prefix + "-b", String(clampByte(color.b)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCursorTint(root, color) {
|
||||||
|
if (!color) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var r = clampByte(color.r);
|
||||||
|
var g = clampByte(color.g);
|
||||||
|
var b = clampByte(color.b);
|
||||||
|
var svg =
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32.3 31.24">' +
|
||||||
|
'<polygon fill="rgb(' + r + "," + g + "," + b + ')" stroke="#000" stroke-width="3.2" ' +
|
||||||
|
'stroke-miterlimit="10" points="17.4 30.41 31.46 15.9 1.2 1.14 17.4 30.41"/></svg>';
|
||||||
|
var url =
|
||||||
|
'url("data:image/svg+xml,' +
|
||||||
|
encodeURIComponent(svg) +
|
||||||
|
'") 1 1, default';
|
||||||
|
root.style.setProperty("--co-cursor", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.applyCoTheme = function (theme) {
|
||||||
|
if (!theme) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var root = document.documentElement;
|
||||||
|
var hud = theme.hud || theme;
|
||||||
|
applyHudRgb(root, "--co", hud);
|
||||||
|
applyHudRgb(root, "--co-bg", theme.hudBackground);
|
||||||
|
applyCursorTint(root, hud);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[CO] Applied HUD theme rgb(" +
|
||||||
|
clampByte(hud.r) + "," +
|
||||||
|
clampByte(hud.g) + "," +
|
||||||
|
clampByte(hud.b) + ")"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -79,6 +79,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="./components/theme.js"></script>
|
||||||
<script src="./components/mock-data.js"></script>
|
<script src="./components/mock-data.js"></script>
|
||||||
<script src="./components/tabs.js"></script>
|
<script src="./components/tabs.js"></script>
|
||||||
<script src="./components/filters.js"></script>
|
<script src="./components/filters.js"></script>
|
||||||
|
|||||||
@@ -7,17 +7,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--co-green: #2dff28;
|
/* RGB components — overridden at runtime from Fallout4Prefs.ini via plugin. */
|
||||||
--co-green-soft: #2dff28;
|
--co-r: 18;
|
||||||
--co-green-dim: rgba(45, 255, 40, 0.22);
|
--co-g: 255;
|
||||||
--co-bg: rgba(0, 12, 0, 0.62);
|
--co-b: 21;
|
||||||
--co-bg-dark: rgba(0, 10, 0, 0.72);
|
--co-bg-r: 0;
|
||||||
|
--co-bg-g: 10;
|
||||||
|
--co-bg-b: 0;
|
||||||
|
--co-green: rgb(var(--co-r), var(--co-g), var(--co-b));
|
||||||
|
--co-green-soft: rgb(var(--co-r), var(--co-g), var(--co-b));
|
||||||
|
--co-green-dim: rgba(var(--co-r), var(--co-g), var(--co-b), 0.22);
|
||||||
|
--co-bg: rgba(var(--co-bg-r), var(--co-bg-g), var(--co-bg-b), 0.62);
|
||||||
|
--co-bg-dark: rgba(var(--co-bg-r), var(--co-bg-g), var(--co-bg-b), 0.72);
|
||||||
--co-bg-list: transparent;
|
--co-bg-list: transparent;
|
||||||
--co-selected: #2dff28;
|
--co-selected: rgb(var(--co-r), var(--co-g), var(--co-b));
|
||||||
--co-selected-text: #001400;
|
--co-selected-text: rgb(
|
||||||
--co-border: #2dff28;
|
calc(var(--co-r) * 0.05),
|
||||||
--co-text-dim: rgba(45, 255, 40, 0.5);
|
calc(var(--co-g) * 0.05),
|
||||||
--co-ping-good: #2dff28;
|
calc(var(--co-b) * 0.05)
|
||||||
|
);
|
||||||
|
--co-border: rgb(var(--co-r), var(--co-g), var(--co-b));
|
||||||
|
--co-text-dim: rgba(var(--co-r), var(--co-g), var(--co-b), 0.5);
|
||||||
|
--co-ping-good: rgb(var(--co-r), var(--co-g), var(--co-b));
|
||||||
--co-ping-mid: #c8ff28;
|
--co-ping-mid: #c8ff28;
|
||||||
--co-ping-bad: #ffd428;
|
--co-ping-bad: #ffd428;
|
||||||
--co-danger: #ff6a6a;
|
--co-danger: #ff6a6a;
|
||||||
@@ -162,7 +173,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.co-nav-item:hover:not(.active) {
|
.co-nav-item:hover:not(.active) {
|
||||||
background: rgba(45, 255, 40, 0.08);
|
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.co-nav-item.active {
|
.co-nav-item.active {
|
||||||
@@ -171,11 +182,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.co-nav-item.focused:not(.active) {
|
.co-nav-item.focused:not(.active) {
|
||||||
background: rgba(45, 255, 40, 0.12);
|
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.co-nav-list.focused .co-nav-item.focused:not(.active) {
|
.co-nav-list.focused .co-nav-item.focused:not(.active) {
|
||||||
background: rgba(45, 255, 40, 0.12);
|
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Right content */
|
/* Right content */
|
||||||
@@ -418,11 +429,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.co-btn:hover {
|
.co-btn:hover {
|
||||||
background: rgba(45, 255, 40, 0.1);
|
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.co-btn.focused {
|
.co-btn.focused {
|
||||||
background: rgba(45, 255, 40, 0.18);
|
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.18);
|
||||||
outline: 1px solid var(--co-green);
|
outline: 1px solid var(--co-green);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,7 +447,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.co-server-list-wrap.focused {
|
.co-server-list-wrap.focused {
|
||||||
background: rgba(45, 255, 40, 0.03);
|
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
.co-server-list {
|
.co-server-list {
|
||||||
@@ -468,7 +479,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.co-server-row:hover:not(.selected) {
|
.co-server-row:hover:not(.selected) {
|
||||||
background: rgba(45, 255, 40, 0.06);
|
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.co-server-row.selected {
|
.co-server-row.selected {
|
||||||
|
|||||||
Reference in New Issue
Block a user