diff --git a/biome.json b/biome.json index 921a7a5..fa06b32 100644 --- a/biome.json +++ b/biome.json @@ -17,6 +17,12 @@ "organizeImports": { "enabled": true }, + "css": { + "formatter": { + "indentStyle": "tab", + "lineEnding": "lf" + } + }, "linter": { "enabled": true, "rules": { diff --git a/public/css/index.css b/public/css/index.css index 1d8ad00..ac5f70f 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -1,34 +1,3 @@ -:root { - --background: #0e0e10; - --card-bg: #1e1f22; - --card-hover-bg: #2a2a2d; - --border-color: #2e2e30; - - --text-color: #ffffff; - --text-subtle: #bbb; - --text-secondary: #b5bac1; - --text-muted: #888; - --link-color: #00b0f4; - - --status-online: #23a55a; - --status-idle: #f0b232; - --status-dnd: #e03e3e; - --status-offline: #747f8d; - --status-streaming: #b700ff; - - --progress-bg: #f23f43; - --progress-fill: #5865f2; - - --button-bg: #5865f2; - --button-hover-bg: #4752c4; - --button-disabled-bg: #2d2e31; - - --blockquote-color: #aaa; - --code-bg: #2e2e30; - - --readme-bg: #1a1a1d; -} - body { font-family: system-ui, sans-serif; background-color: var(--background); diff --git a/src/helpers/colors.ts b/src/helpers/colors.ts new file mode 100644 index 0000000..43a74e7 --- /dev/null +++ b/src/helpers/colors.ts @@ -0,0 +1,49 @@ +import { fetch } from "bun"; +import { Vibrant } from "node-vibrant/node"; + +export async function getImageColors( + url: string, + hex?: boolean, +): Promise { + if (!url) return null; + + if (typeof url !== "string" || !url.startsWith("http")) return null; + + let res: Response; + try { + res = await fetch(url); + } catch { + return null; + } + + if (!res.ok) return null; + + const type: string | null = res.headers.get("content-type"); + if (!type?.startsWith("image/")) return null; + + const buffer: Buffer = Buffer.from(await res.arrayBuffer()); + const base64: string = buffer.toString("base64"); + const colors: Palette = await Vibrant.from(buffer).getPalette(); + + return { + img: `data:${type};base64,${base64}`, + colors: hex + ? { + Muted: rgbToHex(safeRgb(colors.Muted)), + LightVibrant: rgbToHex(safeRgb(colors.LightVibrant)), + Vibrant: rgbToHex(safeRgb(colors.Vibrant)), + LightMuted: rgbToHex(safeRgb(colors.LightMuted)), + DarkVibrant: rgbToHex(safeRgb(colors.DarkVibrant)), + DarkMuted: rgbToHex(safeRgb(colors.DarkMuted)), + } + : colors, + }; +} + +function safeRgb(swatch: Swatch | null | undefined): number[] { + return Array.isArray(swatch?.rgb) ? (swatch.rgb ?? [0, 0, 0]) : [0, 0, 0]; +} + +export function rgbToHex(rgb: number[]): string { + return `#${rgb.map((c) => Math.round(c).toString(16).padStart(2, "0")).join("")}`; +} diff --git a/src/routes/api/colors.ts b/src/routes/api/colors.ts index 9e05bd3..ef97012 100644 --- a/src/routes/api/colors.ts +++ b/src/routes/api/colors.ts @@ -1,7 +1,4 @@ -import { fetch } from "bun"; -import { Vibrant } from "node-vibrant/node"; - -type Palette = Awaited>; +import { getImageColors } from "@helpers/colors"; const routeDef: RouteDef = { method: "GET", @@ -12,46 +9,21 @@ const routeDef: RouteDef = { async function handler(request: ExtendedRequest): Promise { const { url } = request.query; - if (!url) { - return Response.json({ error: "URL is required" }, { status: 400 }); + const result: ImageColorResult | null = await getImageColors(url, true); + await getImageColors(url); + + if (!result) { + return new Response("Invalid URL", { + status: 400, + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Access-Control-Allow-Origin": "*", + }, + }); } - if (typeof url !== "string" || !url.startsWith("http")) { - return Response.json({ error: "Invalid URL" }, { status: 400 }); - } - - let res: Response; - try { - res = await fetch(url); - } catch { - return Response.json({ error: "Failed to fetch image" }, { status: 500 }); - } - - if (!res.ok) { - return Response.json( - { error: "Image fetch returned error" }, - { status: res.status }, - ); - } - - const type: string | null = res.headers.get("content-type"); - if (!type?.startsWith("image/")) { - return Response.json({ error: "Not an image" }, { status: 400 }); - } - - const buffer: Buffer = Buffer.from(await res.arrayBuffer()); - const base64: string = buffer.toString("base64"); - const colors: Palette = await Vibrant.from(buffer).getPalette(); - - const payload: { - img: string; - colors: Palette; - } = { - img: `data:${type};base64,${base64}`, - colors, - }; - - const compressed: Uint8Array = Bun.gzipSync(JSON.stringify(payload)); + const compressed: Uint8Array = Bun.gzipSync(JSON.stringify(result)); return new Response(compressed, { headers: { diff --git a/src/routes/index.ts b/src/routes/index.ts index a15fa81..b0d6a61 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,3 +1,4 @@ +import { getImageColors } from "@/helpers/colors"; import { lanyardConfig } from "@config/environment"; import { renderEjsTemplate } from "@helpers/ejs"; import { getLanyardData, handleReadMe } from "@helpers/lanyard"; @@ -37,6 +38,14 @@ async function handler(): Promise { status = presence.discord_status; } + let colors: ImageColorResult | null = null; + if (presence.kv.colors === "true") { + const avatar: string = presence.discord_user.avatar + ? `https://cdn.discordapp.com/avatars/${presence.discord_user.id}/${presence.discord_user.avatar}` + : `https://cdn.discordapp.com/embed/avatars/${presence.discord_user.discriminator || 1 % 5}`; + colors = await getImageColors(avatar, true); + } + const ejsTemplateData: EjsTemplateData = { title: presence.discord_user.global_name || presence.discord_user.username, username: @@ -53,6 +62,7 @@ async function handler(): Promise { readme, allowSnow: presence.kv.snow === "true", allowRain: presence.kv.rain === "true", + colors: colors?.colors ?? {}, }; return await renderEjsTemplate("index", ejsTemplateData); diff --git a/src/views/index.ejs b/src/views/index.ejs index 77fadd4..0db4f4b 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -24,6 +24,8 @@ +<%- include('partial/style.ejs') %> +
diff --git a/src/views/partial/style.ejs b/src/views/partial/style.ejs new file mode 100644 index 0000000..2f50cdf --- /dev/null +++ b/src/views/partial/style.ejs @@ -0,0 +1,32 @@ + diff --git a/types/routes.d.ts b/types/routes.d.ts index 9d9d809..6af6a4b 100644 --- a/types/routes.d.ts +++ b/types/routes.d.ts @@ -13,3 +13,10 @@ type RouteModule = { ) => Promise | Response; routeDef: RouteDef; }; + +type Palette = Awaited>; +type Swatch = Awaited>; +type ImageColorResult = { + img: string; + colors: Palette | Record; +};