mirror of
https://github.com/katelya77/K-Vault.git
synced 2026-05-06 22:10:57 +08:00
1045 lines
32 KiB
JavaScript
1045 lines
32 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
var STORAGE_KEY = "themeMode";
|
|
var THEME_ATTR = "data-theme";
|
|
var VALID = { light: true, dark: true };
|
|
var root = document.documentElement;
|
|
|
|
function normalizeTheme(theme) {
|
|
return VALID[theme] ? theme : "light";
|
|
}
|
|
|
|
function getStoredTheme() {
|
|
try {
|
|
return normalizeTheme(localStorage.getItem(STORAGE_KEY));
|
|
} catch (e) {
|
|
return "light";
|
|
}
|
|
}
|
|
|
|
function getCurrentTheme() {
|
|
return normalizeTheme(root.getAttribute(THEME_ATTR));
|
|
}
|
|
|
|
function updateToggleVisual(button, theme) {
|
|
var normalized = normalizeTheme(theme);
|
|
var icon = button.querySelector("[data-theme-icon]");
|
|
var label = button.querySelector("[data-theme-label]");
|
|
var toDark = normalized !== "dark";
|
|
|
|
button.setAttribute(
|
|
"aria-label",
|
|
toDark ? "切换到夜间模式" : "切换到亮色模式"
|
|
);
|
|
button.setAttribute(
|
|
"title",
|
|
toDark ? "切换到夜间模式" : "切换到亮色模式"
|
|
);
|
|
|
|
if (icon) {
|
|
icon.className = toDark ? "fas fa-moon" : "fas fa-sun";
|
|
}
|
|
if (label) {
|
|
label.textContent = toDark ? "夜间" : "亮色";
|
|
}
|
|
}
|
|
|
|
function updateAllToggles(theme) {
|
|
var toggles = document.querySelectorAll("[data-theme-toggle]");
|
|
for (var i = 0; i < toggles.length; i++) {
|
|
updateToggleVisual(toggles[i], theme);
|
|
}
|
|
}
|
|
|
|
function applyTheme(theme, options) {
|
|
var opts = options || {};
|
|
var persist = opts.persist !== false;
|
|
var broadcast = opts.broadcast !== false;
|
|
var normalized = normalizeTheme(theme);
|
|
|
|
root.setAttribute(THEME_ATTR, normalized);
|
|
root.style.colorScheme = normalized;
|
|
updateAllToggles(normalized);
|
|
|
|
if (persist) {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, normalized);
|
|
} catch (e) {}
|
|
}
|
|
|
|
if (broadcast) {
|
|
try {
|
|
window.dispatchEvent(
|
|
new CustomEvent("theme:change", { detail: { theme: normalized } })
|
|
);
|
|
} catch (e) {}
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
function bindToggle(button) {
|
|
if (!button || button.dataset.themeBound === "1") return;
|
|
button.dataset.themeBound = "1";
|
|
button.addEventListener("click", function () {
|
|
ThemeManager.toggleTheme();
|
|
});
|
|
updateToggleVisual(button, getCurrentTheme());
|
|
}
|
|
|
|
function createToggleButton(className) {
|
|
var button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = className;
|
|
button.setAttribute("data-theme-toggle", "");
|
|
button.innerHTML =
|
|
'<i class="fas fa-moon" data-theme-icon></i><span data-theme-label>夜间</span>';
|
|
return button;
|
|
}
|
|
|
|
function ensureAutoToggle() {
|
|
if (document.querySelector("[data-theme-toggle]")) return;
|
|
|
|
var navLinks = document.querySelector(".header .nav-links");
|
|
if (navLinks) {
|
|
var inlineBtn = createToggleButton("theme-auto-inline-toggle");
|
|
navLinks.insertBefore(inlineBtn, navLinks.firstChild);
|
|
bindToggle(inlineBtn);
|
|
return;
|
|
}
|
|
|
|
var adminActions = document.querySelector(".header-content .actions");
|
|
if (adminActions) {
|
|
var adminBtn = createToggleButton("theme-admin-toggle");
|
|
adminActions.insertBefore(adminBtn, adminActions.firstChild);
|
|
bindToggle(adminBtn);
|
|
return;
|
|
}
|
|
|
|
if (document.body && document.body.dataset.disableThemeToggle === "true") {
|
|
return;
|
|
}
|
|
|
|
var floatingBtn = createToggleButton("theme-floating-toggle");
|
|
document.body.appendChild(floatingBtn);
|
|
bindToggle(floatingBtn);
|
|
}
|
|
|
|
function initDom() {
|
|
var toggles = document.querySelectorAll("[data-theme-toggle]");
|
|
for (var i = 0; i < toggles.length; i++) {
|
|
bindToggle(toggles[i]);
|
|
}
|
|
ensureAutoToggle();
|
|
updateAllToggles(getCurrentTheme());
|
|
}
|
|
|
|
var ThemeManager = {
|
|
getTheme: getCurrentTheme,
|
|
setTheme: function (theme) {
|
|
return applyTheme(theme, { persist: true, broadcast: true });
|
|
},
|
|
toggleTheme: function () {
|
|
var next = getCurrentTheme() === "dark" ? "light" : "dark";
|
|
return applyTheme(next, { persist: true, broadcast: true });
|
|
},
|
|
};
|
|
|
|
window.ThemeManager = ThemeManager;
|
|
|
|
// Always default to light if user has no saved preference.
|
|
applyTheme(getStoredTheme(), { persist: false, broadcast: false });
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", initDom, { once: true });
|
|
} else {
|
|
initDom();
|
|
}
|
|
|
|
window.addEventListener("storage", function (event) {
|
|
if (event.key !== STORAGE_KEY) return;
|
|
applyTheme(getStoredTheme(), { persist: false, broadcast: false });
|
|
});
|
|
})();
|
|
|
|
(function () {
|
|
"use strict";
|
|
|
|
var STORAGE_KEY = "kvUiDesignSettings";
|
|
var LEGACY_LOGIN_MODE_KEY = "loginBackgroundMode";
|
|
var LEGACY_LOGIN_URL_KEY = "loginBackgroundUrl";
|
|
var API_ENDPOINT = "/api/ui-config";
|
|
var EFFECT_STYLES = { none: true, math: true, particle: true, texture: true };
|
|
|
|
var DEFAULTS = {
|
|
version: 1,
|
|
baseColor: "#fafaf8",
|
|
globalBackgroundUrl: "",
|
|
loginBackgroundMode: "follow-global",
|
|
loginBackgroundUrl: "",
|
|
cardOpacity: 86,
|
|
cardBlur: 14,
|
|
effectStyle: "math",
|
|
effectIntensity: 22,
|
|
optimizeMobile: true,
|
|
};
|
|
|
|
var root = document.documentElement;
|
|
var settings = null;
|
|
var layers = { image: null, canvas: null, noise: null };
|
|
var render = {
|
|
ctx: null,
|
|
rafId: 0,
|
|
width: 0,
|
|
height: 0,
|
|
lastTs: 0,
|
|
style: "none",
|
|
intensity: 0,
|
|
mobile: false,
|
|
maxFps: 30,
|
|
symbols: [],
|
|
particles: [],
|
|
};
|
|
|
|
function cloneSettings(input) {
|
|
return Object.assign({}, input || {});
|
|
}
|
|
|
|
function clampNumber(value, min, max) {
|
|
var numeric = Number(value);
|
|
if (!isFinite(numeric)) return min;
|
|
return Math.min(max, Math.max(min, numeric));
|
|
}
|
|
|
|
function normalizeHexColor(value) {
|
|
var text = String(value || "").trim();
|
|
if (!/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(text)) {
|
|
return DEFAULTS.baseColor;
|
|
}
|
|
if (text.length === 4) {
|
|
return (
|
|
"#" +
|
|
text[1] +
|
|
text[1] +
|
|
text[2] +
|
|
text[2] +
|
|
text[3] +
|
|
text[3]
|
|
).toLowerCase();
|
|
}
|
|
return text.toLowerCase();
|
|
}
|
|
|
|
function sanitizeUrl(url) {
|
|
var text = String(url || "").trim();
|
|
if (!text) return "";
|
|
if (/^(https?:)?\/\//i.test(text)) return text;
|
|
if (/^\//.test(text)) return text;
|
|
return "";
|
|
}
|
|
|
|
function normalizeSettings(raw) {
|
|
var next = Object.assign({}, DEFAULTS, raw || {});
|
|
next.baseColor = normalizeHexColor(next.baseColor);
|
|
next.globalBackgroundUrl = sanitizeUrl(next.globalBackgroundUrl);
|
|
next.loginBackgroundMode =
|
|
next.loginBackgroundMode === "custom" ? "custom" : "follow-global";
|
|
next.loginBackgroundUrl = sanitizeUrl(next.loginBackgroundUrl);
|
|
next.cardOpacity = Math.round(clampNumber(next.cardOpacity, 0, 100));
|
|
next.cardBlur = Math.round(clampNumber(next.cardBlur, 0, 32));
|
|
next.effectStyle = EFFECT_STYLES[next.effectStyle]
|
|
? next.effectStyle
|
|
: DEFAULTS.effectStyle;
|
|
next.effectIntensity = Math.round(clampNumber(next.effectIntensity, 0, 100));
|
|
next.optimizeMobile = next.optimizeMobile !== false;
|
|
return next;
|
|
}
|
|
|
|
function saveLocalSettings(next) {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
|
} catch (e) {}
|
|
}
|
|
|
|
function readSettings() {
|
|
try {
|
|
var raw = localStorage.getItem(STORAGE_KEY);
|
|
if (!raw) return normalizeSettings(DEFAULTS);
|
|
return normalizeSettings(JSON.parse(raw));
|
|
} catch (e) {
|
|
return normalizeSettings(DEFAULTS);
|
|
}
|
|
}
|
|
|
|
function extractConfigPayload(payload) {
|
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
return null;
|
|
}
|
|
|
|
var fromData =
|
|
payload.data && typeof payload.data === "object" && !Array.isArray(payload.data)
|
|
? payload.data
|
|
: null;
|
|
|
|
var candidate =
|
|
payload.config ||
|
|
payload.settings ||
|
|
(fromData && (fromData.config || fromData.settings)) ||
|
|
null;
|
|
|
|
if (!candidate && !("success" in payload) && !("error" in payload)) {
|
|
candidate = payload;
|
|
}
|
|
|
|
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
return null;
|
|
}
|
|
return candidate;
|
|
}
|
|
|
|
function extractErrorMessage(payload, fallback) {
|
|
if (!payload || typeof payload !== "object") return fallback;
|
|
if (typeof payload.error === "string" && payload.error) return payload.error;
|
|
if (payload.error && typeof payload.error === "object") {
|
|
if (typeof payload.error.message === "string" && payload.error.message) {
|
|
return payload.error.message;
|
|
}
|
|
if (typeof payload.error.detail === "string" && payload.error.detail) {
|
|
return payload.error.detail;
|
|
}
|
|
}
|
|
if (typeof payload.message === "string" && payload.message) return payload.message;
|
|
return fallback;
|
|
}
|
|
|
|
function requestUiConfig(method, config) {
|
|
if (typeof fetch !== "function") {
|
|
return Promise.reject(new Error("Fetch API is not available"));
|
|
}
|
|
|
|
var url =
|
|
method === "GET"
|
|
? API_ENDPOINT + (API_ENDPOINT.indexOf("?") >= 0 ? "&" : "?") + "_ts=" + Date.now()
|
|
: API_ENDPOINT;
|
|
|
|
var init = {
|
|
method: method,
|
|
credentials: "include",
|
|
cache: "no-store",
|
|
headers: {
|
|
Accept: "application/json",
|
|
},
|
|
};
|
|
|
|
if (method !== "GET" && config) {
|
|
init.headers["Content-Type"] = "application/json";
|
|
init.body = JSON.stringify({ config: config });
|
|
}
|
|
|
|
return fetch(url, init).then(function (response) {
|
|
return response
|
|
.text()
|
|
.then(function (raw) {
|
|
var payload = {};
|
|
if (raw) {
|
|
try {
|
|
payload = JSON.parse(raw);
|
|
} catch (e) {
|
|
payload = {};
|
|
}
|
|
}
|
|
|
|
if (!response.ok) {
|
|
var fallback = "Request failed (" + response.status + ")";
|
|
throw new Error(extractErrorMessage(payload, fallback));
|
|
}
|
|
|
|
if (payload && payload.success === false) {
|
|
throw new Error(extractErrorMessage(payload, "Request failed"));
|
|
}
|
|
|
|
return payload;
|
|
})
|
|
.catch(function (error) {
|
|
if (error instanceof Error) throw error;
|
|
throw new Error(String(error || "Request failed"));
|
|
});
|
|
});
|
|
}
|
|
|
|
function syncFromServer(options) {
|
|
var opts = options || {};
|
|
var silent = opts.silent !== false;
|
|
var applyLocalOnFailure = opts.applyLocalOnFailure === true;
|
|
|
|
return requestUiConfig("GET")
|
|
.then(function (payload) {
|
|
var remote = extractConfigPayload(payload);
|
|
if (!remote) {
|
|
return {
|
|
success: false,
|
|
source: "local",
|
|
settings: cloneSettings(settings || readSettings()),
|
|
error: "Server returned invalid ui config payload",
|
|
};
|
|
}
|
|
|
|
var normalized = normalizeSettings(remote);
|
|
saveLocalSettings(normalized);
|
|
applySettings(normalized, { persist: false, silent: silent });
|
|
return {
|
|
success: true,
|
|
source: "server",
|
|
binding: payload && payload.binding ? String(payload.binding) : "",
|
|
settings: cloneSettings(normalized),
|
|
};
|
|
})
|
|
.catch(function (error) {
|
|
try {
|
|
console.warn("[ui-config] GET /api/ui-config failed:", error);
|
|
} catch (e) {}
|
|
if (applyLocalOnFailure) {
|
|
var local = readSettings();
|
|
applySettings(local, { persist: false, silent: silent });
|
|
}
|
|
|
|
if (opts.throwOnError) {
|
|
throw error;
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
source: "local",
|
|
settings: cloneSettings(settings || readSettings()),
|
|
error: error && error.message ? error.message : String(error),
|
|
};
|
|
});
|
|
}
|
|
|
|
function saveToServer(partial, options) {
|
|
var opts = options || {};
|
|
var merged = Object.assign({}, settings || DEFAULTS, partial || {});
|
|
var localApplied = applySettings(merged, {
|
|
persist: true,
|
|
silent: !!opts.silent,
|
|
});
|
|
|
|
return requestUiConfig("POST", localApplied)
|
|
.then(function (payload) {
|
|
var remote = extractConfigPayload(payload) || localApplied;
|
|
var normalized = normalizeSettings(remote);
|
|
var binding = payload && payload.binding ? String(payload.binding) : "";
|
|
saveLocalSettings(normalized);
|
|
applySettings(normalized, { persist: false, silent: !!opts.silent });
|
|
return requestUiConfig("GET")
|
|
.then(function (verifyPayload) {
|
|
var verifyRemote = extractConfigPayload(verifyPayload);
|
|
if (!verifyRemote) {
|
|
throw new Error("服务端回读配置格式异常");
|
|
}
|
|
var verified = normalizeSettings(verifyRemote);
|
|
saveLocalSettings(verified);
|
|
applySettings(verified, { persist: false, silent: !!opts.silent });
|
|
|
|
var isMatch = JSON.stringify(verified) === JSON.stringify(normalized);
|
|
if (!isMatch) {
|
|
return {
|
|
success: false,
|
|
source: "local",
|
|
binding:
|
|
binding ||
|
|
(verifyPayload && verifyPayload.binding
|
|
? String(verifyPayload.binding)
|
|
: ""),
|
|
settings: cloneSettings(verified),
|
|
error: "保存后回读校验未通过,请检查 KV 绑定与 Functions 日志。",
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
source: "server",
|
|
binding:
|
|
binding ||
|
|
(verifyPayload && verifyPayload.binding
|
|
? String(verifyPayload.binding)
|
|
: ""),
|
|
settings: cloneSettings(verified),
|
|
};
|
|
})
|
|
.catch(function (verifyError) {
|
|
return {
|
|
success: false,
|
|
source: "local",
|
|
binding: binding,
|
|
settings: cloneSettings(localApplied),
|
|
error:
|
|
"保存后读取校验失败:" +
|
|
(verifyError && verifyError.message
|
|
? verifyError.message
|
|
: String(verifyError)),
|
|
};
|
|
});
|
|
})
|
|
.catch(function (error) {
|
|
try {
|
|
console.error("[ui-config] POST /api/ui-config failed:", error);
|
|
} catch (e) {}
|
|
if (opts.throwOnError) {
|
|
throw error;
|
|
}
|
|
return {
|
|
success: false,
|
|
source: "local",
|
|
binding: "",
|
|
settings: cloneSettings(localApplied),
|
|
error: error && error.message ? error.message : String(error),
|
|
};
|
|
});
|
|
}
|
|
|
|
function migrateLegacySettings() {
|
|
try {
|
|
if (localStorage.getItem(STORAGE_KEY)) return null;
|
|
var legacyMode = String(localStorage.getItem(LEGACY_LOGIN_MODE_KEY) || "")
|
|
.trim()
|
|
.toLowerCase();
|
|
var legacyUrl = sanitizeUrl(localStorage.getItem(LEGACY_LOGIN_URL_KEY));
|
|
if (!legacyMode && !legacyUrl) return null;
|
|
|
|
var migrated = normalizeSettings(DEFAULTS);
|
|
if (legacyMode === "image" && legacyUrl) {
|
|
migrated.loginBackgroundMode = "custom";
|
|
migrated.loginBackgroundUrl = legacyUrl;
|
|
}
|
|
saveLocalSettings(migrated);
|
|
return migrated;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function isLoginPage() {
|
|
var pathname = String(window.location.pathname || "").toLowerCase();
|
|
return /(^|\/)login(\.html)?$/.test(pathname);
|
|
}
|
|
|
|
function isMobileDevice() {
|
|
var byWidth =
|
|
typeof window.matchMedia === "function"
|
|
? window.matchMedia("(max-width: 768px)").matches
|
|
: window.innerWidth <= 768;
|
|
var byTouch = Number(navigator.maxTouchPoints || 0) > 0;
|
|
return byWidth || byTouch;
|
|
}
|
|
|
|
function prefersReducedMotion() {
|
|
return (
|
|
typeof window.matchMedia === "function" &&
|
|
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
);
|
|
}
|
|
|
|
function ensureLayer(tagName, className) {
|
|
var node = document.createElement(tagName);
|
|
node.className = className;
|
|
node.setAttribute("aria-hidden", "true");
|
|
return node;
|
|
}
|
|
|
|
function ensureLayers() {
|
|
if (!document.body) return false;
|
|
|
|
if (!layers.image) layers.image = ensureLayer("div", "ui-bg-image-layer");
|
|
if (!layers.canvas) layers.canvas = ensureLayer("canvas", "ui-bg-canvas-layer");
|
|
if (!layers.noise) layers.noise = ensureLayer("div", "ui-bg-noise-layer");
|
|
|
|
if (!document.body.contains(layers.image)) {
|
|
document.body.insertBefore(layers.image, document.body.firstChild);
|
|
}
|
|
if (!document.body.contains(layers.canvas)) {
|
|
document.body.insertBefore(layers.canvas, layers.image.nextSibling);
|
|
}
|
|
if (!document.body.contains(layers.noise)) {
|
|
document.body.insertBefore(layers.noise, layers.canvas.nextSibling);
|
|
}
|
|
|
|
if (!render.ctx) {
|
|
render.ctx = layers.canvas.getContext("2d", { alpha: true });
|
|
}
|
|
|
|
ensureCanvasSize();
|
|
return true;
|
|
}
|
|
|
|
function ensureCanvasSize() {
|
|
if (!layers.canvas || !render.ctx) return;
|
|
var width = Math.max(window.innerWidth || 0, 1);
|
|
var height = Math.max(window.innerHeight || 0, 1);
|
|
var dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
var pixelWidth = Math.max(1, Math.floor(width * dpr));
|
|
var pixelHeight = Math.max(1, Math.floor(height * dpr));
|
|
|
|
if (layers.canvas.width !== pixelWidth || layers.canvas.height !== pixelHeight) {
|
|
layers.canvas.width = pixelWidth;
|
|
layers.canvas.height = pixelHeight;
|
|
layers.canvas.style.width = width + "px";
|
|
layers.canvas.style.height = height + "px";
|
|
render.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
}
|
|
|
|
render.width = width;
|
|
render.height = height;
|
|
}
|
|
|
|
function clearCanvas() {
|
|
if (!render.ctx) return;
|
|
render.ctx.clearRect(0, 0, render.width || 0, render.height || 0);
|
|
}
|
|
|
|
function stopRender(shouldClear) {
|
|
if (render.rafId) {
|
|
cancelAnimationFrame(render.rafId);
|
|
render.rafId = 0;
|
|
}
|
|
render.lastTs = 0;
|
|
if (shouldClear) clearCanvas();
|
|
}
|
|
|
|
function getEffectNodeCount(intensity, mobile) {
|
|
var count = Math.round(8 + intensity * 0.32);
|
|
if (mobile) count = Math.max(5, Math.round(count * 0.35));
|
|
return Math.min(40, Math.max(4, count));
|
|
}
|
|
|
|
function buildMathSymbols(count, mobile) {
|
|
var chars = ["∑", "∫", "π", "√", "∞", "∆", "∂", "λ", "θ", "∇", "⊕", "≈", "µ"];
|
|
var symbols = [];
|
|
var i = 0;
|
|
for (i = 0; i < count; i += 1) {
|
|
symbols.push({
|
|
text: chars[Math.floor(Math.random() * chars.length)],
|
|
x: Math.random() * render.width,
|
|
y: Math.random() * render.height,
|
|
vx: (Math.random() - 0.5) * (mobile ? 7 : 11),
|
|
vy: (mobile ? 4 : 7) + Math.random() * (mobile ? 6 : 10),
|
|
size: (mobile ? 10 : 11) + Math.random() * (mobile ? 7 : 12),
|
|
alpha: (mobile ? 0.06 : 0.05) + Math.random() * (mobile ? 0.08 : 0.11),
|
|
});
|
|
}
|
|
render.symbols = symbols;
|
|
}
|
|
|
|
function buildParticles(count, mobile) {
|
|
var nodes = [];
|
|
var speed = mobile ? 11 : 16;
|
|
var i = 0;
|
|
for (i = 0; i < count; i += 1) {
|
|
nodes.push({
|
|
x: Math.random() * render.width,
|
|
y: Math.random() * render.height,
|
|
vx: (Math.random() - 0.5) * speed,
|
|
vy: (Math.random() - 0.5) * speed,
|
|
r: (mobile ? 0.75 : 0.9) + Math.random() * (mobile ? 1.2 : 1.5),
|
|
});
|
|
}
|
|
render.particles = nodes;
|
|
}
|
|
|
|
function updateMathSymbols(deltaSec) {
|
|
var i = 0;
|
|
var item = null;
|
|
var width = render.width;
|
|
var height = render.height;
|
|
for (i = 0; i < render.symbols.length; i += 1) {
|
|
item = render.symbols[i];
|
|
item.y -= item.vy * deltaSec;
|
|
item.x += item.vx * deltaSec;
|
|
|
|
if (item.y < -40) item.y = height + 40;
|
|
if (item.x < -40) item.x = width + 40;
|
|
if (item.x > width + 40) item.x = -40;
|
|
}
|
|
}
|
|
|
|
function drawMathSymbols() {
|
|
var ctx = render.ctx;
|
|
var dark = root.getAttribute("data-theme") === "dark";
|
|
var rgb = dark ? "201, 214, 237" : "102, 113, 132";
|
|
var i = 0;
|
|
var item = null;
|
|
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
|
|
for (i = 0; i < render.symbols.length; i += 1) {
|
|
item = render.symbols[i];
|
|
ctx.font =
|
|
Math.round(item.size) +
|
|
'px "Cambria Math", "Times New Roman", "Noto Sans SC", serif';
|
|
ctx.fillStyle = "rgba(" + rgb + ", " + item.alpha.toFixed(3) + ")";
|
|
ctx.fillText(item.text, item.x, item.y);
|
|
}
|
|
}
|
|
|
|
function updateParticles(deltaSec) {
|
|
var i = 0;
|
|
var point = null;
|
|
var width = render.width;
|
|
var height = render.height;
|
|
for (i = 0; i < render.particles.length; i += 1) {
|
|
point = render.particles[i];
|
|
point.x += point.vx * deltaSec;
|
|
point.y += point.vy * deltaSec;
|
|
|
|
if (point.x <= 0 || point.x >= width) point.vx *= -1;
|
|
if (point.y <= 0 || point.y >= height) point.vy *= -1;
|
|
}
|
|
}
|
|
|
|
function drawParticles() {
|
|
var ctx = render.ctx;
|
|
var dark = root.getAttribute("data-theme") === "dark";
|
|
var rgb = dark ? "184, 198, 225" : "119, 131, 150";
|
|
var dotAlphaBase = dark ? 0.23 : 0.18;
|
|
var lineAlphaBase = dark ? 0.16 : 0.12;
|
|
var intensityFactor = Math.max(0.2, render.intensity / 100);
|
|
var distanceLimit = render.mobile ? 110 : 145;
|
|
var i = 0;
|
|
var j = 0;
|
|
var a = null;
|
|
var b = null;
|
|
var dx = 0;
|
|
var dy = 0;
|
|
var distance = 0;
|
|
var alpha = 0;
|
|
|
|
for (i = 0; i < render.particles.length; i += 1) {
|
|
a = render.particles[i];
|
|
ctx.beginPath();
|
|
ctx.arc(a.x, a.y, a.r, 0, Math.PI * 2);
|
|
ctx.fillStyle =
|
|
"rgba(" + rgb + ", " + (dotAlphaBase * intensityFactor).toFixed(3) + ")";
|
|
ctx.fill();
|
|
|
|
for (j = i + 1; j < render.particles.length; j += 1) {
|
|
b = render.particles[j];
|
|
dx = a.x - b.x;
|
|
dy = a.y - b.y;
|
|
distance = Math.sqrt(dx * dx + dy * dy);
|
|
if (distance > distanceLimit) continue;
|
|
alpha =
|
|
((1 - distance / distanceLimit) * lineAlphaBase * intensityFactor).toFixed(
|
|
3
|
|
);
|
|
ctx.beginPath();
|
|
ctx.moveTo(a.x, a.y);
|
|
ctx.lineTo(b.x, b.y);
|
|
ctx.strokeStyle = "rgba(" + rgb + ", " + alpha + ")";
|
|
ctx.lineWidth = 1;
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
|
|
function startRender(style, intensity, mobile) {
|
|
stopRender(true);
|
|
render.style = style;
|
|
render.intensity = intensity;
|
|
render.mobile = mobile;
|
|
render.maxFps = mobile ? 14 : 30;
|
|
|
|
if (!render.ctx || style === "none" || style === "texture" || prefersReducedMotion()) {
|
|
return;
|
|
}
|
|
|
|
var nodeCount = getEffectNodeCount(intensity, mobile);
|
|
if (style === "math") buildMathSymbols(nodeCount, mobile);
|
|
if (style === "particle") buildParticles(nodeCount, mobile);
|
|
|
|
render.rafId = requestAnimationFrame(function frame(ts) {
|
|
render.rafId = requestAnimationFrame(frame);
|
|
if (document.hidden) return;
|
|
|
|
var minDelta = 1000 / render.maxFps;
|
|
if (render.lastTs && ts - render.lastTs < minDelta) return;
|
|
var deltaSec = render.lastTs ? (ts - render.lastTs) / 1000 : minDelta / 1000;
|
|
render.lastTs = ts;
|
|
if (deltaSec > 0.08) deltaSec = 0.08;
|
|
|
|
clearCanvas();
|
|
|
|
if (render.style === "math") {
|
|
updateMathSymbols(deltaSec);
|
|
drawMathSymbols();
|
|
} else if (render.style === "particle") {
|
|
updateParticles(deltaSec);
|
|
drawParticles();
|
|
}
|
|
});
|
|
}
|
|
|
|
function applyCompatibilityVars(next, darkMode) {
|
|
var opacity = clampNumber(next.cardOpacity, 0, 100) / 100;
|
|
var blur = Math.round(clampNumber(next.cardBlur, 0, 32));
|
|
var surfaceAlpha = darkMode
|
|
? Math.max(0.5, Math.min(0.94, opacity))
|
|
: Math.max(0.28, Math.min(0.98, opacity));
|
|
var inputBorder = darkMode
|
|
? "rgba(122, 140, 168, 0.5)"
|
|
: "rgba(214, 220, 228, 0.9)";
|
|
var border = darkMode
|
|
? "rgba(92, 105, 126, 0.46)"
|
|
: "rgba(198, 206, 218, 0.52)";
|
|
var cardBg = darkMode
|
|
? "rgba(19, 24, 33, " + surfaceAlpha.toFixed(2) + ")"
|
|
: "rgba(255, 255, 255, " + surfaceAlpha.toFixed(2) + ")";
|
|
var surface1 = darkMode
|
|
? "rgba(24, 31, 42, " + Math.min(0.98, surfaceAlpha + 0.08).toFixed(2) + ")"
|
|
: "rgba(255, 255, 255, " + Math.min(0.99, surfaceAlpha + 0.07).toFixed(2) + ")";
|
|
var surface2 = darkMode
|
|
? "rgba(24, 31, 42, " + Math.max(0.44, surfaceAlpha - 0.05).toFixed(2) + ")"
|
|
: "rgba(255, 255, 255, " + Math.max(0.44, surfaceAlpha - 0.08).toFixed(2) + ")";
|
|
var surface3 = darkMode
|
|
? "rgba(30, 38, 51, " + Math.max(0.36, surfaceAlpha - 0.1).toFixed(2) + ")"
|
|
: "rgba(245, 246, 248, " + Math.max(0.34, surfaceAlpha - 0.17).toFixed(2) + ")";
|
|
var shadow = darkMode
|
|
? "0 12px 32px rgba(0, 0, 0, 0.34)"
|
|
: "0 10px 30px rgba(15, 23, 42, 0.09)";
|
|
var shadowHover = darkMode
|
|
? "0 18px 38px rgba(0, 0, 0, 0.42)"
|
|
: "0 16px 34px rgba(15, 23, 42, 0.14)";
|
|
var wfShadow = darkMode
|
|
? "0 14px 34px rgba(0, 0, 0, 0.38)"
|
|
: "0 10px 28px rgba(20, 32, 55, 0.12)";
|
|
var wfShadowSoft = darkMode
|
|
? "0 10px 24px rgba(0, 0, 0, 0.3)"
|
|
: "0 6px 18px rgba(20, 32, 55, 0.1)";
|
|
|
|
root.style.setProperty("--ui-page-bg", next.baseColor);
|
|
root.style.setProperty("--ui-page-bg-dark", "#101318");
|
|
root.style.setProperty("--ui-card-opacity", surfaceAlpha.toFixed(2));
|
|
root.style.setProperty("--ui-card-blur", blur + "px");
|
|
root.style.setProperty("--ui-noise-opacity", "0");
|
|
root.style.setProperty("--bg-gradient", "none");
|
|
root.style.setProperty("--bg", darkMode ? "var(--ui-page-bg-dark)" : "var(--ui-page-bg)");
|
|
root.style.setProperty("--card-bg", cardBg);
|
|
root.style.setProperty("--surface-1", surface1);
|
|
root.style.setProperty("--surface-2", surface2);
|
|
root.style.setProperty("--surface-3", surface3);
|
|
root.style.setProperty("--surface-border", border);
|
|
root.style.setProperty("--input-border", inputBorder);
|
|
root.style.setProperty("--shadow", shadow);
|
|
root.style.setProperty("--shadow-hover", shadowHover);
|
|
|
|
root.style.setProperty("--wf-surface", cardBg);
|
|
root.style.setProperty("--wf-border", border);
|
|
root.style.setProperty("--wf-shadow", wfShadow);
|
|
root.style.setProperty("--wf-shadow-soft", wfShadowSoft);
|
|
}
|
|
|
|
function resolveBackgroundUrl(next) {
|
|
var globalUrl = sanitizeUrl(next.globalBackgroundUrl);
|
|
if (isLoginPage() && next.loginBackgroundMode === "custom") {
|
|
return sanitizeUrl(next.loginBackgroundUrl) || globalUrl;
|
|
}
|
|
return globalUrl;
|
|
}
|
|
|
|
function applyBackgroundLayers(next) {
|
|
if (!ensureLayers()) return;
|
|
var url = resolveBackgroundUrl(next);
|
|
if (url) {
|
|
layers.image.style.display = "block";
|
|
layers.image.style.backgroundImage = 'url("' + url.replace(/"/g, '\\"') + '")';
|
|
} else {
|
|
layers.image.style.display = "none";
|
|
layers.image.style.backgroundImage = "none";
|
|
}
|
|
}
|
|
|
|
function applyEffect(next) {
|
|
if (!ensureLayers()) return;
|
|
|
|
var style = next.effectStyle;
|
|
var intensity = clampNumber(next.effectIntensity, 0, 100);
|
|
var mobile = isMobileDevice();
|
|
var optimizedMobile = next.optimizeMobile && mobile;
|
|
|
|
if (style === "texture") {
|
|
var noiseBase = 0.06 + intensity / 100 * 0.16;
|
|
root.style.setProperty("--ui-noise-opacity", noiseBase.toFixed(3));
|
|
} else {
|
|
root.style.setProperty("--ui-noise-opacity", "0");
|
|
}
|
|
|
|
if (optimizedMobile && (style === "math" || style === "particle")) {
|
|
intensity = Math.max(6, Math.round(intensity * 0.45));
|
|
root.setAttribute("data-ui-mobile-optimized", "true");
|
|
} else {
|
|
root.removeAttribute("data-ui-mobile-optimized");
|
|
}
|
|
|
|
startRender(style, intensity, optimizedMobile);
|
|
}
|
|
|
|
function hideLegacyLoginLayers() {
|
|
if (!document.body) return;
|
|
document.body.classList.remove("has-bg-image");
|
|
var legacyImageLayer = document.getElementById("bgImageLayer");
|
|
var legacyOverlay = document.getElementById("bgOverlay");
|
|
if (legacyImageLayer) legacyImageLayer.style.display = "none";
|
|
if (legacyOverlay) legacyOverlay.style.display = "none";
|
|
}
|
|
|
|
function dispatchDesignChange(next, persisted) {
|
|
try {
|
|
window.dispatchEvent(
|
|
new CustomEvent("ui:design-change", {
|
|
detail: { settings: cloneSettings(next), persisted: !!persisted },
|
|
})
|
|
);
|
|
} catch (e) {}
|
|
}
|
|
|
|
function applySettings(next, options) {
|
|
var opts = options || {};
|
|
var normalized = normalizeSettings(next || settings || DEFAULTS);
|
|
var darkMode = root.getAttribute("data-theme") === "dark";
|
|
settings = normalized;
|
|
applyCompatibilityVars(settings, darkMode);
|
|
|
|
if (document.body) {
|
|
if (isLoginPage()) document.body.classList.add("login-page");
|
|
hideLegacyLoginLayers();
|
|
applyBackgroundLayers(settings);
|
|
applyEffect(settings);
|
|
}
|
|
|
|
if (opts.persist) {
|
|
saveLocalSettings(settings);
|
|
}
|
|
if (!opts.silent) {
|
|
dispatchDesignChange(settings, opts.persist);
|
|
}
|
|
return cloneSettings(settings);
|
|
}
|
|
|
|
function setSettings(partial, options) {
|
|
var opts = options || {};
|
|
var merged = Object.assign({}, settings || DEFAULTS, partial || {});
|
|
return applySettings(merged, {
|
|
persist: opts.persist !== false,
|
|
silent: !!opts.silent,
|
|
});
|
|
}
|
|
|
|
function previewSettings(partial) {
|
|
var merged = Object.assign({}, settings || DEFAULTS, partial || {});
|
|
return applySettings(merged, { persist: false, silent: true });
|
|
}
|
|
|
|
function resetSettings() {
|
|
var fresh = normalizeSettings(DEFAULTS);
|
|
saveLocalSettings(fresh);
|
|
return applySettings(fresh, { persist: false, silent: false });
|
|
}
|
|
|
|
function restorePersisted() {
|
|
var persisted = readSettings();
|
|
return applySettings(persisted, { persist: false, silent: true });
|
|
}
|
|
|
|
function clearBackgrounds(options) {
|
|
return setSettings(
|
|
{
|
|
globalBackgroundUrl: "",
|
|
loginBackgroundMode: "follow-global",
|
|
loginBackgroundUrl: "",
|
|
},
|
|
options
|
|
);
|
|
}
|
|
|
|
function bindGlobalListeners() {
|
|
window.addEventListener("theme:change", function () {
|
|
applySettings(settings, { persist: false, silent: true });
|
|
});
|
|
|
|
window.addEventListener("storage", function (event) {
|
|
if (event.key !== STORAGE_KEY) return;
|
|
settings = readSettings();
|
|
applySettings(settings, { persist: false, silent: true });
|
|
});
|
|
|
|
window.addEventListener("resize", function () {
|
|
if (!ensureLayers()) return;
|
|
ensureCanvasSize();
|
|
if (render.style === "math" || render.style === "particle") {
|
|
startRender(render.style, render.intensity, render.mobile);
|
|
}
|
|
});
|
|
|
|
document.addEventListener("visibilitychange", function () {
|
|
if (document.hidden) {
|
|
stopRender(false);
|
|
} else {
|
|
applyEffect(settings);
|
|
}
|
|
});
|
|
|
|
if (typeof window.matchMedia === "function") {
|
|
var media = window.matchMedia("(max-width: 768px)");
|
|
if (typeof media.addEventListener === "function") {
|
|
media.addEventListener("change", function () {
|
|
applyEffect(settings);
|
|
});
|
|
} else if (typeof media.addListener === "function") {
|
|
media.addListener(function () {
|
|
applyEffect(settings);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function init() {
|
|
if (!document.body) return;
|
|
if (isLoginPage()) document.body.classList.add("login-page");
|
|
ensureLayers();
|
|
applySettings(settings, { persist: false, silent: true });
|
|
}
|
|
|
|
var manager = {
|
|
getSettings: function () {
|
|
return cloneSettings(settings);
|
|
},
|
|
getDefaults: function () {
|
|
return cloneSettings(DEFAULTS);
|
|
},
|
|
setSettings: setSettings,
|
|
syncFromServer: syncFromServer,
|
|
saveToServer: saveToServer,
|
|
previewSettings: previewSettings,
|
|
restorePersisted: restorePersisted,
|
|
resetSettings: resetSettings,
|
|
clearBackgrounds: clearBackgrounds,
|
|
applySettings: function (next, options) {
|
|
return applySettings(next, options || {});
|
|
},
|
|
};
|
|
|
|
window.UIDesignManager = manager;
|
|
|
|
settings = migrateLegacySettings() || readSettings();
|
|
applySettings(settings, { persist: false, silent: true });
|
|
bindGlobalListeners();
|
|
syncFromServer({ silent: true, applyLocalOnFailure: false });
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", init, { once: true });
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|