jormungandr-bite/packages/client/src/scripts/theme.ts

236 lines
6 KiB
TypeScript

import { ref } from "vue";
import tinycolor from "tinycolor2";
import { globalEvents } from "@/events";
export type Theme = {
id: string;
name: string;
author: string;
desc?: string;
base?: "dark" | "light";
props: Record<string, string>;
};
import lightTheme from "@/themes/_light.json5";
import darkTheme from "@/themes/_dark.json5";
import { deepClone } from "./clone";
import { defaultStore } from "@/store";
export const themeProps = Object.keys(lightTheme.props).filter(
(key) => !key.startsWith("X"),
);
export const getBuiltinThemes = () =>
Promise.all(
[
"l-iceshrimp",
"l-rosepinedawn",
"l-light",
"l-nord",
"l-gruvbox",
"l-catppuccin-latte-blue",
"l-catppuccin-latte-flamingo",
"l-catppuccin-latte-green",
"l-catppuccin-latte-lavender",
"l-catppuccin-latte-maroon",
"l-catppuccin-latte-mauve",
"l-catppuccin-latte-peach",
"l-catppuccin-latte-pink",
"l-catppuccin-latte-red",
"l-catppuccin-latte-rosewater",
"l-catppuccin-latte-sapphire",
"l-catppuccin-latte-sky",
"l-catppuccin-latte-teal",
"l-catppuccin-latte-yellow",
"l-coffee",
"l-apricot",
"l-rainy",
"l-vivid",
"l-cherry",
"l-sushi",
"l-u0",
"d-iceshrimp",
"d-rosepine",
"d-rosepinemoon",
"d-dark",
"d-nord",
"d-gruvbox",
"d-catppuccin-frappe-blue",
"d-catppuccin-frappe-flamingo",
"d-catppuccin-frappe-green",
"d-catppuccin-frappe-lavender",
"d-catppuccin-frappe-maroon",
"d-catppuccin-frappe-mauve",
"d-catppuccin-frappe-peach",
"d-catppuccin-frappe-pink",
"d-catppuccin-frappe-red",
"d-catppuccin-frappe-rosewater",
"d-catppuccin-frappe-sapphire",
"d-catppuccin-frappe-sky",
"d-catppuccin-frappe-teal",
"d-catppuccin-frappe-yellow",
"d-catppuccin-mocha-blue",
"d-catppuccin-mocha-flamingo",
"d-catppuccin-mocha-green",
"d-catppuccin-mocha-lavender",
"d-catppuccin-mocha-maroon",
"d-catppuccin-mocha-mauve",
"d-catppuccin-mocha-peach",
"d-catppuccin-mocha-pink",
"d-catppuccin-mocha-red",
"d-catppuccin-mocha-rosewater",
"d-catppuccin-mocha-sapphire",
"d-catppuccin-mocha-sky",
"d-catppuccin-mocha-teal",
"d-catppuccin-mocha-yellow",
"d-catppuccin-macchiato-blue",
"d-catppuccin-macchiato-flamingo",
"d-catppuccin-macchiato-green",
"d-catppuccin-macchiato-lavender",
"d-catppuccin-macchiato-maroon",
"d-catppuccin-macchiato-mauve",
"d-catppuccin-macchiato-peach",
"d-catppuccin-macchiato-pink",
"d-catppuccin-macchiato-red",
"d-catppuccin-macchiato-rosewater",
"d-catppuccin-macchiato-sapphire",
"d-catppuccin-macchiato-sky",
"d-catppuccin-macchiato-teal",
"d-catppuccin-macchiato-yellow",
"d-persimmon",
"d-astro",
"d-future",
"d-botanical",
"d-green-lime",
"d-green-orange",
"d-cherry",
"d-ice",
"d-u0"
].map((name) =>
import(`../themes/${name}.json5`).then(
({ default: _default }): Theme => _default,
),
),
);
export const getBuiltinThemesRef = () => {
const builtinThemes = ref<Theme[]>([]);
getBuiltinThemes().then((themes) => (builtinThemes.value = themes));
return builtinThemes;
};
let timeout = null;
export function applyTheme(theme: Theme, persist = true) {
if (timeout) window.clearTimeout(timeout);
document.documentElement.classList.add("_themeChanging_");
timeout = window.setTimeout(() => {
document.documentElement.classList.remove("_themeChanging_");
}, 1000);
const colorSchema = theme.base === "dark" ? "dark" : "light";
// Deep copy
const _theme = deepClone(theme);
if (_theme.base) {
const base = [lightTheme, darkTheme].find((x) => x.id === _theme.base);
if (base) _theme.props = Object.assign({}, base.props, _theme.props);
}
const props = compile(_theme);
for (const tag of document.head.children) {
if (tag.tagName === "META" && tag.getAttribute("name") === "theme-color") {
tag.setAttribute("content", props["htmlThemeColor"]);
break;
}
}
for (const [k, v] of Object.entries(props)) {
document.documentElement.style.setProperty(`--${k}`, v.toString());
}
document.documentElement.style.setProperty("color-schema", colorSchema);
if (persist) {
localStorage.setItem("theme", JSON.stringify(props));
localStorage.setItem("colorSchema", colorSchema);
}
// Site-wide notification that the theme has changed
globalEvents.emit("themeChanged");
}
function compile(theme: Theme): Record<string, string> {
function getColor(val: string, key?: string): tinycolor.Instance {
// ref (prop)
if (val[0] === "@") {
return getColor(theme.props[val.slice(1)]);
}
// ref (const)
else if (val[0] === "$") {
return getColor(theme.props[val]);
}
// func
else if (val[0] === ":") {
const parts = val.split("<");
const func = parts.shift().slice(1);
const arg = parseFloat(parts.shift());
const color = getColor(parts.join("<"));
const ignoreAlphaForKeys = ["windowHeader", "acrylicPanel", "pageHeader"];
switch (func) {
case "darken":
return color.darken(arg);
case "lighten":
return color.lighten(arg);
case "alpha":
if (!defaultStore.state.useBlurEffect && key && ignoreAlphaForKeys.includes(key)) {
return color.setAlpha(1.0);
}
else {
return color.setAlpha(arg);
}
case "hue":
return color.spin(arg);
case "saturate":
return color.saturate(arg);
}
}
// other case
return tinycolor(val);
}
const props = {};
for (const [k, v] of Object.entries(theme.props)) {
if (k.startsWith("$")) continue; // ignore const
props[k] = v.startsWith('"')
? v.replace(/^"\s*/, "")
: genValue(getColor(v, k));
}
return props;
}
function genValue(c: tinycolor.Instance): string {
return c.toRgbString();
}
export function validateTheme(theme: Record<string, any>): boolean {
if (theme.id == null || typeof theme.id !== "string") return false;
if (theme.name == null || typeof theme.name !== "string") return false;
if (theme.base == null || !["light", "dark"].includes(theme.base))
return false;
if (theme.props == null || typeof theme.props !== "object") return false;
return true;
}