allow easy custom branding in settings + fix some auth shit

This commit is contained in:
KingKDot
2026-07-17 20:09:47 -05:00
parent f404f22b29
commit 1cdeae0bc3
10 changed files with 559 additions and 2 deletions
+225 -1
View File
@@ -108,6 +108,20 @@ const appearanceForm = document.getElementById("appearance-form");
const appearancePermissionNote = document.getElementById("appearance-permission-note");
const appearanceSaveBtn = document.getElementById("appearance-save-btn");
const appearanceCustomCssInput = document.getElementById("appearance-custom-css");
const simpleThemeEnabledInput = document.getElementById("simple-theme-enabled");
const simpleThemeControls = document.getElementById("simple-theme-controls");
const simpleThemeAccentInput = document.getElementById("simple-theme-accent");
const simpleThemeBackgroundInput = document.getElementById("simple-theme-background");
const simpleThemeSurfaceInput = document.getElementById("simple-theme-surface");
const simpleThemeTextInput = document.getElementById("simple-theme-text");
const simpleThemeCornersInput = document.getElementById("simple-theme-corners");
const simpleThemeResetBtn = document.getElementById("simple-theme-reset");
const simpleThemePreview = document.getElementById("simple-theme-preview");
const simpleThemePreviewBar = document.getElementById("simple-theme-preview-bar");
const simpleThemePreviewBody = document.getElementById("simple-theme-preview-body");
const simpleThemePreviewDot = document.getElementById("simple-theme-preview-dot");
const simpleThemePreviewMuted = document.getElementById("simple-theme-preview-muted");
const simpleThemePreviewButton = document.getElementById("simple-theme-preview-button");
const brandProductNameInput = document.getElementById("brand-product-name");
const brandTabNameInput = document.getElementById("brand-tab-name");
const brandFaviconUrlInput = document.getElementById("brand-favicon-url");
@@ -1409,11 +1423,218 @@ const brandInputs = [
];
function setAppearanceFormDisabled(disabled) {
for (const input of [...brandInputs, appearanceCustomCssInput, appearanceSaveBtn]) {
const simpleThemeInputs = [
simpleThemeEnabledInput,
simpleThemeAccentInput,
simpleThemeBackgroundInput,
simpleThemeSurfaceInput,
simpleThemeTextInput,
simpleThemeCornersInput,
simpleThemeResetBtn,
];
for (const input of [...brandInputs, ...simpleThemeInputs, appearanceCustomCssInput, appearanceSaveBtn]) {
if (input) input.disabled = disabled;
}
}
const SIMPLE_THEME_START = "/* === OVERLORD SIMPLE THEME START === */";
const SIMPLE_THEME_END = "/* === OVERLORD SIMPLE THEME END === */";
const SIMPLE_THEME_BLOCK_RE = /\/\* === OVERLORD SIMPLE THEME START === \*\/[\s\S]*?\/\* === OVERLORD SIMPLE THEME END === \*\/\s*/;
const SIMPLE_THEME_DEFAULTS = Object.freeze({
accent: "#7a5bff",
background: "#020617",
surface: "#0f172a",
text: "#f1f5f9",
corners: "soft",
});
function normalizeThemeColor(value, fallback) {
return /^#[0-9a-f]{6}$/i.test(String(value || "")) ? String(value).toLowerCase() : fallback;
}
function mixThemeColors(colorA, colorB, amount) {
const parse = (color) => [1, 3, 5].map((offset) => Number.parseInt(color.slice(offset, offset + 2), 16));
const a = parse(colorA);
const b = parse(colorB);
return `#${a.map((channel, i) => Math.round(channel + ((b[i] - channel) * amount)).toString(16).padStart(2, "0")).join("")}`;
}
function themeColorRgba(color, alpha) {
const channels = [1, 3, 5].map((offset) => Number.parseInt(color.slice(offset, offset + 2), 16));
return `rgba(${channels.join(", ")}, ${alpha})`;
}
function getSimpleThemeSettings() {
return {
accent: normalizeThemeColor(simpleThemeAccentInput?.value, SIMPLE_THEME_DEFAULTS.accent),
background: normalizeThemeColor(simpleThemeBackgroundInput?.value, SIMPLE_THEME_DEFAULTS.background),
surface: normalizeThemeColor(simpleThemeSurfaceInput?.value, SIMPLE_THEME_DEFAULTS.surface),
text: normalizeThemeColor(simpleThemeTextInput?.value, SIMPLE_THEME_DEFAULTS.text),
corners: ["sharp", "soft", "round"].includes(simpleThemeCornersInput?.value)
? simpleThemeCornersInput.value
: SIMPLE_THEME_DEFAULTS.corners,
};
}
function applySimpleThemeSettings(settings = SIMPLE_THEME_DEFAULTS) {
const normalized = {
accent: normalizeThemeColor(settings.accent, SIMPLE_THEME_DEFAULTS.accent),
background: normalizeThemeColor(settings.background, SIMPLE_THEME_DEFAULTS.background),
surface: normalizeThemeColor(settings.surface, SIMPLE_THEME_DEFAULTS.surface),
text: normalizeThemeColor(settings.text, SIMPLE_THEME_DEFAULTS.text),
corners: ["sharp", "soft", "round"].includes(settings.corners) ? settings.corners : SIMPLE_THEME_DEFAULTS.corners,
};
if (simpleThemeAccentInput) simpleThemeAccentInput.value = normalized.accent;
if (simpleThemeBackgroundInput) simpleThemeBackgroundInput.value = normalized.background;
if (simpleThemeSurfaceInput) simpleThemeSurfaceInput.value = normalized.surface;
if (simpleThemeTextInput) simpleThemeTextInput.value = normalized.text;
if (simpleThemeCornersInput) simpleThemeCornersInput.value = normalized.corners;
updateSimpleThemePreview();
}
function buildSimpleThemeCss(settings) {
const { accent, background, surface, text, corners } = settings;
const accentShades = {
50: mixThemeColors(accent, "#ffffff", 0.92),
100: mixThemeColors(accent, "#ffffff", 0.82),
200: mixThemeColors(accent, "#ffffff", 0.68),
300: mixThemeColors(accent, "#ffffff", 0.48),
400: mixThemeColors(accent, "#ffffff", 0.24),
500: accent,
600: mixThemeColors(accent, "#000000", 0.15),
700: mixThemeColors(accent, "#000000", 0.3),
800: mixThemeColors(accent, "#000000", 0.45),
900: mixThemeColors(accent, "#000000", 0.58),
950: mixThemeColors(accent, "#000000", 0.72),
};
const neutralShades = {
50: mixThemeColors(text, "#ffffff", 0.35),
100: text,
200: mixThemeColors(surface, text, 0.86),
300: mixThemeColors(surface, text, 0.72),
400: mixThemeColors(surface, text, 0.55),
500: mixThemeColors(surface, text, 0.4),
600: mixThemeColors(surface, text, 0.26),
700: mixThemeColors(surface, text, 0.16),
800: mixThemeColors(surface, text, 0.08),
900: surface,
950: background,
};
const radii = {
sharp: ["2px", "3px", "4px", "5px"],
soft: ["8px", "10px", "14px", "18px"],
round: ["14px", "18px", "24px", "30px"],
}[corners];
const paletteNames = ["sky", "blue", "cyan", "indigo", "violet", "purple", "fuchsia"];
const accentVariables = paletteNames.flatMap((name) => (
Object.entries(accentShades).map(([shade, value]) => ` --color-${name}-${shade}: ${value};`)
));
const neutralVariables = Object.entries(neutralShades).map(([shade, value]) => ` --color-slate-${shade}: ${value};`);
return [
SIMPLE_THEME_START,
`/* settings: ${JSON.stringify(settings)} */`,
":root {",
...accentVariables,
...neutralVariables,
` --color-primary: ${accent};`,
` --color-primary-hover: ${accentShades[400]};`,
` --color-text: ${text};`,
` --color-text-bright: ${neutralShades[50]};`,
` --color-text-muted: ${neutralShades[400]};`,
` --color-surface: ${themeColorRgba(surface, 0.72)};`,
` --color-surface-dark: ${themeColorRgba(background, 0.88)};`,
` --color-border: ${themeColorRgba(text, 0.12)};`,
` --ui-border-strong: ${themeColorRgba(accent, 0.45)};`,
` --ui-ring: ${themeColorRgba(accent, 0.28)};`,
` --radius-sm: ${radii[0]};`,
` --radius-md: ${radii[1]};`,
` --radius-lg: ${radii[2]};`,
` --radius-xl: ${radii[3]};`,
` --radius-2xl: ${radii[3]};`,
"}",
"html, body {",
` background-color: ${background} !important;`,
` color: ${text};`,
"}",
SIMPLE_THEME_END,
].join("\n");
}
function replaceSimpleThemeBlock(css, replacement = "") {
const withoutManagedBlock = String(css || "").replace(SIMPLE_THEME_BLOCK_RE, "").trim();
return [replacement, withoutManagedBlock].filter(Boolean).join("\n\n");
}
function syncSimpleThemeCss() {
if (!appearanceCustomCssInput) return;
const enabled = !!simpleThemeEnabledInput?.checked;
const settings = getSimpleThemeSettings();
const replacement = enabled ? buildSimpleThemeCss(settings) : "";
if (enabled && brandAccentColorInput) brandAccentColorInput.value = settings.accent;
appearanceCustomCssInput.value = replaceSimpleThemeBlock(appearanceCustomCssInput.value, replacement);
if (simpleThemeControls) simpleThemeControls.classList.toggle("opacity-50", !enabled);
}
function loadSimpleThemeFromCss(css) {
const block = String(css || "").match(SIMPLE_THEME_BLOCK_RE)?.[0] || "";
if (simpleThemeEnabledInput) simpleThemeEnabledInput.checked = !!block;
if (block) {
const settingsText = block.match(/\/\* settings: (\{.*\}) \*\//)?.[1];
try {
applySimpleThemeSettings(settingsText ? JSON.parse(settingsText) : SIMPLE_THEME_DEFAULTS);
} catch {
applySimpleThemeSettings(SIMPLE_THEME_DEFAULTS);
}
} else {
applySimpleThemeSettings(SIMPLE_THEME_DEFAULTS);
}
if (simpleThemeControls) simpleThemeControls.classList.toggle("opacity-50", !block);
}
function updateSimpleThemePreview() {
const { accent, background, surface, text, corners } = getSimpleThemeSettings();
const radius = { sharp: "4px", soft: "12px", round: "22px" }[corners];
if (simpleThemePreview) {
simpleThemePreview.style.borderColor = mixThemeColors(surface, text, 0.2);
simpleThemePreview.style.borderRadius = radius;
}
if (simpleThemePreviewBar) {
simpleThemePreviewBar.style.backgroundColor = mixThemeColors(surface, background, 0.35);
simpleThemePreviewBar.style.color = text;
}
if (simpleThemePreviewBody) {
simpleThemePreviewBody.style.backgroundColor = surface;
simpleThemePreviewBody.style.color = text;
}
if (simpleThemePreviewDot) simpleThemePreviewDot.style.backgroundColor = accent;
if (simpleThemePreviewMuted) simpleThemePreviewMuted.style.color = mixThemeColors(surface, text, 0.6);
if (simpleThemePreviewButton) {
simpleThemePreviewButton.style.backgroundColor = accent;
simpleThemePreviewButton.style.borderRadius = radius;
}
}
function initSimpleThemeBuilder() {
const inputs = [simpleThemeAccentInput, simpleThemeBackgroundInput, simpleThemeSurfaceInput, simpleThemeTextInput, simpleThemeCornersInput];
for (const input of inputs) {
input?.addEventListener("input", () => {
if (simpleThemeEnabledInput && !simpleThemeEnabledInput.checked) simpleThemeEnabledInput.checked = true;
if (input === simpleThemeAccentInput && brandAccentColorInput) brandAccentColorInput.value = simpleThemeAccentInput.value;
updateSimpleThemePreview();
syncSimpleThemeCss();
});
}
simpleThemeEnabledInput?.addEventListener("change", syncSimpleThemeCss);
simpleThemeResetBtn?.addEventListener("click", () => {
applySimpleThemeSettings(SIMPLE_THEME_DEFAULTS);
if (simpleThemeEnabledInput) simpleThemeEnabledInput.checked = true;
if (brandAccentColorInput) brandAccentColorInput.value = SIMPLE_THEME_DEFAULTS.accent;
syncSimpleThemeCss();
});
updateSimpleThemePreview();
}
function applyBrandingForm(loginBranding = {}) {
if (brandProductNameInput) brandProductNameInput.value = loginBranding.productName || "Overlord";
if (brandTabNameInput) brandTabNameInput.value = loginBranding.tabName || loginBranding.productName || "Overlord";
@@ -1558,6 +1779,7 @@ async function loadAppearanceSettings() {
}
const data = await res.json().catch(() => ({}));
if (appearanceCustomCssInput) appearanceCustomCssInput.value = data.customCSS || "";
loadSimpleThemeFromCss(data.customCSS || "");
applyBrandingForm(data.loginBranding || {});
setAppearanceFormDisabled(false);
} catch {
@@ -1571,6 +1793,7 @@ async function saveAppearanceSettings(event) {
return;
}
syncSimpleThemeCss();
const customCSS = appearanceCustomCssInput ? appearanceCustomCssInput.value : "";
if (customCSS.length > 51200) {
showMessage("CSS exceeds the 50 KB size limit.", "error");
@@ -2720,6 +2943,7 @@ async function init() {
tlsCertbotAutoBtn.addEventListener("click", runCertbotAutoSetup);
if (oidcForm) oidcForm.addEventListener("submit", saveOidcSettings);
if (appearanceForm) appearanceForm.addEventListener("submit", saveAppearanceSettings);
initSimpleThemeBuilder();
initBrandingUploads();
if (chatSettingsForm) chatSettingsForm.addEventListener("submit", saveChatSettings);
if (inputArchiveUserForm) inputArchiveUserForm.addEventListener("submit", saveInputArchivePreference);
+61
View File
@@ -1405,6 +1405,67 @@
</label>
</div>
<fieldset class="rounded-xl border border-slate-700 bg-slate-950/50 p-4 space-y-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<h3 class="text-sm font-semibold text-slate-200">Simple theme</h3>
<p class="mt-1 text-xs text-slate-500">Pick the main colors and corner style. Overlord builds the CSS for you.</p>
</div>
<label class="inline-flex items-center gap-2 text-sm text-slate-300 cursor-pointer">
<input id="simple-theme-enabled" type="checkbox" class="h-4 w-4 rounded border-slate-600 bg-slate-900 text-fuchsia-500 focus:ring-fuchsia-500" />
Use simple theme
</label>
</div>
<div id="simple-theme-controls" class="grid grid-cols-2 lg:grid-cols-5 gap-3">
<label class="block">
<span class="text-xs text-slate-400">Accent</span>
<input id="simple-theme-accent" type="color" value="#7a5bff" class="mt-1 h-10 w-full px-1 py-1 rounded-lg bg-slate-900 border border-slate-700 cursor-pointer" />
</label>
<label class="block">
<span class="text-xs text-slate-400">Page background</span>
<input id="simple-theme-background" type="color" value="#020617" class="mt-1 h-10 w-full px-1 py-1 rounded-lg bg-slate-900 border border-slate-700 cursor-pointer" />
</label>
<label class="block">
<span class="text-xs text-slate-400">Cards &amp; panels</span>
<input id="simple-theme-surface" type="color" value="#0f172a" class="mt-1 h-10 w-full px-1 py-1 rounded-lg bg-slate-900 border border-slate-700 cursor-pointer" />
</label>
<label class="block">
<span class="text-xs text-slate-400">Text</span>
<input id="simple-theme-text" type="color" value="#f1f5f9" class="mt-1 h-10 w-full px-1 py-1 rounded-lg bg-slate-900 border border-slate-700 cursor-pointer" />
</label>
<label class="block col-span-2 lg:col-span-1">
<span class="text-xs text-slate-400">Corners</span>
<select id="simple-theme-corners" class="mt-1 h-10 w-full px-3 rounded-lg bg-slate-900 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-fuchsia-500 text-sm">
<option value="sharp">Sharp</option>
<option value="soft" selected>Soft</option>
<option value="round">Round</option>
</select>
</label>
</div>
<div id="simple-theme-preview" class="overflow-hidden rounded-xl border border-slate-700">
<div id="simple-theme-preview-bar" class="flex items-center gap-2 px-3 py-2">
<span id="simple-theme-preview-dot" class="h-2.5 w-2.5 rounded-full"></span>
<span class="text-xs font-medium">Theme preview</span>
</div>
<div id="simple-theme-preview-body" class="flex items-center justify-between gap-3 p-3">
<div>
<div class="text-sm font-semibold">Cards, text, and buttons</div>
<div id="simple-theme-preview-muted" class="mt-0.5 text-xs">Your custom palette is shown here.</div>
</div>
<button id="simple-theme-preview-button" type="button" tabindex="-1" class="px-3 py-1.5 text-xs font-medium text-white">Primary</button>
</div>
</div>
<div class="flex flex-wrap items-center gap-2">
<button id="simple-theme-reset" type="button" class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-slate-800 hover:bg-slate-700 border border-slate-700 text-xs text-slate-200 transition-colors">
<i class="fa-solid fa-rotate-left"></i>Reset colors
</button>
<p class="text-xs text-slate-500">Changes are added to the managed block below. Your handwritten CSS is preserved.</p>
</div>
</fieldset>
<label class="block">
<span class="text-sm text-slate-300">Custom CSS</span>
<textarea