Files
pi-kit/pikit-web/assets/settings.js
2025-12-10 18:51:31 -05:00

131 lines
4.2 KiB
JavaScript

// Handles user-facing settings (theme, motion, refresh cadence) and persistence
// across reloads. Keeps side effects isolated so main.js simply wires callbacks.
const DEFAULT_REFRESH_SEC = 10;
const MIN_REFRESH_SEC = 5;
const MAX_REFRESH_SEC = 120;
const THEME_KEY = "pikit-theme";
const ANIM_KEY = "pikit-anim";
const REFRESH_KEY = "pikit-refresh-sec";
export function initSettings({
refreshHintMain,
refreshHintServices,
refreshFlagTop,
refreshIntervalInput,
refreshIntervalSave,
refreshIntervalMsg,
themeToggle,
themeToggleIcon,
animToggle,
onTick,
toast = null,
defaultIntervalSec = DEFAULT_REFRESH_SEC,
}) {
let refreshIntervalMs = defaultIntervalSec * 1000;
let refreshTimer = null;
const prefersReduce =
typeof window !== "undefined" &&
window.matchMedia &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
function updateRefreshHints(seconds) {
const text = `${seconds} second${seconds === 1 ? "" : "s"}`;
if (refreshHintMain) refreshHintMain.textContent = text;
if (refreshHintServices) refreshHintServices.textContent = text;
if (refreshFlagTop) refreshFlagTop.textContent = `Refresh: ${seconds}s`;
if (refreshIntervalInput) refreshIntervalInput.value = seconds;
}
function setRefreshInterval(seconds, { silent = false } = {}) {
const sec = Math.max(
MIN_REFRESH_SEC,
Math.min(MAX_REFRESH_SEC, Math.floor(seconds)),
);
// Clamp to safe bounds; store milliseconds for setInterval
refreshIntervalMs = sec * 1000;
if (refreshTimer) clearInterval(refreshTimer);
if (onTick) {
refreshTimer = setInterval(onTick, refreshIntervalMs);
}
updateRefreshHints(sec);
try {
localStorage.setItem(REFRESH_KEY, String(sec));
} catch (e) {
console.warn("Refresh persistence unavailable", e);
}
if (!silent && refreshIntervalMsg) {
refreshIntervalMsg.textContent = "";
}
}
refreshIntervalSave?.addEventListener("click", () => {
if (!refreshIntervalInput) return;
const sec = Number(refreshIntervalInput.value);
if (Number.isNaN(sec)) {
if (refreshIntervalMsg) refreshIntervalMsg.textContent = "";
toast?.("Enter seconds.", "error");
return;
}
setRefreshInterval(sec);
onTick?.(); // immediate refresh on change
if (refreshIntervalMsg) refreshIntervalMsg.textContent = "";
toast?.("Refresh interval updated", "success");
});
function applyTheme(mode) {
const theme = mode === "light" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", theme);
if (themeToggleIcon)
themeToggleIcon.textContent = theme === "light" ? "☀️" : "🌙";
try {
localStorage.setItem(THEME_KEY, theme);
} catch (e) {
console.warn("Theme persistence unavailable", e);
}
}
themeToggle?.addEventListener("click", () => {
const current =
document.documentElement.getAttribute("data-theme") || "dark";
applyTheme(current === "dark" ? "light" : "dark");
});
function applyAnim(enabled) {
const on = enabled !== false; // default true
// Using a data attribute lets CSS fully disable motion when off
document.documentElement.setAttribute("data-anim", on ? "on" : "off");
if (animToggle) animToggle.checked = on;
try {
localStorage.setItem(ANIM_KEY, on ? "on" : "off");
} catch (e) {
console.warn("Anim persistence unavailable", e);
}
}
animToggle?.addEventListener("change", () => {
applyAnim(animToggle.checked);
});
// Initialize defaults
let storedRefresh = defaultIntervalSec;
try {
const saved = localStorage.getItem(REFRESH_KEY);
if (saved) {
const n = Number(saved);
if (!Number.isNaN(n)) storedRefresh = n;
}
} catch (e) {
console.warn("Refresh persistence unavailable", e);
}
updateRefreshHints(storedRefresh);
setRefreshInterval(storedRefresh, { silent: true });
applyTheme(localStorage.getItem(THEME_KEY) || "dark");
const storedAnim = localStorage.getItem(ANIM_KEY);
const animDefault =
storedAnim === "on" || storedAnim === "off"
? storedAnim === "on"
: !prefersReduce; // respect system reduce-motion if no user choice
applyAnim(animDefault);
return { setRefreshInterval, applyTheme };
}