fix(queue): marquee long track names + dedupe now-playing bars
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user