add option to use vibrant colors from avatar needs moving around

This commit is contained in:
creations 2025-04-10 07:09:10 -04:00
parent 30e9057ba8
commit ff0ece9626
Signed by: creations
GPG key ID: 8F553AA4320FC711
8 changed files with 120 additions and 73 deletions

View file

@ -17,6 +17,12 @@
"organizeImports": {
"enabled": true
},
"css": {
"formatter": {
"indentStyle": "tab",
"lineEnding": "lf"
}
},
"linter": {
"enabled": true,
"rules": {

View file

@ -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);

49
src/helpers/colors.ts Normal file
View file

@ -0,0 +1,49 @@
import { fetch } from "bun";
import { Vibrant } from "node-vibrant/node";
export async function getImageColors(
url: string,
hex?: boolean,
): Promise<ImageColorResult | null> {
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("")}`;
}

View file

@ -1,7 +1,4 @@
import { fetch } from "bun";
import { Vibrant } from "node-vibrant/node";
type Palette = Awaited<ReturnType<typeof Vibrant.prototype.getPalette>>;
import { getImageColors } from "@helpers/colors";
const routeDef: RouteDef = {
method: "GET",
@ -12,46 +9,21 @@ const routeDef: RouteDef = {
async function handler(request: ExtendedRequest): Promise<Response> {
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: {

View file

@ -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<Response> {
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<Response> {
readme,
allowSnow: presence.kv.snow === "true",
allowRain: presence.kv.rain === "true",
colors: colors?.colors ?? {},
};
return await renderEjsTemplate("index", ejsTemplateData);

View file

@ -24,6 +24,8 @@
<meta name="color-scheme" content="dark">
</head>
<%- include('partial/style.ejs') %>
<body>
<div class="user-card">
<div class="avatar-status-wrapper">

View file

@ -0,0 +1,32 @@
<style>
:root {
/* --background: #0e0e10; */
--background: <%= colors.DarkVibrant || '#0e0e10' %>;
--readme-bg: <%= colors.DarkMuted || '#1a1a1d' %>;
--card-bg: <%= colors.DarkMuted || '#1e1f22' %>;
--card-hover-bg: <%= colors.Muted || '#2a2a2d' %>;
--border-color: <%= colors.Muted || '#2e2e30' %>;
--text-color: #ffffff;
--text-subtle: #bbb;
--text-secondary: #b5bac1;
--text-muted: #888;
--link-color: <%= colors.Vibrant || '#00b0f4' %>;
--button-bg: <%= colors.Vibrant || '#5865f2' %>;
--button-hover-bg: <%= colors.LightVibrant || '#4752c4' %>;
--button-disabled-bg: #2d2e31;
--progress-bg: <%= colors.DarkVibrant || '#f23f43' %>;
--progress-fill: <%= colors.Vibrant || '#5865f2' %>;
--status-online: #23a55a;
--status-idle: #f0b232;
--status-dnd: #e03e3e;
--status-offline: #747f8d;
--status-streaming: #b700ff;
--blockquote-color: #aaa;
--code-bg: <%= colors.Muted || '#2e2e30' %>;
}
</style>

7
types/routes.d.ts vendored
View file

@ -13,3 +13,10 @@ type RouteModule = {
) => Promise<Response> | Response;
routeDef: RouteDef;
};
type Palette = Awaited<ReturnType<typeof Vibrant.prototype.getPalette>>;
type Swatch = Awaited<ReturnType<typeof Vibrant.prototype.getSwatches>>;
type ImageColorResult = {
img: string;
colors: Palette | Record<string, string>;
};