Something went wrong. Try again.
a simple but full-featured web player for OpenSubsonic music servers (Navidrome, Gonic, Ampache, etc) devins.page/tinysub
svelte typescript javascript subsonic gonic navidrome
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138import { 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<string, { bg: string; text: string; dark: boolean }>();
let colorThiefPromise: Promise<typeof import("colorthief")> | 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 = ""; }; }); });};