fix(queue): marquee long track names + dedupe now-playing bars
Docker Build & Publish / build (push) Has been cancelled
Docker Build & Publish / push (push) Has been cancelled
Docker Build & Publish / Prune old image versions (push) Has been cancelled

Queue sidebar no longer scrolls horizontally on long titles/artists:
text now ping-pong scrolls (news-ticker style) only when it overflows,
via a new Marquee component; .qd-scroll also clips overflow-x.

The current track previously showed the playing-bars indicator both in
place of the drag grip and over the cover. Keep only the cover overlay
and restore the drag grip on the current row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Senko-san
2026-06-14 02:28:27 +03:00
parent b966ad8be5
commit cdcacc56d1
3 changed files with 81 additions and 21 deletions
+39
View File
@@ -0,0 +1,39 @@
import { useLayoutEffect, useRef, useState, type CSSProperties } from 'react';
/** Single-line text that ping-pong scrolls (like a news ticker) only when it
* overflows its container, otherwise renders as static clipped text. Keeps the
* queue panel from ever growing a horizontal scrollbar on long titles. */
export function Marquee({
text,
className,
}: {
text: string;
className?: string;
}) {
const ref = useRef<HTMLSpanElement>(null);
const [shift, setShift] = useState(0);
useLayoutEffect(() => {
const el = ref.current;
if (!el) return;
const measure = () => {
const inner = el.firstElementChild as HTMLElement | null;
const overflow = (inner?.scrollWidth ?? 0) - el.clientWidth;
setShift(overflow > 1 ? overflow : 0);
};
measure();
const ro = new ResizeObserver(measure);
ro.observe(el);
return () => ro.disconnect();
}, [text]);
return (
<span
ref={ref}
className={`marquee${shift ? ' on' : ''}${className ? ` ${className}` : ''}`}
style={shift ? ({ '--mq-shift': `-${shift}px` } as CSSProperties) : undefined}
>
<span className="marquee-inner">{text}</span>
</span>
);
}
+6 -9
View File
@@ -26,6 +26,7 @@ import {
import { CSS } from '@dnd-kit/utilities';
import { Icon } from '../common/Icon';
import { ArtTile } from '../common/ArtTile';
import { Marquee } from '../common/Marquee';
import { PlayingIndicator } from '../common/PlayingIndicator';
import { useAppDispatch, useAppSelector } from '../../hooks/useAppDispatch';
import {
@@ -259,13 +260,9 @@ function QueueRow({
onDoubleClick={onPlay}
title={t('queue.doubleClickPlay')}
>
{isCurrent ? (
<PlayingIndicator animate={isPlaying} />
) : (
<span className="grip" {...attributes} {...listeners}>
<Icon name="dots-six-vertical" />
</span>
)}
<span className="grip" {...attributes} {...listeners}>
<Icon name="dots-six-vertical" />
</span>
<div className="qart">
<ArtTile seed={albumTitle} size={36} label={albumTitle} src={artUrl} />
{isCurrent && (
@@ -275,8 +272,8 @@ function QueueRow({
)}
</div>
<div className="qt">
<div className="t">{resolved?.title ?? entry.title}</div>
<div className="r">{resolved?.artistName ?? entry.artistName}</div>
<Marquee className="t" text={resolved?.title ?? entry.title} />
<Marquee className="r" text={resolved?.artistName ?? entry.artistName} />
</div>
<Menu>
<MenuTrigger asChild>
+36 -12
View File
@@ -651,6 +651,7 @@
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 12px 12px 18px;
}
.qrow {
@@ -700,23 +701,46 @@
font-size: 13px;
font-weight: 500;
color: var(--fg-1);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.qrow .qt .r {
font-size: 11px;
color: var(--fg-3);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
gap: 4px;
}
.qrow .qt .r .ph {
color: var(--lime);
font-size: 11px;
/* News-ticker text: clips by default, ping-pong scrolls only when it overflows
(the .on class is set by the Marquee component after measuring). */
.marquee {
display: block;
max-width: 100%;
overflow: hidden;
white-space: nowrap;
}
.marquee-inner {
display: inline-block;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 100%;
overflow: hidden;
vertical-align: bottom;
}
.marquee.on .marquee-inner {
max-width: none;
animation: marquee-pingpong 9s ease-in-out infinite alternate;
}
@keyframes marquee-pingpong {
0%,
12% {
transform: translateX(0);
}
88%,
100% {
transform: translateX(var(--mq-shift, 0));
}
}
@media (prefers-reduced-motion: reduce) {
.marquee.on .marquee-inner {
animation: none;
}
}
.qd-radio {
margin-bottom: 14px;