iceshrimp/packages/client/src/scripts/theme.ts

173 lines
3.9 KiB
TypeScript
Raw Normal View History

2023-01-13 13:40:33 +09:00
import { ref } from "vue";
import tinycolor from "tinycolor2";
import { globalEvents } from "@/events";
export type Theme = {
id: string;
name: string;
author: string;
desc?: string;
2023-01-13 13:40:33 +09:00
base?: "dark" | "light";
2020-10-19 13:17:11 +09:00
props: Record<string, string>;
};
2023-01-13 13:40:33 +09:00
import lightTheme from "@/themes/_light.json5";
import darkTheme from "@/themes/_dark.json5";
import { deepClone } from "./clone";
export const themeProps = Object.keys(lightTheme.props).filter(
(key) => !key.startsWith("X"),
2022-05-28 21:59:23 +09:00
);
2023-01-13 13:40:33 +09:00
export const getBuiltinThemes = () =>
Promise.all(
[
"l-rosepinedawn",
"l-light",
2023-03-20 10:44:08 +09:00
"l-nord",
"l-gruvbox",
2023-01-13 13:40:33 +09:00
"l-coffee",
"l-apricot",
"l-rainy",
"l-vivid",
"l-cherry",
"l-sushi",
"l-u0",
"d-rosepine",
"d-rosepinemoon",
"d-dark",
2023-03-20 10:44:08 +09:00
"d-nord",
"d-gruvbox",
"d-catppuccin-frappe",
"d-catppuccin-mocha",
2023-01-13 13:40:33 +09:00
"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,
),
),
);
2022-05-28 21:59:23 +09:00
export const getBuiltinThemesRef = () => {
const builtinThemes = ref<Theme[]>([]);
2023-01-13 13:40:33 +09:00
getBuiltinThemes().then((themes) => (builtinThemes.value = themes));
2022-05-28 21:59:23 +09:00
return builtinThemes;
};
let timeout = null;
export function applyTheme(theme: Theme, persist = true) {
2022-01-16 10:14:14 +09:00
if (timeout) window.clearTimeout(timeout);
2023-01-13 13:40:33 +09:00
document.documentElement.classList.add("_themeChanging_");
2022-01-16 10:14:14 +09:00
timeout = window.setTimeout(() => {
2023-01-13 13:40:33 +09:00
document.documentElement.classList.remove("_themeChanging_");
}, 1000);
2023-01-13 13:40:33 +09:00
const colorSchema = theme.base === "dark" ? "dark" : "light";
// Deep copy
const _theme = deepClone(theme);
if (_theme.base) {
2023-01-13 13:40:33 +09:00
const base = [lightTheme, darkTheme].find((x) => x.id === _theme.base);
2022-04-03 13:56:00 +09:00
if (base) _theme.props = Object.assign({}, base.props, _theme.props);
}
const props = compile(_theme);
for (const tag of document.head.children) {
2023-01-13 13:40:33 +09:00
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());
}
2023-01-13 13:40:33 +09:00
document.documentElement.style.setProperty("color-schema", colorSchema);
if (persist) {
2023-01-13 13:40:33 +09:00
localStorage.setItem("theme", JSON.stringify(props));
localStorage.setItem("colorSchema", colorSchema);
}
2021-10-11 00:36:47 +09:00
2022-12-15 05:17:06 +09:00
// Site-wide notification that the theme has changed
2023-01-13 13:40:33 +09:00
globalEvents.emit("themeChanged");
}
2020-03-29 16:09:44 +09:00
function compile(theme: Theme): Record<string, string> {
function getColor(val: string): tinycolor.Instance {
// ref (prop)
2023-01-13 13:40:33 +09:00
if (val[0] === "@") {
2020-03-29 16:09:44 +09:00
return getColor(theme.props[val.substr(1)]);
}
// ref (const)
2023-01-13 13:40:33 +09:00
else if (val[0] === "$") {
2020-03-29 16:09:44 +09:00
return getColor(theme.props[val]);
}
// func
2023-01-13 13:40:33 +09:00
else if (val[0] === ":") {
const parts = val.split("<");
const func = parts.shift().substr(1);
const arg = parseFloat(parts.shift());
2023-01-13 13:40:33 +09:00
const color = getColor(parts.join("<"));
switch (func) {
2023-01-13 13:40:33 +09:00
case "darken":
return color.darken(arg);
case "lighten":
return color.lighten(arg);
case "alpha":
return color.setAlpha(arg);
case "hue":
return color.spin(arg);
case "saturate":
return color.saturate(arg);
}
}
2020-03-29 16:09:44 +09:00
// other case
return tinycolor(val);
}
const props = {};
for (const [k, v] of Object.entries(theme.props)) {
2023-01-13 13:40:33 +09:00
if (k.startsWith("$")) continue; // ignore const
2020-03-29 16:09:44 +09:00
2023-01-13 13:40:33 +09:00
props[k] = v.startsWith('"')
? v.replace(/^"\s*/, "")
: genValue(getColor(v));
}
return props;
}
function genValue(c: tinycolor.Instance): string {
return c.toRgbString();
}
export function validateTheme(theme: Record<string, any>): boolean {
2023-01-13 13:40:33 +09:00
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;
}