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 { useTranslation } from 'react-i18next';
import { Icon } from '../common/Icon'; import { Icon } from '../common/Icon';
import { ArtTile } from '../common/ArtTile'; import { ArtTile } from '../common/ArtTile';
@@ -7,6 +15,7 @@ import { useAppDispatch, useAppSelector } from '../../hooks/useAppDispatch';
import { import {
goToIndex, goToIndex,
removeFromQueue, removeFromQueue,
moveInQueue,
clearQueue, clearQueue,
type QueueEntry, type QueueEntry,
} from '../../store/slices/queue'; } from '../../store/slices/queue';
@@ -112,6 +121,11 @@ export function QueuePanel() {
isCurrent={index === queue.currentIndex} isCurrent={index === queue.currentIndex}
isPlaying={isPlaying} isPlaying={isPlaying}
onPlay={() => dispatch(goToIndex(index))} onPlay={() => dispatch(goToIndex(index))}
onMoveNext={() =>
dispatch(
moveInQueue({ from: index, to: queue.currentIndex + 1 }),
)
}
onRemove={() => dispatch(removeFromQueue(index))} onRemove={() => dispatch(removeFromQueue(index))}
/> />
))} ))}
@@ -137,12 +151,14 @@ function QueueRow({
isCurrent, isCurrent,
isPlaying, isPlaying,
onPlay, onPlay,
onMoveNext,
onRemove, onRemove,
}: { }: {
entry: QueueEntry; entry: QueueEntry;
isCurrent: boolean; isCurrent: boolean;
isPlaying: boolean; isPlaying: boolean;
onPlay: () => void; onPlay: () => void;
onMoveNext: () => void;
onRemove: () => void; onRemove: () => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -174,24 +190,29 @@ function QueueRow({
<div className="t">{resolved?.title ?? entry.title}</div> <div className="t">{resolved?.title ?? entry.title}</div>
<div className="r">{resolved?.artistName ?? entry.artistName}</div> <div className="r">{resolved?.artistName ?? entry.artistName}</div>
</div> </div>
{isCurrent && ( <Menu>
<button <MenuTrigger asChild>
type="button" <IconButton
className="iconbtn sm" variant="ghost"
onClick={() => dispatch(openTrackInfo(entry.trackId))} size="sm"
title={t('trackInfo.open')} 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 <MenuItem onSelect={() => dispatch(openTrackInfo(entry.trackId))}>
type="button" {t('queue.menu.info')}
className="iconbtn sm" </MenuItem>
onClick={onRemove} <MenuItem onSelect={onRemove}>{t('queue.menu.remove')}</MenuItem>
title={t('queue.removeFromQueue')} </MenuContent>
> </Menu>
<Icon name="x" />
</button>
</div> </div>
); );
} }
+7
View File
@@ -147,6 +147,13 @@ const en = {
loadingMore: 'Loading more from radio…', loadingMore: 'Loading more from radio…',
doubleClickPlay: 'Double-click to play', doubleClickPlay: 'Double-click to play',
removeFromQueue: 'Remove from queue', removeFromQueue: 'Remove from queue',
menu: {
options: 'Track options',
playNow: 'Play now',
moveNext: 'Move next',
info: 'Track info',
remove: 'Remove from queue',
},
}, },
track: { track: {
menu: { menu: {
+7
View File
@@ -149,6 +149,13 @@ const ru: Translations = {
loadingMore: 'Загрузка радио…', loadingMore: 'Загрузка радио…',
doubleClickPlay: 'Двойной клик для воспроизведения', doubleClickPlay: 'Двойной клик для воспроизведения',
removeFromQueue: 'Убрать из очереди', removeFromQueue: 'Убрать из очереди',
menu: {
options: 'Параметры трека',
playNow: 'Воспроизвести сейчас',
moveNext: 'Сделать следующим',
info: 'Информация о треке',
remove: 'Убрать из очереди',
},
}, },
track: { track: {
menu: { menu: {