Merge branch 'dev'

This commit is contained in:
OTKUSteyler 2026-01-05 17:07:46 +01:00
commit 90d4d47303
3 changed files with 213 additions and 123 deletions

View file

@ -4,7 +4,7 @@ import { readFile } from "fs/promises";
import http from "http";
import os from "os";
import readline from "readline";
import url from "url";
import { URL } from "url";
import yargs from "yargs-parser";
import { buildBundle } from "./build.mjs";
@ -15,7 +15,7 @@ const args = yargs(process.argv.slice(2));
export function serve(options) {
// @ts-ignore
const server = http.createServer(async (req, res) => {
const { pathname } = url.parse(req.url || "", true);
const { pathname } = new URL(req.url || "", `http://localhost:${args.port ?? 4040}`);
if (pathname?.endsWith(".js")) {
try {
let fileToServe;

View file

@ -11,147 +11,224 @@ const tokenRef = findByProps("SemanticColor");
export function parseColorManifest(manifest: ColorManifest): InternalColorDefinition {
const resolveType = (type = "dark") => (colorsPref.type ?? type) === "dark" ? "darker" : "light";
if (manifest.spec === 3) {
const semanticColorDefinitions: InternalColorDefinition["semantic"] = {};
if (manifest.spec === 3) {
const semanticColorDefinitions: InternalColorDefinition["semantic"] = {};
for (const [semanticColorKey, semanticColorValue] of Object.entries(
manifest.main.semantic ?? {},
)) {
if (typeof semanticColorValue === "object") {
const {
type,
value,
opacity: semanticColorOpacity,
} = semanticColorValue;
for (const [semanticColorKey, semanticColorValue] of Object.entries(manifest.main.semantic ?? {})) {
if (typeof semanticColorValue === "object") {
const { type, value, opacity: semanticColorOpacity } = semanticColorValue;
if (type === "raw") {
semanticColorDefinitions[semanticColorKey] = {
value,
opacity: semanticColorOpacity ?? 1,
};
} else {
const rawColorValue = tokenRef.RawColor[value];
semanticColorDefinitions[semanticColorKey] = {
value: rawColorValue,
opacity: semanticColorOpacity ?? 1,
};
if (type === "raw") {
semanticColorDefinitions[semanticColorKey] = {
value,
opacity: semanticColorOpacity ?? 1,
};
} else {
const rawColorValue = tokenRef.RawColor[value];
semanticColorDefinitions[semanticColorKey] = {
value: rawColorValue,
opacity: semanticColorOpacity ?? 1,
};
}
} else if (typeof semanticColorValue === "string") {
if (semanticColorValue.startsWith("#")) {
semanticColorDefinitions[semanticColorKey] = {
value: chroma(semanticColorValue).hex(),
opacity: 1,
};
} else {
semanticColorDefinitions[semanticColorKey] = {
value: tokenRef.RawColor[semanticColorValue],
opacity: 1,
};
}
} else {
throw new Error(`Invalid semantic definitions: ${semanticColorValue}`);
}
}
} else if (typeof semanticColorValue === "string") {
if (semanticColorValue.startsWith("#")) {
semanticColorDefinitions[semanticColorKey] = {
value: chroma.hex(semanticColorValue).hex(),
opacity: 1,
};
} else {
semanticColorDefinitions[semanticColorKey] = {
value: tokenRef.RawColor[semanticColorValue],
opacity: 1,
};
}
} else {
throw new Error(`Invalid semantic definitions: ${semanticColorValue}`);
}
}
if (Platform.OS === "android") applyAndroidAlphaKeys(manifest.main.raw);
if (Platform.OS === "android") applyAndroidAlphaKeys(manifest.main.raw);
return {
spec: 3,
reference: resolveType(manifest.type),
semantic: semanticColorDefinitions,
raw: manifest.main.raw ?? {},
background: manifest.main.background,
};
}
if (manifest.spec === 2) {
// is Vendetta theme
const semanticDefinitions: InternalColorDefinition["semantic"] = {};
const background: InternalColorDefinition["background"] | undefined =
manifest.background
? {
...omit(manifest.background, ["alpha"]),
opacity: manifest.background.alpha,
}
: undefined;
if (manifest.semanticColors) {
for (const key in manifest.semanticColors) {
const values = manifest.semanticColors[key]
.map((c) => c || undefined)
.slice(0, 2);
if (!values[0]) continue;
semanticDefinitions[key] = {
value: normalizeToHex(values[resolveType() === "light" ? 1 : 0])!,
opacity: 1,
return {
spec: 3,
reference: resolveType(manifest.type),
semantic: semanticColorDefinitions,
raw: manifest.main.raw ?? {},
background: manifest.main.background,
};
}
}
if (manifest.rawColors) {
const draft: typeof manifest.rawColors = {};
if (manifest.spec === 2) { // is Vendetta theme
const semanticDefinitions: InternalColorDefinition["semantic"] = {};
const background: InternalColorDefinition["background"] | undefined = manifest.background ? {
...omit(manifest.background, ["alpha"]),
opacity: manifest.background.alpha
} : undefined;
for (const key in manifest.rawColors) {
const value = manifest.rawColors[key];
if (!value) continue;
draft[key] = normalizeToHex(value)!;
}
if (manifest.semanticColors) {
for (const key in manifest.semanticColors) {
const values = manifest.semanticColors[key].map(c => c || undefined).slice(0, 2);
if (!values[0]) continue;
if (Platform.OS === "android") applyAndroidAlphaKeys(draft);
manifest.rawColors = draft;
semanticDefinitions[key] = {
value: normalizeToHex(values[resolveType() === "light" ? 1 : 0])!,
opacity: 1
};
}
}
if (manifest.rawColors) {
const draft: typeof manifest.rawColors = {};
for (const key in manifest.rawColors) {
const value = manifest.rawColors[key];
if (!value) continue;
draft[key] = normalizeToHex(value)!;
}
if (Platform.OS === "android") applyAndroidAlphaKeys(draft);
manifest.rawColors = draft;
}
return {
spec: 2,
reference: resolveType(),
semantic: semanticDefinitions,
raw: manifest.rawColors ?? {},
background
};
}
return {
spec: 2,
reference: resolveType(),
semantic: semanticDefinitions,
raw: manifest.rawColors ?? {},
background,
};
}
if (manifest.spec === 1 || (manifest.theme_color_map && !manifest.spec)) {
throw new Error("Invalid theme spec");
const semanticDefinitions: InternalColorDefinition["semantic"] = {};
const rawDefinitions: Record<string, string> = {};
const themeIndex = resolveType() === "light" ? 1 : 0;
if (manifest.theme_color_map) {
for (const [key, colorArray] of Object.entries(manifest.theme_color_map)) {
if (!Array.isArray(colorArray) || colorArray.length < 2) {
console.warn(`[Parser] Invalid color array for ${key}:`, colorArray);
continue;
}
const colorValue = colorArray[themeIndex];
if (!colorValue) {
console.warn(`[Parser] Missing color value for ${key} at index ${themeIndex}`);
continue;
}
const normalized = normalizeToHex(colorValue);
if (normalized) {
semanticDefinitions[key] = {
value: normalized,
opacity: 1
};
}
}
}
const rawColors = manifest.colours || manifest.colors;
if (rawColors) {
for (const [key, colorValue] of Object.entries(rawColors)) {
if (typeof colorValue !== "string") continue;
const normalized = normalizeToHex(colorValue);
if (normalized) {
const discordKey = convertEnmityToDiscordRawKey(key);
rawDefinitions[discordKey] = normalized;
if (discordKey !== key) {
rawDefinitions[key] = normalized;
}
}
}
}
if (manifest.unsafe_colors) {
for (const [key, colorValue] of Object.entries(manifest.unsafe_colors)) {
if (typeof colorValue !== "string") continue;
const normalized = normalizeToHex(colorValue);
if (normalized) {
rawDefinitions[key] = normalized;
}
}
}
if (Platform.OS === "android") applyAndroidAlphaKeys(rawDefinitions);
return {
spec: 2,
reference: resolveType(),
semantic: semanticDefinitions,
raw: rawDefinitions,
background: manifest.background
};
}
throw new Error("Invalid theme spec");
}
function convertEnmityToDiscordRawKey(enmityKey: string): string {
const conversions: Record<string, string> = {
"PRIMARY_DARK": "PRIMARY_100",
"PRIMARY_DARK_100": "PRIMARY_100",
"PRIMARY_DARK_200": "PRIMARY_200",
"PRIMARY_DARK_300": "PRIMARY_300",
"PRIMARY_DARK_360": "PRIMARY_360",
"PRIMARY_DARK_400": "PRIMARY_400",
"PRIMARY_DARK_500": "PRIMARY_500",
"PRIMARY_DARK_600": "PRIMARY_600",
"PRIMARY_DARK_630": "PRIMARY_630",
"PRIMARY_DARK_700": "PRIMARY_700",
"PRIMARY_DARK_800": "PRIMARY_800",
"PRIMARY_DARK_900": "PRIMARY_900",
};
return conversions[enmityKey] || enmityKey;
}
export function applyAndroidAlphaKeys(rawColors?: Record<string, string>) {
if (!rawColors) return;
if (!rawColors) return;
// these are native Discord Android keys
const alphaMap: Record<string, [string, number]> = {
BLACK_ALPHA_60: ["BLACK", 0.6],
BRAND_NEW_360_ALPHA_20: ["BRAND_360", 0.2],
BRAND_NEW_360_ALPHA_25: ["BRAND_360", 0.25],
BRAND_NEW_500_ALPHA_20: ["BRAND_500", 0.2],
PRIMARY_DARK_500_ALPHA_20: ["PRIMARY_500", 0.2],
PRIMARY_DARK_700_ALPHA_60: ["PRIMARY_700", 0.6],
STATUS_GREEN_500_ALPHA_20: ["GREEN_500", 0.2],
STATUS_RED_500_ALPHA_20: ["RED_500", 0.2],
};
// these are native Discord Android keys
const alphaMap: Record<string, [string, number]> = {
"BLACK_ALPHA_60": ["BLACK", 0.6],
"BRAND_NEW_360_ALPHA_20": ["BRAND_360", 0.2],
"BRAND_NEW_360_ALPHA_25": ["BRAND_360", 0.25],
"BRAND_NEW_500_ALPHA_20": ["BRAND_500", 0.2],
"PRIMARY_DARK_500_ALPHA_20": ["PRIMARY_500", 0.2],
"PRIMARY_DARK_700_ALPHA_60": ["PRIMARY_700", 0.6],
"STATUS_GREEN_500_ALPHA_20": ["GREEN_500", 0.2],
"STATUS_RED_500_ALPHA_20": ["RED_500", 0.2],
};
for (const key in alphaMap) {
const [colorKey, alpha] = alphaMap[key];
if (!rawColors[colorKey]) continue;
rawColors[key] = chroma(rawColors[colorKey]).alpha(alpha).hex();
}
for (const key in alphaMap) {
const [colorKey, alpha] = alphaMap[key];
if (!rawColors[colorKey]) continue;
rawColors[key] = chroma(rawColors[colorKey]).alpha(alpha).hex();
}
return rawColors;
return rawColors;
}
export function normalizeToHex(
colorString: string | undefined,
): string | undefined {
if (colorString === undefined) return undefined;
if (chroma.valid(colorString)) return chroma(colorString).hex();
export function normalizeToHex(colorString: string | undefined): string | undefined {
if (colorString === undefined) return undefined;
const color = Number(processColor(colorString));
if (colorString.toLowerCase() === "transparent") {
return "#00000000";
}
return chroma
.rgb(
(color >> 16) & 0xff, // red
(color >> 8) & 0xff, // green
color & 0xff, // blue
(color >> 24) & 0xff, // alpha
)
.hex();
if (chroma.valid(colorString)) return chroma(colorString).hex();
const color = Number(processColor(colorString));
return chroma.rgb(
color >> 16 & 0xff, // red
color >> 8 & 0xff, // green
color & 0xff, // blue
color >> 24 & 0xff // alpha
).hex();
}

View file

@ -37,6 +37,19 @@ export interface VendettaThemeManifest {
};
}
// enmity themes
export interface EnmityColorManifest {
spec?: 1;
name?: string;
description?: string;
authors?: Author[];
theme_color_map?: Record<string, string[]>;
colours?: Record<string, string>;
colors?: Record<string, string>;
unsafe_colors?: Record<string, string>;
background?: BackgroundDefinition;
}
/** @internal */
export interface InternalColorDefinition {
spec: 1 | 2 | 3;
@ -49,4 +62,4 @@ export interface InternalColorDefinition {
background?: BackgroundDefinition;
}
export type ColorManifest = BunnyColorManifest | VendettaThemeManifest;
export type ColorManifest = BunnyColorManifest | VendettaThemeManifest | EnmityColorManifest;