This commit is contained in:
2026-08-06 10:38:07 -07:00
parent ac80cd1db4
commit 97a3f1be48
29 changed files with 1008 additions and 115 deletions
+30 -5
View File
@@ -174,7 +174,10 @@ function useSongQueue(preview: boolean, language: string, stream?: ComponentStre
const itemId = String(payload.itemId ?? '')
const item = normalizeSongRequestItem(payload.item)
const current = normalizeSongRequestItem(payload.current)
let queued = previous.queued.filter(entry => entry.id !== itemId && entry.id !== current?.id)
let queued =
operation === 'cleared'
? []
: previous.queued.filter(entry => entry.id !== itemId && entry.id !== current?.id)
if (item?.status === 'queued') queued.push(item)
if (operation === 'promoted' && item) {
queued = queued.map(entry =>
@@ -229,6 +232,9 @@ function useBounceScroll(
let direction = 1
let last = performance.now()
let pausedUntil = last + pauseSeconds * 1000
// OBS can ship an older Chromium that rounds scrollTop to whole pixels.
// Keep fractional movement here so low configured speeds still advance.
let pendingPixels = 0
const resize = new ResizeObserver(() => {
container.scrollTop = Math.min(
@@ -244,14 +250,21 @@ function useBounceScroll(
last = now
if (maximum === 0) container.scrollTop = 0
else if (now >= pausedUntil) {
container.scrollTop += direction * Math.max(5, speed) * (elapsed / 1000)
pendingPixels += Math.max(5, speed) * (elapsed / 1000)
const wholePixels = Math.floor(pendingPixels)
if (wholePixels > 0) {
container.scrollTop += direction * wholePixels
pendingPixels -= wholePixels
}
if (container.scrollTop >= maximum - 0.5) {
container.scrollTop = maximum
direction = -1
pendingPixels = 0
pausedUntil = now + pauseSeconds * 1000
} else if (container.scrollTop <= 0.5) {
container.scrollTop = 0
direction = 1
pendingPixels = 0
pausedUntil = now + pauseSeconds * 1000
}
}
@@ -309,8 +322,12 @@ export function SongRequestOverlay({
{
...themeCssVariables(theme),
...typographyVariables(settings.fontFamily, settings.fontBrightness),
['--component-song-requester-color' as string]: settings.requesterColor || undefined,
['--component-song-title-color' as string]: settings.songTitleColor || undefined,
['--song-font-size' as string]: `${Math.min(22, Math.max(11, Math.min(bounds.width, bounds.height) * 0.024)) * (settings.fontScale / 100)}px`,
['--song-current-min' as string]: `${Math.min(72, Math.max(48, bounds.height * 0.08))}px`,
['--song-moonlit-font-size' as string]: `${Math.min(26, Math.max(14, Math.min(bounds.width * 0.045, bounds.height * 0.032))) * (settings.fontScale / 100)}px`,
['--song-moonlit-current-min' as string]: `${Math.min(118, Math.max(74, bounds.height * 0.135))}px`,
} as CSSProperties
}
>
@@ -321,7 +338,8 @@ export function SongRequestOverlay({
>
<SongParticles theme={theme} />
<div className="song-current-mark" aria-hidden="true">
{translate('song.overlay.sing_mark')}
<span className="song-current-mark-label">{translate('song.overlay.sing_mark')}</span>
<span className="song-current-mark-note">♫</span>
</div>
{queue.current ? (
<div className="song-current-copy">
@@ -339,6 +357,11 @@ export function SongRequestOverlay({
<div className="song-score">
<b>{score(queue.current)}</b>
<small>{translate('song.overlay.rate_help')}</small>
<div className="song-waveform" aria-hidden="true">
{Array.from({ length: 8 }, (_, index) => (
<i key={index} />
))}
</div>
</div>
</section>
<section className="song-queue" aria-label={translate('song.overlay.queue_aria')}>
@@ -355,8 +378,10 @@ export function SongRequestOverlay({
style={{ ['--song-row-delay' as string]: `${Math.min(index, 6) * 35}ms` }}
>
<span className="song-index">{String(index + 1).padStart(2, '0')}</span>
<strong>{item.title}</strong>
<span className="song-requester">{item.requester.name}</span>
<div className="song-row-copy">
<span className="song-requester">{item.requester.name}</span>
<strong>{item.title}</strong>
</div>
</article>
))}
{queue.queued.length === 0 && (