Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<script defer="defer" src="mods/better_database.js"></script>
<script defer="defer" src="mods/track_backup_manager.js"></script>
<script defer="defer" src="mods/kodub_mod_config.js"></script>
<script defer="defer" src="mods/dynamic_theme.js"></script>
<script defer="defer" src="mods/proximity_chat.js"></script>
<script defer="defer" src="mods/better_track_codes.js"></script>
<script defer="defer" src="mods/rank_badges.js"></script>
Expand Down
98 changes: 98 additions & 0 deletions mods/dynamic_theme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
(() => {
const THEME_HUE_KEY = "cwctrackThemeHue";
const THEME_HUE_VAR = "--cwctrack-theme-hue";
const THEME_STYLE_ID = "cwctrack-dynamic-theme-style";
const THEME_SETTING_CLASS = "cwctrack-theme-setting";
const DEFAULT_HUE = 0;
const MIN_HUE = 0;
const MAX_HUE = 360;

const clampHue = value => {
if (!Number.isFinite(value)) return DEFAULT_HUE;
return Math.min(MAX_HUE, Math.max(MIN_HUE, Math.round(value)));
};

const readStoredHue = () => {
try {
return clampHue(Number(localStorage.getItem(THEME_HUE_KEY)));
} catch (_error) {
return DEFAULT_HUE;
}
};

const writeStoredHue = hue => {
try {
localStorage.setItem(THEME_HUE_KEY, String(hue));
} catch (_error) {}
};

const applyHue = hue => {
document.documentElement.style.setProperty(THEME_HUE_VAR, `${hue}deg`);
};

const ensureThemeStyle = () => {
if (document.getElementById(THEME_STYLE_ID)) return;

const style = document.createElement("style");
style.id = THEME_STYLE_ID;
style.textContent = `
:root { ${THEME_HUE_VAR}: 0deg; }
#screen, #ui, #transition-layer { filter: hue-rotate(var(${THEME_HUE_VAR})); }
.${THEME_SETTING_CLASS} > input[type="range"] { width: 220px; }
`;
document.head.append(style);
};

const createSetting = container => {
if (!container || container.querySelector(`.${THEME_SETTING_CLASS}`)) return;

const setting = document.createElement("div");
setting.className = `setting ${THEME_SETTING_CLASS}`;

const label = document.createElement("p");
label.textContent = "Theme hue";

const slider = document.createElement("input");
slider.type = "range";
slider.min = String(MIN_HUE);
slider.max = String(MAX_HUE);
slider.step = "1";
slider.value = String(readStoredHue());

slider.addEventListener("input", () => {
const hue = clampHue(Number(slider.value));
applyHue(hue);
writeStoredHue(hue);
});

setting.append(label, slider);
container.append(setting);
};

const run = () => {
ensureThemeStyle();
applyHue(readStoredHue());

let checkQueued = false;
const checkForSettings = () => {
checkQueued = false;
createSetting(document.querySelector(".settings-menu-ui > .container"));
};
const queueSettingsCheck = () => {
if (checkQueued) return;
checkQueued = true;
setTimeout(checkForSettings, 100);
};

const root = document.getElementById("ui") || document.body;
const observer = new MutationObserver(queueSettingsCheck);
observer.observe(root, { childList: true, subtree: true });
queueSettingsCheck();
};

if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", run, { once: true });
} else {
run();
}
})();
17 changes: 17 additions & 0 deletions tests/main-menu.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,23 @@ test('test settings entry button', async ({ page }) => {
await expect(page.locator('.settings-menu-ui')).toBeVisible({ timeout: 20_000 });
});

test('dynamic theme hue slider updates css variable', async ({ page }) => {
await page.getByRole('button', { name: 'Settings' }).click();
const slider = page.locator('.cwctrack-theme-setting input[type="range"]');
await expect(slider).toBeVisible({ timeout: 20_000 });

await slider.evaluate((element, value) => {
element.value = value;
element.dispatchEvent(new Event('input', { bubbles: true }));
}, '120');

await expect.poll(async () => {
return await page.evaluate(() => {
return document.documentElement.style.getPropertyValue('--cwctrack-theme-hue').trim();
});
}).toBe('120deg');
});

test('test multiplayer entry button', async ({ page }) => {
await page.getByRole('button', { name: 'Multiplayer' }).click();
await expect(page.locator('.multiplayer-ui > .join')).toBeVisible({ timeout: 20_000 });
Expand Down