import { getSourceApi, songArtId } from "./client.svelte.js"; import { offlineImages } from "./offline/offlineManager.svelte.js"; import { player } from "./player.svelte.js"; import { settings } from "./settings.svelte.js"; import { sourceStorageKey, sources } from "./sources.svelte.js"; const cache = new Map(); let colorThiefPromise: Promise | null = null; const darkMQ = window.matchMedia("(prefers-color-scheme: dark)"); let themeMeta: HTMLMetaElement | null = null; export const getDominantColor = async (image: HTMLImageElement) => { if (!colorThiefPromise) { colorThiefPromise = import("colorthief").catch((err: unknown) => { colorThiefPromise = null; throw err; }); } const { getColor } = await colorThiefPromise; const dominant = await getColor(image, { ignoreWhite: false, }); if (!dominant) throw new Error("unable to extract image color"); return dominant; }; const resolveCanvasColor = (): string => { const temp = document.createElement("div"); temp.style.display = "none"; temp.style.colorScheme = darkMQ.matches ? "dark" : "light"; temp.style.color = "Canvas"; document.documentElement.appendChild(temp); const color = getComputedStyle(temp).color; temp.remove(); return color; }; const updateMeta = (color?: string) => { if (!themeMeta) { themeMeta = document.createElement("meta"); themeMeta.name = "theme-color"; document.head.appendChild(themeMeta); darkMQ.addEventListener("change", () => updateMeta( document.documentElement.style.getPropertyValue("--bg") || undefined, ), ); } themeMeta.content = color ?? resolveCanvasColor(); }; const applyTheme = (bg: string, text: string, dark: boolean) => { if (!settings.dynamicColors) return clearTheme(); const style = document.documentElement.style; style.setProperty("--bg", bg); style.setProperty("--text", text); style.colorScheme = dark ? "dark" : "light"; updateMeta(bg); }; const clearTheme = () => { const style = document.documentElement.style; style.removeProperty("--bg"); style.removeProperty("--text"); style.colorScheme = ""; updateMeta(); }; export const initTheme = () => { $effect.root(() => { $effect(() => { const track = player.track; if (!settings.dynamicColors || !track) return clearTheme(); const sourceId = track.sourceId || sources.activeId; if (!sourceId) return clearTheme(); const id = songArtId(track); const cacheKey = sourceStorageKey(sourceId, id); const cached = cache.get(cacheKey); if (cached) return applyTheme(cached.bg, cached.text, cached.dark); const coverUrl = getSourceApi(sourceId).getCoverArt(id, 512); if (!coverUrl) { if (offlineImages.has(cacheKey)) return; return clearTheme(); } const img = new Image(); if (!coverUrl.startsWith("blob:")) img.crossOrigin = "anonymous"; img.src = coverUrl; img.onload = async () => { img.onload = null; img.onerror = null; if ( !settings.dynamicColors || player.track?.id !== track.id || player.track?.sourceId !== track.sourceId ) return; try { const dominant = await getDominantColor(img); const theme = { bg: dominant.css(), text: dominant.textColor, dark: dominant.isDark, }; cache.set(cacheKey, theme); if ( player.track?.id === track.id && player.track?.sourceId === track.sourceId ) applyTheme(theme.bg, theme.text, theme.dark); } catch {} }; img.onerror = () => { img.onload = null; img.onerror = null; if ( player.track?.id === track.id && player.track?.sourceId === track.sourceId ) { clearTheme(); } }; return () => { img.onload = null; img.onerror = null; img.src = ""; }; }); }); };