This commit is contained in:
larssand
2026-03-20 16:57:16 +01:00
parent 1de654a220
commit ee8db954bc
3 changed files with 99 additions and 4 deletions

View File

@@ -15,6 +15,8 @@ const NAV_ITEMS = [
const SESSION_TYPES = ["open_practice", "free_practice", "practice", "qualification", "heat", "final", "team_race"];
const STORAGE_KEY = "rc_timing_control_v1";
const DEFAULT_LANGUAGE = "sv";
const DEFAULT_THEME = "dark";
const AVAILABLE_THEMES = ["dark", "nord", "light"];
const TRANSLATIONS = {
sv: {
@@ -41,6 +43,10 @@ const TRANSLATIONS = {
"nav.guide": "Guide",
"nav.guide_sub": "Dokumentation och uppstart",
"ui.language": "Språk",
"ui.theme": "Tema",
"ui.theme_dark": "Mörk",
"ui.theme_nord": "Nord",
"ui.theme_light": "Ljus",
"brand.title": "JMK RB RaceController",
"brand.subtitle": "RC Timing System",
"ui.no_active_session": "Ingen aktiv session",
@@ -775,6 +781,10 @@ const TRANSLATIONS = {
"nav.guide": "Guide",
"nav.guide_sub": "Documentation and setup",
"ui.language": "Language",
"ui.theme": "Theme",
"ui.theme_dark": "Dark",
"ui.theme_nord": "Nord",
"ui.theme_light": "Light",
"brand.title": "JMK RB RaceController",
"brand.subtitle": "RC Timing System",
"ui.no_active_session": "No Active Session",
@@ -1554,7 +1564,9 @@ init();
async function init() {
document.body.classList.toggle("overlay-mode", overlayMode);
seedDefaultData();
applyTheme();
await hydrateFromBackend();
applyTheme();
await loadAmmcConfigFromBackend();
renderNav();
renderView();
@@ -1594,6 +1606,10 @@ function seedDefaultData() {
state.settings.language = DEFAULT_LANGUAGE;
}
if (!AVAILABLE_THEMES.includes(String(state.settings.theme || "").toLowerCase())) {
state.settings.theme = DEFAULT_THEME;
}
if (typeof state.settings.audioEnabled !== "boolean") {
state.settings.audioEnabled = true;
}
@@ -1678,6 +1694,7 @@ function loadState() {
wsUrl: parsed.settings?.wsUrl || "ws://127.0.0.1:9000",
backendUrl: parsed.settings?.backendUrl || getDefaultBackendUrl(),
language: parsed.settings?.language || DEFAULT_LANGUAGE,
theme: AVAILABLE_THEMES.includes(String(parsed.settings?.theme || "").toLowerCase()) ? String(parsed.settings.theme).toLowerCase() : DEFAULT_THEME,
autoReconnect: parsed.settings?.autoReconnect !== false,
audioEnabled: parsed.settings?.audioEnabled !== false,
passingSoundMode: parsed.settings?.passingSoundMode || "beep",
@@ -1720,6 +1737,7 @@ function loadState() {
wsUrl: "ws://127.0.0.1:9000",
backendUrl: getDefaultBackendUrl(),
language: DEFAULT_LANGUAGE,
theme: DEFAULT_THEME,
autoReconnect: true,
audioEnabled: true,
passingSoundMode: "beep",
@@ -1761,6 +1779,14 @@ function currentLanguage() {
return state.settings.language === "en" ? "en" : "sv";
}
function currentTheme() {
return AVAILABLE_THEMES.includes(String(state.settings.theme || "").toLowerCase()) ? String(state.settings.theme).toLowerCase() : DEFAULT_THEME;
}
function applyTheme() {
document.body.dataset.theme = currentTheme();
}
function t(key, vars = {}) {
const lang = currentLanguage();
const dict = TRANSLATIONS[lang] || TRANSLATIONS.en;
@@ -1769,6 +1795,23 @@ function t(key, vars = {}) {
}
function setupLanguageControl() {
const themeLabel = document.getElementById("themeLabel");
if (themeLabel) {
themeLabel.textContent = t("ui.theme");
}
const themeSelect = document.getElementById("themeSelect");
if (themeSelect instanceof HTMLSelectElement) {
themeSelect.value = currentTheme();
Array.from(themeSelect.options).forEach((option) => {
option.textContent = t(`ui.theme_${option.value}`);
});
themeSelect.onchange = () => {
state.settings.theme = AVAILABLE_THEMES.includes(themeSelect.value) ? themeSelect.value : DEFAULT_THEME;
applyTheme();
saveState();
};
}
const label = document.getElementById("languageLabel");
if (label) {
label.textContent = t("ui.language");
@@ -1966,6 +2009,7 @@ function applyPersistedState(persisted) {
wsUrl: persisted.settings?.wsUrl || state.settings.wsUrl || "ws://127.0.0.1:9000",
backendUrl: persisted.settings?.backendUrl || state.settings.backendUrl || getDefaultBackendUrl(),
language: persisted.settings?.language || state.settings.language || DEFAULT_LANGUAGE,
theme: AVAILABLE_THEMES.includes(String(persisted.settings?.theme || state.settings.theme || DEFAULT_THEME).toLowerCase()) ? String(persisted.settings?.theme || state.settings.theme || DEFAULT_THEME).toLowerCase() : DEFAULT_THEME,
autoReconnect: persisted.settings?.autoReconnect !== false,
audioEnabled: persisted.settings?.audioEnabled !== false,
passingSoundMode: persisted.settings?.passingSoundMode || state.settings.passingSoundMode || "beep",
@@ -1987,6 +2031,7 @@ function applyPersistedState(persisted) {
? state.settings.racePresets.map((preset) => normalizeStoredRacePreset(preset)).filter((preset) => preset.name)
: [],
};
applyTheme();
}
function normalizeSession(session) {
@@ -2405,6 +2450,10 @@ function renderView() {
if (languageLabel) {
languageLabel.textContent = t("ui.language");
}
const themeLabel = document.getElementById("themeLabel");
if (themeLabel) {
themeLabel.textContent = t("ui.theme");
}
switch (currentView) {
case "dashboard":

View File

@@ -13,6 +13,36 @@
--shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
}
body[data-theme="nord"] {
--bg: #0b1220;
--bg-soft: #131d31;
--panel: #182235;
--panel-2: #10192b;
--line: #31425f;
--text: #edf3ff;
--muted: #a7b8d8;
--accent: #5e81ac;
--accent-2: #88c0d0;
--ok: #79c9a4;
--warn: #f0c674;
--shadow: 0 12px 32px rgba(4, 10, 18, 0.42);
}
body[data-theme="light"] {
--bg: #eef2f8;
--bg-soft: #f7f9fc;
--panel: #ffffff;
--panel-2: #ecf1f7;
--line: #cfd8e6;
--text: #152033;
--muted: #5f708d;
--accent: #d62828;
--accent-2: #ef4444;
--ok: #1f9d67;
--warn: #c98914;
--shadow: 0 10px 28px rgba(27, 45, 78, 0.12);
}
* {
box-sizing: border-box;
}
@@ -22,9 +52,9 @@ body {
font-family: Barlow, "Segoe UI", sans-serif;
color: var(--text);
background:
radial-gradient(circle at 15% 0%, rgba(225, 6, 0, 0.18), transparent 30%),
radial-gradient(circle at 100% 80%, rgba(37, 59, 103, 0.22), transparent 30%),
linear-gradient(160deg, #05070b 0%, #090d16 55%, #07090e 100%);
radial-gradient(circle at 15% 0%, color-mix(in srgb, var(--accent) 18%, transparent), transparent 30%),
radial-gradient(circle at 100% 80%, color-mix(in srgb, var(--accent-2) 16%, transparent), transparent 30%),
linear-gradient(160deg, color-mix(in srgb, var(--bg) 90%, #000 10%) 0%, var(--bg-soft) 55%, var(--bg) 100%);
min-height: 100vh;
}
@@ -178,7 +208,7 @@ body {
.topbar-right {
display: flex;
align-items: center;
gap: 8px;
gap: 10px;
}
.lang-wrap {
@@ -195,6 +225,14 @@ body {
border-radius: 8px;
}
.theme-wrap {
min-width: 120px;
}
.theme-select {
min-width: 120px;
}
.view {
margin-top: 16px;
}