ghp / packages /client /src /components /GlobalBackground.tsx
QSLY's picture
deploy: build Hugging Face Space from source
00a912e
Raw
History Blame Contribute Delete
4.53 kB
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(() => {
// The default theme remains usable if a server is temporarily unavailable.
})
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}
/>
)
}