translations and pickers
This commit is contained in:
+5
-3
@@ -8,7 +8,8 @@
|
||||
*/
|
||||
|
||||
import { logDebug, logError, logWarn } from "@rustpad/logger";
|
||||
import { formatNumber, setLanguage, translateApiMessage } from "@rustpad/i18n";
|
||||
import { formatNumber, restoreGuestLanguage, translateApiMessage } from "@rustpad/i18n";
|
||||
import { restoreGuestTheme } from "@rustpad/theme";
|
||||
|
||||
const DEFAULT_ERRORS = {
|
||||
400: "Invalid request.",
|
||||
@@ -70,10 +71,11 @@ function clearExpiredSession() {
|
||||
sessionStorage.removeItem("rustpad:auth-token");
|
||||
localStorage.removeItem("rustpad:nickname");
|
||||
sessionStorage.removeItem("rustpad:nickname");
|
||||
localStorage.removeItem("rustpad:language");
|
||||
document.cookie = "rustpad_nickname=; Path=/; SameSite=Lax; Max-Age=0";
|
||||
void setLanguage("en", { persist: false });
|
||||
restoreGuestTheme();
|
||||
void restoreGuestLanguage();
|
||||
window.dispatchEvent(new CustomEvent("rustpad:session-expired"));
|
||||
window.dispatchEvent(new CustomEvent("rustpad:session-change", { detail: { session: null } }));
|
||||
}
|
||||
|
||||
async function clearSessionIfInvalid() {
|
||||
|
||||
+8
-6
@@ -17,7 +17,7 @@ import { api } from "@rustpad/api";
|
||||
import { copyText } from "@rustpad/clipboard";
|
||||
import { safeAppUrl } from "@rustpad/security";
|
||||
import { toast } from "@rustpad/toast";
|
||||
import { formatDateTime, populateLanguageSelect, setLanguage, t } from "@rustpad/i18n";
|
||||
import { formatDateTime, getLanguage, populateLanguageSelect, setLanguage, t } from "@rustpad/i18n";
|
||||
|
||||
function slugify(value, fallback) {
|
||||
return value.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || fallback;
|
||||
@@ -423,8 +423,9 @@ function renderAccount(session) {
|
||||
: "";
|
||||
}
|
||||
|
||||
userLabelPrimary.textContent = `Signed as ${primaryIdentity}`;
|
||||
userLabelPrimary.title = `Signed as ${primaryIdentity}`;
|
||||
const signedAs = t("profile.signedAs", { identity: primaryIdentity }, `Signed as ${primaryIdentity}`);
|
||||
userLabelPrimary.textContent = signedAs;
|
||||
userLabelPrimary.title = signedAs;
|
||||
userLabelSecondary.textContent = secondaryIdentity;
|
||||
userLabelSecondary.title = secondaryIdentity;
|
||||
userLabelSecondary.hidden = !secondaryIdentity;
|
||||
@@ -461,9 +462,9 @@ if (identityDialog) {
|
||||
document.querySelector("#profile-nickname").value = currentSession?.nickname || "";
|
||||
const profileColor = document.querySelector("#profile-color");
|
||||
profileColor.value = currentSession?.editor_color || "#7c6cff";
|
||||
const selectedTheme = currentSession?.theme || getTheme();
|
||||
const selectedTheme = getTheme();
|
||||
profileForm.querySelectorAll(`input[name="profile-theme"]`).forEach(input => { input.checked = input.value === selectedTheme; });
|
||||
populateLanguageSelect(document.querySelector("#profile-language"), currentSession?.language || "en");
|
||||
populateLanguageSelect(document.querySelector("#profile-language"), getLanguage());
|
||||
document.querySelector("#profile-current-email").value = currentSession?.email || "";
|
||||
document.querySelector("#profile-email").value = "";
|
||||
document.querySelector("#profile-new-password").value = "";
|
||||
@@ -486,8 +487,9 @@ if (identityDialog) {
|
||||
profileDialog.showModal();
|
||||
});
|
||||
document.addEventListener("rustpad:languagechange", () => {
|
||||
renderAccount(currentSession);
|
||||
if (!profileDialog?.open) return;
|
||||
populateLanguageSelect(document.querySelector("#profile-language"), currentSession?.language || "en");
|
||||
populateLanguageSelect(document.querySelector("#profile-language"), getLanguage());
|
||||
});
|
||||
document.querySelector("#close-profile")?.addEventListener("click", () => profileDialog.close());
|
||||
profileDialog?.addEventListener("click", event => { if (event.target === profileDialog) profileDialog.close(); });
|
||||
|
||||
+47
-11
@@ -7,6 +7,8 @@
|
||||
*/
|
||||
|
||||
const STORAGE_KEY = "rustpad:language";
|
||||
const GUEST_STORAGE_KEY = "rustpad:guest-language";
|
||||
const AUTH_STATE_KEY = "rustpad:auth-state";
|
||||
const FALLBACK_LANGUAGE = "en";
|
||||
const TRANSLATABLE_ATTRIBUTES = ["placeholder", "title", "aria-label", "aria-description"];
|
||||
const SKIP_SELECTOR = [
|
||||
@@ -285,21 +287,50 @@ async function loadBundle(code) {
|
||||
return bundle;
|
||||
}
|
||||
|
||||
function hasAccountSession() {
|
||||
try { return localStorage.getItem(AUTH_STATE_KEY) === "1"; } catch { return false; }
|
||||
}
|
||||
|
||||
function preferredCode() {
|
||||
// English is always the default. A cached language is only used while an
|
||||
// authenticated profile is expected; the server session remains the source
|
||||
// of truth and will re-apply its saved preference after validation.
|
||||
let hasAccountSession = false;
|
||||
let saved = "";
|
||||
try {
|
||||
hasAccountSession = localStorage.getItem("rustpad:auth-state") === "1";
|
||||
if (hasAccountSession) saved = localStorage.getItem(STORAGE_KEY) || "";
|
||||
saved = hasAccountSession()
|
||||
? (localStorage.getItem(STORAGE_KEY) || "")
|
||||
: (localStorage.getItem(GUEST_STORAGE_KEY) || localStorage.getItem(STORAGE_KEY) || "");
|
||||
} catch { /* storage may be blocked */ }
|
||||
if (!hasAccountSession) return FALLBACK_LANGUAGE;
|
||||
const available = new Set(catalog.map(item => item.code));
|
||||
return available.has(saved) ? saved : FALLBACK_LANGUAGE;
|
||||
}
|
||||
|
||||
function persistLanguage(code, scope = "auto") {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, code);
|
||||
const guestScope = scope === "guest" || (scope === "auto" && !hasAccountSession());
|
||||
if (guestScope) localStorage.setItem(GUEST_STORAGE_KEY, code);
|
||||
} catch { /* storage may be blocked */ }
|
||||
}
|
||||
|
||||
export function rememberGuestLanguage(code = activeCode) {
|
||||
try {
|
||||
const available = new Set(catalog.map(item => item.code));
|
||||
const saved = localStorage.getItem(GUEST_STORAGE_KEY);
|
||||
if (available.has(saved)) return;
|
||||
const value = available.has(code) ? code : FALLBACK_LANGUAGE;
|
||||
localStorage.setItem(GUEST_STORAGE_KEY, value);
|
||||
} catch { /* storage may be blocked */ }
|
||||
}
|
||||
|
||||
export async function restoreGuestLanguage() {
|
||||
let code = FALLBACK_LANGUAGE;
|
||||
try { code = localStorage.getItem(GUEST_STORAGE_KEY) || FALLBACK_LANGUAGE; } catch { /* storage may be blocked */ }
|
||||
const applied = await setLanguage(code, { persist: false });
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, applied);
|
||||
localStorage.setItem(GUEST_STORAGE_KEY, applied);
|
||||
} catch { /* storage may be blocked */ }
|
||||
return applied;
|
||||
}
|
||||
|
||||
function applyDocumentLanguage() {
|
||||
const meta = activeBundle?.meta || fallbackBundle?.meta;
|
||||
if (!meta) return;
|
||||
@@ -307,16 +338,14 @@ function applyDocumentLanguage() {
|
||||
document.documentElement.dataset.locale = meta.locale;
|
||||
}
|
||||
|
||||
export async function setLanguage(code, { persist = true } = {}) {
|
||||
export async function setLanguage(code, { persist = true, scope = "auto" } = {}) {
|
||||
await initI18n();
|
||||
const available = catalog.find(item => item.code === code);
|
||||
const target = available ? available.code : FALLBACK_LANGUAGE;
|
||||
activeBundle = await loadBundle(target);
|
||||
activeCode = activeBundle.meta.code;
|
||||
rebuildSourceIndex();
|
||||
if (persist) {
|
||||
try { localStorage.setItem(STORAGE_KEY, activeCode); } catch { /* storage may be blocked */ }
|
||||
}
|
||||
if (persist) persistLanguage(activeCode, scope);
|
||||
applyDocumentLanguage();
|
||||
translateTree(document, true);
|
||||
document.dispatchEvent(new CustomEvent("rustpad:languagechange", {
|
||||
@@ -473,6 +502,13 @@ export function populateLanguageSelect(input, selectedCode = getLanguage()) {
|
||||
}));
|
||||
}
|
||||
|
||||
window.addEventListener("storage", event => {
|
||||
if (event.key !== STORAGE_KEY && event.key !== GUEST_STORAGE_KEY && event.key !== AUTH_STATE_KEY) return;
|
||||
if (event.key === GUEST_STORAGE_KEY && hasAccountSession()) return;
|
||||
const next = preferredCode();
|
||||
if (next !== activeCode) void setLanguage(next, { persist: false });
|
||||
});
|
||||
|
||||
export function formatDateTime(value, options) {
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return String(value ?? "");
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Mateusz Gruszczynski @linuxiarz.pl
|
||||
* Source-Available Code / Dual-Licensed.
|
||||
*/
|
||||
|
||||
import { api } from "@rustpad/api";
|
||||
import { getAvailableLanguages, getLanguage, setLanguage, t } from "@rustpad/i18n";
|
||||
import { applyTheme, getTheme } from "@rustpad/theme";
|
||||
import { toast } from "@rustpad/toast";
|
||||
|
||||
const AUTH_STATE_KEY = "rustpad:auth-state";
|
||||
const THEMES = ["dark", "light"];
|
||||
let initialized = false;
|
||||
let saveChain = Promise.resolve();
|
||||
|
||||
function hasAccountSession() {
|
||||
try { return localStorage.getItem(AUTH_STATE_KEY) === "1"; } catch { return false; }
|
||||
}
|
||||
|
||||
function icon(name) {
|
||||
if (name === "language") {
|
||||
return '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"></circle><path d="M3 12h18M12 3a15 15 0 0 1 0 18M12 3a15 15 0 0 0 0 18"></path></svg>';
|
||||
}
|
||||
if (name === "light") {
|
||||
return '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2M12 20v2M4.93 4.93l1.42 1.42M17.65 17.65l1.42 1.42M2 12h2M20 12h2M4.93 19.07l1.42-1.42M17.65 6.35l1.42-1.42"></path></svg>';
|
||||
}
|
||||
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20.2 15.3A8.5 8.5 0 0 1 8.7 3.8 8.5 8.5 0 1 0 20.2 15.3Z"></path></svg>';
|
||||
}
|
||||
|
||||
function closeOtherPickers(except = null) {
|
||||
document.querySelectorAll(".header-preference-picker[open]").forEach(details => {
|
||||
if (details !== except) details.open = false;
|
||||
});
|
||||
}
|
||||
|
||||
function preferenceButton({ value, label, selected, languageCode }) {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "header-preference-option";
|
||||
button.dataset.value = value;
|
||||
button.setAttribute("role", "option");
|
||||
button.setAttribute("aria-selected", selected ? "true" : "false");
|
||||
if (languageCode) button.lang = languageCode;
|
||||
|
||||
const check = document.createElement("span");
|
||||
check.className = "header-preference-option__check";
|
||||
check.setAttribute("aria-hidden", "true");
|
||||
check.textContent = selected ? "✓" : "";
|
||||
|
||||
const copy = document.createElement("span");
|
||||
copy.className = "header-preference-option__copy";
|
||||
copy.textContent = label;
|
||||
|
||||
button.append(check, copy);
|
||||
return button;
|
||||
}
|
||||
|
||||
function createPicker(kind) {
|
||||
const details = document.createElement("details");
|
||||
details.className = `header-preference-picker header-preference-picker--${kind}`;
|
||||
details.dataset.preferenceKind = kind;
|
||||
|
||||
const summary = document.createElement("summary");
|
||||
summary.className = "header-preference-trigger";
|
||||
|
||||
const iconWrap = document.createElement("span");
|
||||
iconWrap.className = "header-preference-trigger__icon";
|
||||
iconWrap.innerHTML = icon(kind === "theme" ? getTheme() : "language");
|
||||
|
||||
const compactValue = document.createElement("span");
|
||||
compactValue.className = "header-preference-trigger__value";
|
||||
compactValue.setAttribute("aria-hidden", "true");
|
||||
|
||||
const chevron = document.createElement("span");
|
||||
chevron.className = "header-preference-trigger__chevron";
|
||||
chevron.setAttribute("aria-hidden", "true");
|
||||
chevron.textContent = "▾";
|
||||
|
||||
summary.append(iconWrap, compactValue, chevron);
|
||||
|
||||
const menu = document.createElement("div");
|
||||
menu.className = "header-preference-menu";
|
||||
menu.setAttribute("role", "listbox");
|
||||
|
||||
details.append(summary, menu);
|
||||
details.addEventListener("toggle", () => {
|
||||
if (details.open) closeOtherPickers(details);
|
||||
});
|
||||
return details;
|
||||
}
|
||||
|
||||
async function persistAccountPreference(patch) {
|
||||
if (!hasAccountSession()) return;
|
||||
saveChain = saveChain.catch(() => undefined).then(() => api("/api/auth/profile", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(patch),
|
||||
}));
|
||||
return saveChain;
|
||||
}
|
||||
|
||||
function renderLanguagePicker(details) {
|
||||
const languages = getAvailableLanguages();
|
||||
const current = getLanguage();
|
||||
const selected = languages.find(language => language.code === current) || languages[0];
|
||||
const summary = details.querySelector(".header-preference-trigger");
|
||||
const value = details.querySelector(".header-preference-trigger__value");
|
||||
const menu = details.querySelector(".header-preference-menu");
|
||||
const label = t("common.language", {}, "Language");
|
||||
|
||||
summary.setAttribute("aria-label", label);
|
||||
summary.title = label;
|
||||
menu.setAttribute("aria-label", label);
|
||||
value.textContent = String(selected?.code || current || "en").toUpperCase();
|
||||
menu.replaceChildren(...languages.map(language => preferenceButton({
|
||||
value: language.code,
|
||||
label: language.native_name || language.name || language.code,
|
||||
selected: language.code === current,
|
||||
languageCode: language.code,
|
||||
})));
|
||||
}
|
||||
|
||||
function renderThemePicker(details) {
|
||||
const current = getTheme();
|
||||
const summary = details.querySelector(".header-preference-trigger");
|
||||
const iconWrap = details.querySelector(".header-preference-trigger__icon");
|
||||
const value = details.querySelector(".header-preference-trigger__value");
|
||||
const menu = details.querySelector(".header-preference-menu");
|
||||
const label = t("profile.theme", {}, "Interface theme");
|
||||
|
||||
summary.setAttribute("aria-label", label);
|
||||
summary.title = label;
|
||||
menu.setAttribute("aria-label", label);
|
||||
iconWrap.innerHTML = icon(current);
|
||||
value.textContent = t(`theme.${current}`, {}, current === "light" ? "Light" : "Dark");
|
||||
menu.replaceChildren(...THEMES.map(theme => preferenceButton({
|
||||
value: theme,
|
||||
label: t(`theme.${theme}`, {}, theme === "light" ? "Light" : "Dark"),
|
||||
selected: theme === current,
|
||||
})));
|
||||
}
|
||||
|
||||
function renderPicker(details) {
|
||||
if (details.dataset.preferenceKind === "language") renderLanguagePicker(details);
|
||||
else renderThemePicker(details);
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
document.querySelectorAll(".header-preference-picker").forEach(renderPicker);
|
||||
}
|
||||
|
||||
function bindPicker(details) {
|
||||
details.querySelector(".header-preference-menu")?.addEventListener("click", async event => {
|
||||
const button = event.target.closest(".header-preference-option");
|
||||
if (!(button instanceof HTMLButtonElement)) return;
|
||||
const next = button.dataset.value || "";
|
||||
const kind = details.dataset.preferenceKind;
|
||||
details.open = false;
|
||||
|
||||
if (kind === "language") {
|
||||
const previous = getLanguage();
|
||||
if (!next || next === previous) return;
|
||||
await setLanguage(next);
|
||||
renderAll();
|
||||
try {
|
||||
await persistAccountPreference({ language: next });
|
||||
} catch (error) {
|
||||
if (error?.status !== 401) {
|
||||
await setLanguage(previous);
|
||||
renderAll();
|
||||
toast.danger(error.message, { title: t("profile.language.failed", {}, "Could not change language") });
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const previous = getTheme();
|
||||
if (!THEMES.includes(next) || next === previous) return;
|
||||
applyTheme(next);
|
||||
renderAll();
|
||||
try {
|
||||
await persistAccountPreference({ theme: next });
|
||||
} catch (error) {
|
||||
if (error?.status !== 401) {
|
||||
applyTheme(previous);
|
||||
renderAll();
|
||||
toast.danger(error.message, { title: t("profile.updateFailed", {}, "Could not update profile") });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function mount(container) {
|
||||
if (!(container instanceof HTMLElement) || container.dataset.headerPreferencesMounted === "true") return;
|
||||
container.dataset.headerPreferencesMounted = "true";
|
||||
container.setAttribute("data-i18n-ignore", "");
|
||||
const language = createPicker("language");
|
||||
const theme = createPicker("theme");
|
||||
container.replaceChildren(language, theme);
|
||||
bindPicker(language);
|
||||
bindPicker(theme);
|
||||
renderPicker(language);
|
||||
renderPicker(theme);
|
||||
}
|
||||
|
||||
export function initHeaderPreferences() {
|
||||
document.querySelectorAll("[data-header-preferences]").forEach(mount);
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
|
||||
document.addEventListener("pointerdown", event => {
|
||||
const target = event.target instanceof Element ? event.target : null;
|
||||
if (!target?.closest(".header-preference-picker")) closeOtherPickers();
|
||||
}, { passive: true });
|
||||
document.addEventListener("keydown", event => {
|
||||
if (event.key === "Escape") closeOtherPickers();
|
||||
});
|
||||
document.addEventListener("rustpad:languagechange", renderAll);
|
||||
window.addEventListener("rustpad:theme-change", renderAll);
|
||||
window.addEventListener("rustpad:session-change", renderAll);
|
||||
window.addEventListener("rustpad:session-expired", renderAll);
|
||||
}
|
||||
+4
-1
@@ -26,6 +26,9 @@ const lineLinksToggle = document.querySelector("#public-line-links-toggle");
|
||||
const fullWidthToggle = document.querySelector("#public-full-width-toggle");
|
||||
const fullWidthStorageKey = "rustpad:public-full-width";
|
||||
const passwordDialog = document.querySelector("#public-password-dialog"), passwordForm = document.querySelector("#public-password-form"), passwordInput = document.querySelector("#public-password"), passwordError = document.querySelector("#public-password-error");
|
||||
const publicTitle = document.querySelector("#public-title");
|
||||
publicTitle?.removeAttribute("data-i18n");
|
||||
if (publicTitle) publicTitle.textContent = t("common.loading", {}, "Loading…");
|
||||
let pagePassword = "";
|
||||
let mermaidRenderVersion = 0;
|
||||
let publicAnchorSettleCleanup = null;
|
||||
@@ -179,7 +182,7 @@ async function initialize() {
|
||||
const page = await api(`/api/public/${encodeURIComponent(token)}`, { headers: pageHeaders() });
|
||||
if (passwordDialog.open) passwordDialog.close();
|
||||
passwordError.textContent = "";
|
||||
document.querySelector("#public-title").textContent = page.title;
|
||||
publicTitle.textContent = page.title;
|
||||
document.querySelector("#public-meta").textContent = page.allow_task_updates
|
||||
? t("public.updatedTasks", { date: formatDateTime(page.updated_at) }, `Updated: ${formatDateTime(page.updated_at)} · tasks can be updated`)
|
||||
: t("public.updated", { date: formatDateTime(page.updated_at) }, `Updated: ${formatDateTime(page.updated_at)}`);
|
||||
|
||||
+12
-5
@@ -7,8 +7,8 @@
|
||||
* See LICENSE file in repository root for details.
|
||||
*/
|
||||
|
||||
import { applySessionTheme } from "@rustpad/theme";
|
||||
import { setLanguage } from "@rustpad/i18n";
|
||||
import { applySessionTheme, rememberGuestTheme, restoreGuestTheme } from "@rustpad/theme";
|
||||
import { getLanguage, rememberGuestLanguage, restoreGuestLanguage, setLanguage } from "@rustpad/i18n";
|
||||
|
||||
const ACCESS_STORAGE_VERSION_KEY = "rustpad:access-storage-version";
|
||||
const ACCESS_STORAGE_VERSION = "http-only-cookie-v1";
|
||||
@@ -84,10 +84,16 @@ localStorage.removeItem("rustpad:auth-token");
|
||||
sessionStorage.removeItem("rustpad:auth-token");
|
||||
export function getAuthToken() { return localStorage.getItem(AUTH_STATE_KEY) ? "cookie" : ""; }
|
||||
export async function setAuthSession(session) {
|
||||
const hadAccountSession = localStorage.getItem(AUTH_STATE_KEY) === "1";
|
||||
if (!hadAccountSession) {
|
||||
rememberGuestTheme();
|
||||
rememberGuestLanguage(getLanguage());
|
||||
}
|
||||
localStorage.setItem(AUTH_STATE_KEY, "1");
|
||||
setNickname(session.nickname);
|
||||
applySessionTheme(session);
|
||||
await setLanguage(session?.language || "en");
|
||||
await setLanguage(session?.language || "en", { scope: "account" });
|
||||
window.dispatchEvent(new CustomEvent("rustpad:session-change", { detail: { session } }));
|
||||
}
|
||||
export async function clearAuthSession() {
|
||||
localStorage.removeItem(AUTH_STATE_KEY);
|
||||
@@ -95,7 +101,8 @@ export async function clearAuthSession() {
|
||||
sessionStorage.removeItem("rustpad:auth-token");
|
||||
localStorage.removeItem(NICKNAME_KEY);
|
||||
sessionStorage.removeItem(NICKNAME_KEY);
|
||||
localStorage.removeItem("rustpad:language");
|
||||
setNicknameCookie("");
|
||||
await setLanguage("en", { persist: false });
|
||||
restoreGuestTheme();
|
||||
await restoreGuestLanguage();
|
||||
window.dispatchEvent(new CustomEvent("rustpad:session-change", { detail: { session: null } }));
|
||||
}
|
||||
|
||||
+48
-8
@@ -7,6 +7,8 @@
|
||||
* See LICENSE file in repository root for details.
|
||||
*/
|
||||
const STORAGE_KEY = "rustpad:theme";
|
||||
const GUEST_STORAGE_KEY = "rustpad:guest-theme";
|
||||
const AUTH_STATE_KEY = "rustpad:auth-state";
|
||||
const THEMES = new Set(["dark", "light"]);
|
||||
const systemThemeQuery = window.matchMedia("(prefers-color-scheme: light)");
|
||||
|
||||
@@ -28,31 +30,69 @@ export function getTheme() {
|
||||
return normalizeTheme(document.documentElement.dataset.theme);
|
||||
}
|
||||
|
||||
export function applyTheme(value, { persist = true } = {}) {
|
||||
function hasAccountSession() {
|
||||
try { return localStorage.getItem(AUTH_STATE_KEY) === "1"; } catch { return false; }
|
||||
}
|
||||
|
||||
function persistTheme(theme, scope = "auto") {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, theme);
|
||||
const guestScope = scope === "guest" || (scope === "auto" && !hasAccountSession());
|
||||
if (guestScope) localStorage.setItem(GUEST_STORAGE_KEY, theme);
|
||||
} catch { }
|
||||
}
|
||||
|
||||
export function applyTheme(value, { persist = true, scope = "auto" } = {}) {
|
||||
const theme = normalizeTheme(value);
|
||||
const previousTheme = getTheme();
|
||||
updateBrowserChrome(theme);
|
||||
if (persist) {
|
||||
try { localStorage.setItem(STORAGE_KEY, theme); } catch { }
|
||||
}
|
||||
if (persist) persistTheme(theme, scope);
|
||||
if (theme !== previousTheme) {
|
||||
window.dispatchEvent(new CustomEvent("rustpad:theme-change", { detail: { theme } }));
|
||||
}
|
||||
return theme;
|
||||
}
|
||||
|
||||
export function rememberGuestTheme(value = getTheme()) {
|
||||
try {
|
||||
if (THEMES.has(localStorage.getItem(GUEST_STORAGE_KEY))) return;
|
||||
localStorage.setItem(GUEST_STORAGE_KEY, normalizeTheme(value));
|
||||
} catch { }
|
||||
}
|
||||
|
||||
export function restoreGuestTheme() {
|
||||
let saved = null;
|
||||
try { saved = localStorage.getItem(GUEST_STORAGE_KEY); } catch { }
|
||||
if (THEMES.has(saved)) {
|
||||
try { localStorage.setItem(STORAGE_KEY, saved); } catch { }
|
||||
return applyTheme(saved, { persist: false });
|
||||
}
|
||||
try { localStorage.removeItem(STORAGE_KEY); } catch { }
|
||||
return applyTheme(systemTheme(), { persist: false });
|
||||
}
|
||||
|
||||
export function applySessionTheme(session) {
|
||||
if (session?.theme) applyTheme(session.theme);
|
||||
if (session?.theme) applyTheme(session.theme, { scope: "account" });
|
||||
}
|
||||
|
||||
window.addEventListener("storage", event => {
|
||||
if (event.key !== STORAGE_KEY) return;
|
||||
applyTheme(THEMES.has(event.newValue) ? event.newValue : systemTheme(), { persist: false });
|
||||
if (event.key !== STORAGE_KEY && event.key !== GUEST_STORAGE_KEY && event.key !== AUTH_STATE_KEY) return;
|
||||
if (event.key === GUEST_STORAGE_KEY && hasAccountSession()) return;
|
||||
let saved = null;
|
||||
try {
|
||||
saved = hasAccountSession()
|
||||
? localStorage.getItem(STORAGE_KEY)
|
||||
: (localStorage.getItem(GUEST_STORAGE_KEY) || localStorage.getItem(STORAGE_KEY));
|
||||
} catch { }
|
||||
applyTheme(THEMES.has(saved) ? saved : systemTheme(), { persist: false });
|
||||
});
|
||||
|
||||
systemThemeQuery.addEventListener("change", () => {
|
||||
try {
|
||||
if (THEMES.has(localStorage.getItem(STORAGE_KEY))) return;
|
||||
const saved = hasAccountSession()
|
||||
? localStorage.getItem(STORAGE_KEY)
|
||||
: (localStorage.getItem(GUEST_STORAGE_KEY) || localStorage.getItem(STORAGE_KEY));
|
||||
if (THEMES.has(saved)) return;
|
||||
} catch { }
|
||||
applyTheme(systemTheme(), { persist: false });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user