feat(queue): add per-track overflow menu in queue panel
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

Replace the bare "remove" cross on each queue row with a ghost
three-dot menu offering Play now, Move next (reposition right after
the current track), Track info, and Remove — consolidating the
previously separate info button into the same menu.
This commit is contained in:
Senko-san
2026-06-13 17:37:17 +03:00
parent 5c8f89675d
commit 9c70b8a11f
3 changed files with 54 additions and 19 deletions
+38 -17
View File
@@ -1,4 +1,12 @@
import { Slider, Badge } from '@olly/modern-sk';
import {
Slider,
Badge,
Menu,
MenuTrigger,
MenuContent,
MenuItem,
IconButton,
} from '@olly/modern-sk';
import { useTranslation } from 'react-i18next';
import { Icon } from '../common/Icon';
import { ArtTile } from '../common/ArtTile';
@@ -7,6 +15,7 @@ import { useAppDispatch, useAppSelector } from '../../hooks/useAppDispatch';
import {
goToIndex,
removeFromQueue,
moveInQueue,
clearQueue,
type QueueEntry,
} from '../../store/slices/queue';
@@ -112,6 +121,11 @@ export function QueuePanel() {
isCurrent={index === queue.currentIndex}
isPlaying={isPlaying}
onPlay={() => dispatch(goToIndex(index))}
onMoveNext={() =>
dispatch(
moveInQueue({ from: index, to: queue.currentIndex + 1 }),
)
}
onRemove={() => dispatch(removeFromQueue(index))}
/>
))}
@@ -137,12 +151,14 @@ function QueueRow({
isCurrent,
isPlaying,
onPlay,
onMoveNext,
onRemove,
}: {
entry: QueueEntry;
isCurrent: boolean;
isPlaying: boolean;
onPlay: () => void;
onMoveNext: () => void;
onRemove: () => void;
}) {
const { t } = useTranslation();
@@ -174,24 +190,29 @@ function QueueRow({
<div className="t">{resolved?.title ?? entry.title}</div>
<div className="r">{resolved?.artistName ?? entry.artistName}</div>
</div>
{isCurrent && (
<button
type="button"
className="iconbtn sm"
onClick={() => dispatch(openTrackInfo(entry.trackId))}
title={t('trackInfo.open')}
<Menu>
<MenuTrigger asChild>
<IconButton
variant="ghost"
size="sm"
aria-label={t('queue.menu.options')}
>
<Icon name="info" />
</button>
</IconButton>
</MenuTrigger>
<MenuContent>
<MenuItem onSelect={onPlay}>{t('queue.menu.playNow')}</MenuItem>
{!isCurrent && (
<MenuItem onSelect={onMoveNext}>
{t('queue.menu.moveNext')}
</MenuItem>
)}
<button
type="button"
className="iconbtn sm"
onClick={onRemove}
title={t('queue.removeFromQueue')}
>
<Icon name="x" />
</button>
<MenuItem onSelect={() => dispatch(openTrackInfo(entry.trackId))}>
{t('queue.menu.info')}
</MenuItem>
<MenuItem onSelect={onRemove}>{t('queue.menu.remove')}</MenuItem>
</MenuContent>
</Menu>
</div>
);
}
+7
View File
@@ -147,6 +147,13 @@ const en = {
loadingMore: 'Loading more from radio…',
doubleClickPlay: 'Double-click to play',
removeFromQueue: 'Remove from queue',
menu: {
options: 'Track options',
playNow: 'Play now',
moveNext: 'Move next',
info: 'Track info',
remove: 'Remove from queue',
},
},
track: {
menu: {
+7
View File
@@ -149,6 +149,13 @@ const ru: Translations = {
loadingMore: 'Загрузка радио…',
doubleClickPlay: 'Двойной клик для воспроизведения',
removeFromQueue: 'Убрать из очереди',
menu: {
options: 'Параметры трека',
playNow: 'Воспроизвести сейчас',
moveNext: 'Сделать следующим',
info: 'Информация о треке',
remove: 'Убрать из очереди',
},
},
track: {
menu: {