add tema
This commit is contained in:
@@ -36,6 +36,14 @@
|
|||||||
<p id="pageSubtitle">RC race timing with AMMC integration</p>
|
<p id="pageSubtitle">RC race timing with AMMC integration</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="topbar-right">
|
<div class="topbar-right">
|
||||||
|
<label class="lang-wrap theme-wrap">
|
||||||
|
<span id="themeLabel">Theme</span>
|
||||||
|
<select id="themeSelect" class="lang-select theme-select">
|
||||||
|
<option value="dark">Dark</option>
|
||||||
|
<option value="nord">Nord</option>
|
||||||
|
<option value="light">Light</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label class="lang-wrap">
|
<label class="lang-wrap">
|
||||||
<span id="languageLabel">Language</span>
|
<span id="languageLabel">Language</span>
|
||||||
<select id="languageSelect" class="lang-select">
|
<select id="languageSelect" class="lang-select">
|
||||||
|
|||||||
49
src/app.js
49
src/app.js
@@ -15,6 +15,8 @@ const NAV_ITEMS = [
|
|||||||
const SESSION_TYPES = ["open_practice", "free_practice", "practice", "qualification", "heat", "final", "team_race"];
|
const SESSION_TYPES = ["open_practice", "free_practice", "practice", "qualification", "heat", "final", "team_race"];
|
||||||
const STORAGE_KEY = "rc_timing_control_v1";
|
const STORAGE_KEY = "rc_timing_control_v1";
|
||||||
const DEFAULT_LANGUAGE = "sv";
|
const DEFAULT_LANGUAGE = "sv";
|
||||||
|
const DEFAULT_THEME = "dark";
|
||||||
|
const AVAILABLE_THEMES = ["dark", "nord", "light"];
|
||||||
|
|
||||||
const TRANSLATIONS = {
|
const TRANSLATIONS = {
|
||||||
sv: {
|
sv: {
|
||||||
@@ -41,6 +43,10 @@ const TRANSLATIONS = {
|
|||||||
"nav.guide": "Guide",
|
"nav.guide": "Guide",
|
||||||
"nav.guide_sub": "Dokumentation och uppstart",
|
"nav.guide_sub": "Dokumentation och uppstart",
|
||||||
"ui.language": "Språk",
|
"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.title": "JMK RB RaceController",
|
||||||
"brand.subtitle": "RC Timing System",
|
"brand.subtitle": "RC Timing System",
|
||||||
"ui.no_active_session": "Ingen aktiv session",
|
"ui.no_active_session": "Ingen aktiv session",
|
||||||
@@ -775,6 +781,10 @@ const TRANSLATIONS = {
|
|||||||
"nav.guide": "Guide",
|
"nav.guide": "Guide",
|
||||||
"nav.guide_sub": "Documentation and setup",
|
"nav.guide_sub": "Documentation and setup",
|
||||||
"ui.language": "Language",
|
"ui.language": "Language",
|
||||||
|
"ui.theme": "Theme",
|
||||||
|
"ui.theme_dark": "Dark",
|
||||||
|
"ui.theme_nord": "Nord",
|
||||||
|
"ui.theme_light": "Light",
|
||||||
"brand.title": "JMK RB RaceController",
|
"brand.title": "JMK RB RaceController",
|
||||||
"brand.subtitle": "RC Timing System",
|
"brand.subtitle": "RC Timing System",
|
||||||
"ui.no_active_session": "No Active Session",
|
"ui.no_active_session": "No Active Session",
|
||||||
@@ -1554,7 +1564,9 @@ init();
|
|||||||
async function init() {
|
async function init() {
|
||||||
document.body.classList.toggle("overlay-mode", overlayMode);
|
document.body.classList.toggle("overlay-mode", overlayMode);
|
||||||
seedDefaultData();
|
seedDefaultData();
|
||||||
|
applyTheme();
|
||||||
await hydrateFromBackend();
|
await hydrateFromBackend();
|
||||||
|
applyTheme();
|
||||||
await loadAmmcConfigFromBackend();
|
await loadAmmcConfigFromBackend();
|
||||||
renderNav();
|
renderNav();
|
||||||
renderView();
|
renderView();
|
||||||
@@ -1594,6 +1606,10 @@ function seedDefaultData() {
|
|||||||
state.settings.language = DEFAULT_LANGUAGE;
|
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") {
|
if (typeof state.settings.audioEnabled !== "boolean") {
|
||||||
state.settings.audioEnabled = true;
|
state.settings.audioEnabled = true;
|
||||||
}
|
}
|
||||||
@@ -1678,6 +1694,7 @@ function loadState() {
|
|||||||
wsUrl: parsed.settings?.wsUrl || "ws://127.0.0.1:9000",
|
wsUrl: parsed.settings?.wsUrl || "ws://127.0.0.1:9000",
|
||||||
backendUrl: parsed.settings?.backendUrl || getDefaultBackendUrl(),
|
backendUrl: parsed.settings?.backendUrl || getDefaultBackendUrl(),
|
||||||
language: parsed.settings?.language || DEFAULT_LANGUAGE,
|
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,
|
autoReconnect: parsed.settings?.autoReconnect !== false,
|
||||||
audioEnabled: parsed.settings?.audioEnabled !== false,
|
audioEnabled: parsed.settings?.audioEnabled !== false,
|
||||||
passingSoundMode: parsed.settings?.passingSoundMode || "beep",
|
passingSoundMode: parsed.settings?.passingSoundMode || "beep",
|
||||||
@@ -1720,6 +1737,7 @@ function loadState() {
|
|||||||
wsUrl: "ws://127.0.0.1:9000",
|
wsUrl: "ws://127.0.0.1:9000",
|
||||||
backendUrl: getDefaultBackendUrl(),
|
backendUrl: getDefaultBackendUrl(),
|
||||||
language: DEFAULT_LANGUAGE,
|
language: DEFAULT_LANGUAGE,
|
||||||
|
theme: DEFAULT_THEME,
|
||||||
autoReconnect: true,
|
autoReconnect: true,
|
||||||
audioEnabled: true,
|
audioEnabled: true,
|
||||||
passingSoundMode: "beep",
|
passingSoundMode: "beep",
|
||||||
@@ -1761,6 +1779,14 @@ function currentLanguage() {
|
|||||||
return state.settings.language === "en" ? "en" : "sv";
|
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 = {}) {
|
function t(key, vars = {}) {
|
||||||
const lang = currentLanguage();
|
const lang = currentLanguage();
|
||||||
const dict = TRANSLATIONS[lang] || TRANSLATIONS.en;
|
const dict = TRANSLATIONS[lang] || TRANSLATIONS.en;
|
||||||
@@ -1769,6 +1795,23 @@ function t(key, vars = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupLanguageControl() {
|
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");
|
const label = document.getElementById("languageLabel");
|
||||||
if (label) {
|
if (label) {
|
||||||
label.textContent = t("ui.language");
|
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",
|
wsUrl: persisted.settings?.wsUrl || state.settings.wsUrl || "ws://127.0.0.1:9000",
|
||||||
backendUrl: persisted.settings?.backendUrl || state.settings.backendUrl || getDefaultBackendUrl(),
|
backendUrl: persisted.settings?.backendUrl || state.settings.backendUrl || getDefaultBackendUrl(),
|
||||||
language: persisted.settings?.language || state.settings.language || DEFAULT_LANGUAGE,
|
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,
|
autoReconnect: persisted.settings?.autoReconnect !== false,
|
||||||
audioEnabled: persisted.settings?.audioEnabled !== false,
|
audioEnabled: persisted.settings?.audioEnabled !== false,
|
||||||
passingSoundMode: persisted.settings?.passingSoundMode || state.settings.passingSoundMode || "beep",
|
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)
|
? state.settings.racePresets.map((preset) => normalizeStoredRacePreset(preset)).filter((preset) => preset.name)
|
||||||
: [],
|
: [],
|
||||||
};
|
};
|
||||||
|
applyTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeSession(session) {
|
function normalizeSession(session) {
|
||||||
@@ -2405,6 +2450,10 @@ function renderView() {
|
|||||||
if (languageLabel) {
|
if (languageLabel) {
|
||||||
languageLabel.textContent = t("ui.language");
|
languageLabel.textContent = t("ui.language");
|
||||||
}
|
}
|
||||||
|
const themeLabel = document.getElementById("themeLabel");
|
||||||
|
if (themeLabel) {
|
||||||
|
themeLabel.textContent = t("ui.theme");
|
||||||
|
}
|
||||||
|
|
||||||
switch (currentView) {
|
switch (currentView) {
|
||||||
case "dashboard":
|
case "dashboard":
|
||||||
|
|||||||
@@ -13,6 +13,36 @@
|
|||||||
--shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
|
--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;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@@ -22,9 +52,9 @@ body {
|
|||||||
font-family: Barlow, "Segoe UI", sans-serif;
|
font-family: Barlow, "Segoe UI", sans-serif;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 15% 0%, rgba(225, 6, 0, 0.18), transparent 30%),
|
radial-gradient(circle at 15% 0%, color-mix(in srgb, var(--accent) 18%, transparent), transparent 30%),
|
||||||
radial-gradient(circle at 100% 80%, rgba(37, 59, 103, 0.22), transparent 30%),
|
radial-gradient(circle at 100% 80%, color-mix(in srgb, var(--accent-2) 16%, transparent), transparent 30%),
|
||||||
linear-gradient(160deg, #05070b 0%, #090d16 55%, #07090e 100%);
|
linear-gradient(160deg, color-mix(in srgb, var(--bg) 90%, #000 10%) 0%, var(--bg-soft) 55%, var(--bg) 100%);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +208,7 @@ body {
|
|||||||
.topbar-right {
|
.topbar-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lang-wrap {
|
.lang-wrap {
|
||||||
@@ -195,6 +225,14 @@ body {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.theme-wrap {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-select {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
.view {
|
.view {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user