a simple web player for subsonic
tinysub.devins.page
subsonic
navidrome
javascript
1import { api } from "./client.svelte.js";
2import { player } from "./player.svelte.js";
3import { settings } from "./settings.svelte.js";
4import { getSwatches, getColor } from "colorthief";
5
6const getLuminance = (rgb: number[]) => {
7 const [rs, gs, bs] = rgb.map((v) => {
8 const val = v / 255;
9 return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4);
10 });
11 return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
12};
13
14const getContrast = (c1: number[], c2: number[]) => {
15 const l1 = getLuminance(c1) + 0.05;
16 const l2 = getLuminance(c2) + 0.05;
17 return l1 > l2 ? l1 / l2 : l2 / l1;
18};
19
20const cache = new Map<string, { bg: string; text: string; dark: boolean }>();
21
22const apply = (bg: string, text: string, dark: boolean) => {
23 const style = document.documentElement.style;
24 style.setProperty("--bg", bg);
25 style.setProperty("--text", text);
26 document.documentElement.style.colorScheme = dark ? "dark" : "light";
27};
28
29const clear = () => {
30 const style = document.documentElement.style;
31 style.removeProperty("--bg");
32 style.removeProperty("--text");
33 document.documentElement.style.colorScheme = "";
34};
35
36export const initTheme = () => {
37 $effect.root(() => {
38 $effect(() => {
39 const track = player.track;
40 if (!settings.dynamicColors || !track) return clear();
41
42 const id = track.albumId || track.coverArt || track.id;
43 const cached = cache.get(id);
44 if (cached) return apply(cached.bg, cached.text, cached.dark);
45
46 const img = new Image();
47 img.crossOrigin = "Anonymous";
48 img.src = api.art(id, 512);
49 img.onload = async () => {
50 if (!settings.dynamicColors || player.track?.id !== track.id) return;
51 try {
52 const dominant = await getColor(img);
53 const swatches = await getSwatches(img);
54 if (!dominant) return;
55
56 const bg = dominant.array();
57 let text: number[] | null = null;
58
59 for (const role of [
60 "Vibrant",
61 "Muted",
62 "DarkVibrant",
63 "DarkMuted",
64 "LightVibrant",
65 "LightMuted",
66 ] as const) {
67 const swatch = swatches[role];
68 if (swatch && getContrast(bg, swatch.color.array()) >= 4.5) {
69 text = swatch.color.array();
70 break;
71 }
72 }
73
74 if (!text) text = dominant.contrast.foreground.array();
75
76 const theme = {
77 bg: `rgb(${bg[0]},${bg[1]},${bg[2]})`,
78 text: `rgb(${text[0]},${text[1]},${text[2]})`,
79 dark: dominant.isDark,
80 };
81
82 cache.set(id, theme);
83 apply(theme.bg, theme.text, theme.dark);
84 } catch (e) {
85 console.error("Theme fail", e);
86 }
87 };
88 });
89 });
90};