| import { useEffect, useState, type CSSProperties } from 'react' |
| import { EVENTS, type GlobalBackgroundSettings } from '@music-together/shared' |
| import { SERVER_URL } from '@/lib/config' |
| import { useSocketEvent } from '@/hooks/useSocketEvent' |
| import { useGlobalBackgroundStore } from '@/stores/globalBackgroundStore' |
|
|
| function resolveBackgroundUrl(backgroundUrl: string | null): string | undefined { |
| if (!backgroundUrl) return undefined |
| return backgroundUrl.startsWith('/uploads/') ? `${SERVER_URL}${backgroundUrl}` : backgroundUrl |
| } |
|
|
| export function GlobalBackground() { |
| const backgroundUrl = useGlobalBackgroundStore((state) => state.backgroundUrl) |
| const glassOverlay = useGlobalBackgroundStore((state) => state.glassOverlay) |
| const colorPreset = useGlobalBackgroundStore((state) => state.colorPreset) |
| const backgroundBrightness = useGlobalBackgroundStore((state) => state.backgroundBrightness) |
| const autoTint = useGlobalBackgroundStore((state) => state.autoTint) |
| const setBackgroundUrl = useGlobalBackgroundStore((state) => state.setBackgroundUrl) |
| const setGlassOverlay = useGlobalBackgroundStore((state) => state.setGlassOverlay) |
| const setColorPreset = useGlobalBackgroundStore((state) => state.setColorPreset) |
| const setBackgroundBrightness = useGlobalBackgroundStore((state) => state.setBackgroundBrightness) |
| const setAutoTint = useGlobalBackgroundStore((state) => state.setAutoTint) |
| const [tint, setTint] = useState<string | null>(null) |
|
|
| useEffect(() => { |
| let cancelled = false |
| void fetch(`${SERVER_URL}/api/settings/background`, { credentials: 'include' }) |
| .then(async (response) => { |
| if (!response.ok) throw new Error(`Request failed: ${response.status}`) |
| return (await response.json()) as GlobalBackgroundSettings |
| }) |
| .then((settings) => { |
| if (!cancelled) { |
| setBackgroundUrl(settings.backgroundUrl) |
| setGlassOverlay(settings.glassOverlay) |
| setColorPreset(settings.colorPreset) |
| setBackgroundBrightness(settings.backgroundBrightness) |
| setAutoTint(settings.autoTint) |
| } |
| }) |
| .catch(() => { |
| |
| }) |
| return () => { |
| cancelled = true |
| } |
| }, [setBackgroundUrl, setGlassOverlay, setColorPreset, setBackgroundBrightness, setAutoTint]) |
|
|
| useSocketEvent(EVENTS.SERVER_GLOBAL_BACKGROUND, (settings) => { |
| setBackgroundUrl(settings.backgroundUrl) |
| setGlassOverlay(settings.glassOverlay) |
| setColorPreset(settings.colorPreset) |
| setBackgroundBrightness(settings.backgroundBrightness) |
| setAutoTint(settings.autoTint) |
| }) |
|
|
| useEffect(() => { |
| document.documentElement.dataset.colorPreset = colorPreset |
| }, [colorPreset]) |
|
|
| const resolvedUrl = resolveBackgroundUrl(backgroundUrl) |
|
|
| useEffect(() => { |
| if (!autoTint || !resolvedUrl) { |
| setTint(null) |
| return |
| } |
|
|
| let cancelled = false |
| const image = new Image() |
| image.crossOrigin = 'anonymous' |
| image.onload = () => { |
| try { |
| const canvas = document.createElement('canvas') |
| canvas.width = 32 |
| canvas.height = 32 |
| const context = canvas.getContext('2d', { willReadFrequently: true }) |
| if (!context) return |
| context.drawImage(image, 0, 0, 32, 32) |
| const pixels = context.getImageData(0, 0, 32, 32).data |
| let red = 0 |
| let green = 0 |
| let blue = 0 |
| let count = 0 |
| for (let index = 0; index < pixels.length; index += 4) { |
| if (pixels[index + 3] < 32) continue |
| red += pixels[index]! |
| green += pixels[index + 1]! |
| blue += pixels[index + 2]! |
| count += 1 |
| } |
| if (!cancelled && count > 0) { |
| setTint(`rgb(${Math.round(red / count)} ${Math.round(green / count)} ${Math.round(blue / count)})`) |
| } |
| } catch { |
| if (!cancelled) setTint(null) |
| } |
| } |
| image.onerror = () => { |
| if (!cancelled) setTint(null) |
| } |
| image.src = resolvedUrl |
| return () => { |
| cancelled = true |
| } |
| }, [autoTint, resolvedUrl]) |
|
|
| if (!resolvedUrl) return null |
|
|
| return ( |
| <div |
| className={glassOverlay ? 'mt-global-background mt-global-background-glass' : 'mt-global-background'} |
| aria-hidden="true" |
| style={{ |
| backgroundImage: `url("${resolvedUrl}")`, |
| '--mt-background-brightness': backgroundBrightness / 100, |
| '--mt-auto-tint': tint ?? 'transparent', |
| } as CSSProperties} |
| /> |
| ) |
| } |
|
|