a simple web player for subsonic tinysub.devins.page
subsonic navidrome javascript
9
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 90 lines 2.5 kB view raw
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};