feat(player-bar): configurable stop button, album line and reorderable actions (#1287)

* feat(player-bar): configurable stop button, album line and reorderable actions

Extends the existing player-bar layout store instead of adding a second one.

- 'stop' becomes a layout item, so the stop button can be hidden. It lives in a
  'transport' zone: visibility only, no reordering -- the play button is a
  centred special case and shuffling controls around it would fight the adaptive
  small-window layout. Hiding it leaves no dead end, since the primary button
  already acts as stop while previewing.
- Track info gains an optional album line under the artist, off by default and
  suppressed for radio and previews, which have no album.
- The right-hand action buttons are drag-reorderable via the shared
  useListReorderDnd primitive. Moves resolve by stable id against the full list,
  so the zone filter that decides which rows render cannot desync the reorder.
- The section is no longer gated behind Advanced.

Rehydrate keeps older stored layouts working: items added later ('stop') are
appended with their default instead of silently disappearing.

* i18n(settings): player bar constructor strings in all 14 locales

* docs(changelog): add entry and credit for PR #1287
This commit is contained in:
Psychotoxical
2026-07-13 21:29:46 +02:00
committed by GitHub
parent dac7bf56d9
commit d0edd925e4
23 changed files with 352 additions and 41 deletions
+7
View File
@@ -142,6 +142,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* While a Navidrome public share queue is active, **Save Playlist** is hidden in the queue toolbar (share tracks cannot be saved to the server). **Load Playlist** stays available.
* The queue **Share** button copies the original Navidrome `/share/{id}` page URL instead of a Psysonic magic-string queue payload.
### Player bar — build your own
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1287](https://github.com/Psychotoxical/psysonic/pull/1287)**
* **Settings → Personalisation → Player bar** now also hides the **stop button** and shows the **album name** under the artist (off by default; clicking it opens the album). The right-hand buttons — star rating, favorite, love, playback speed, equalizer, mini player — can be **dragged into any order** you like.
* The section is no longer behind **Advanced**.
## Fixed
### Per-track covers when playing from a playlist
+1
View File
@@ -202,6 +202,7 @@ const CONTRIBUTOR_ENTRIES = [
'Windows MSI bundle on dev/RC versions — numeric WiX mapping; album easter-egg import chunk (PR #1278)',
'Track lists — optional album cover thumbnails via standard cover pipeline; queue rows use playback scope (PR #1280)',
'Internet Radio — Web Audio EQ on HTML5 streams; presets apply without reconnect (PR #1284)',
'Player bar — hideable stop button, optional album line, drag-reorderable action buttons (request: mikmik on Psysonic Discord, PR #1287)',
],
},
{
@@ -73,6 +73,11 @@ export function PlayerTrackInfo({
const layoutItems = usePlayerBarLayoutStore(s => s.items);
const isLayoutVisible = (id: PlayerBarLayoutItemId) =>
layoutItems.find(i => i.id === id)?.visible !== false;
const trackInfoMode = usePlayerBarLayoutStore(s => s.trackInfoMode);
// Radio has no album, and a preview shows the previewed track's own meta.
const albumLine = trackInfoMode === 'titleAlbum' && !isRadio && !showPreviewMeta
? currentTrack?.album
: undefined;
const offlineBrowseActive = useOfflineBrowseContext().active;
const playerPolicy = offlineActionPolicy('playerBar', offlineBrowseActive);
@@ -177,6 +182,14 @@ export function PlayerTrackInfo({
onClick={() => !isRadio && !showPreviewMeta && currentTrack?.artistId && navigate(`/artist/${currentTrack.artistId}`)}
/>
)}
{albumLine && (
<MarqueeText
text={albumLine}
className="player-track-album"
style={{ cursor: currentTrack?.albumId ? 'pointer' : 'default' }}
onClick={() => currentTrack?.albumId && navigate(`/album/${currentTrack.albumId}`)}
/>
)}
{currentTrack && !isRadio && !showPreviewMeta && isLayoutVisible('starRating') && playerPolicy.canRate && (
<StarRating
value={userRatingOverrides[currentTrack.id] ?? currentTrack.userRating ?? 0}
@@ -8,6 +8,7 @@ import { usePreviewStore } from '@/features/playback/store/previewStore';
import PlaybackScheduleBadge from '@/features/playback/components/PlaybackScheduleBadge';
import { usePlaybackDelayPress } from '@/features/playback/hooks/usePlaybackDelayPress';
import { usePlaybackScheduleRemaining } from '@/features/playback/utils/playbackScheduleFormat';
import { usePlayerBarLayoutStore } from '@/features/playback/store/playerBarLayoutStore';
type RepeatMode = PlayerState['repeatMode'];
type PlayPauseBind = ReturnType<typeof usePlaybackDelayPress>['playPauseBind'];
@@ -36,24 +37,31 @@ export function PlayerTransportControls({
const autodjPhase = useAutodjTransitionUi(s => s.phase);
const showAutodjTransition =
isPlaying && !isPreviewing && scheduleRemaining == null && autodjPhase === 'mixing';
// Hiding Stop leaves no dead end: while previewing, the primary button already
// renders as a stop control and ends the preview.
const showStop = usePlayerBarLayoutStore(
s => s.items.find(i => i.id === 'stop')?.visible !== false,
);
return (
<div className="player-buttons" ref={transportAnchorRef}>
<button
className="player-btn player-btn-sm"
onClick={() => {
if (isPreviewing) {
usePreviewStore.setState({ previewingId: null, previewingTrack: null, elapsed: 0 });
audioPreviewStopSilent().catch(() => {});
} else {
stop();
}
}}
aria-label={isPreviewing ? t('playlists.previewStop') : t('player.stop')}
data-tooltip={isPreviewing ? t('playlists.previewStop') : t('player.stop')}
>
<Square size={14} fill="currentColor" />
</button>
{showStop && (
<button
className="player-btn player-btn-sm"
onClick={() => {
if (isPreviewing) {
usePreviewStore.setState({ previewingId: null, previewingTrack: null, elapsed: 0 });
audioPreviewStopSilent().catch(() => {});
} else {
stop();
}
}}
aria-label={isPreviewing ? t('playlists.previewStop') : t('player.stop')}
data-tooltip={isPreviewing ? t('playlists.previewStop') : t('player.stop')}
>
<Square size={14} fill="currentColor" />
</button>
)}
<button
className="player-btn"
onClick={() => previous()}
@@ -1,35 +1,107 @@
import { describe, expect, it, beforeEach } from 'vitest';
import {
DEFAULT_PLAYER_BAR_LAYOUT_ITEMS,
DEFAULT_PLAYER_BAR_TRACK_INFO_MODE,
PLAYER_BAR_LAYOUT_ZONES,
usePlayerBarLayoutStore,
type PlayerBarLayoutItemConfig,
} from '@/features/playback/store/playerBarLayoutStore';
type State = ReturnType<typeof usePlayerBarLayoutStore.getState>;
/**
* Drives the persist middleware's rehydrate hook against a stored snapshot.
* That hook is the risky path: a layout persisted by an older version knows
* nothing about items added later, and a corrupted entry must not be able to
* drop a button or leave the track-info picker on an unrenderable value.
*/
function rehydrate(stored: Partial<Record<'items' | 'trackInfoMode', unknown>>): State {
const state = { ...usePlayerBarLayoutStore.getState(), ...stored } as State;
usePlayerBarLayoutStore.persist.getOptions().onRehydrateStorage?.(state)?.(state, undefined);
return state;
}
const ids = (items: PlayerBarLayoutItemConfig[]) => items.map(i => i.id);
describe('playerBarLayoutStore', () => {
beforeEach(() => {
usePlayerBarLayoutStore.getState().reset();
});
it('starts with all six items visible in declared order', () => {
it('starts with every item visible in declared order', () => {
const items = usePlayerBarLayoutStore.getState().items;
expect(items.map(i => i.id)).toEqual([
'starRating', 'favorite', 'lastfmLove', 'playbackRate', 'equalizer', 'miniPlayer',
expect(ids(items)).toEqual([
'stop', 'starRating', 'favorite', 'lastfmLove', 'playbackRate', 'equalizer', 'miniPlayer',
]);
expect(items.every(i => i.visible)).toBe(true);
expect(usePlayerBarLayoutStore.getState().trackInfoMode).toBe(DEFAULT_PLAYER_BAR_TRACK_INFO_MODE);
});
it('places stop in the transport zone and the rest in actions', () => {
expect(PLAYER_BAR_LAYOUT_ZONES.stop).toBe('transport');
expect(ids(DEFAULT_PLAYER_BAR_LAYOUT_ITEMS)
.filter(id => id !== 'stop')
.every(id => PLAYER_BAR_LAYOUT_ZONES[id] === 'actions')).toBe(true);
});
it('toggleItem flips the matching id without disturbing the others', () => {
usePlayerBarLayoutStore.getState().toggleItem('equalizer');
const items = usePlayerBarLayoutStore.getState().items;
expect(items.find(i => i.id === 'equalizer')?.visible).toBe(false);
expect(items.find(i => i.id === 'starRating')?.visible).toBe(true);
expect(items.find(i => i.id === 'miniPlayer')?.visible).toBe(true);
expect(items.filter(i => i.id !== 'equalizer').every(i => i.visible)).toBe(true);
});
it('reset restores defaults after toggles', () => {
const { toggleItem, reset } = usePlayerBarLayoutStore.getState();
it('reset restores items and the track-info mode together', () => {
const { toggleItem, setTrackInfoMode, reset } = usePlayerBarLayoutStore.getState();
toggleItem('favorite');
toggleItem('lastfmLove');
toggleItem('stop');
setTrackInfoMode('titleAlbum');
reset();
expect(usePlayerBarLayoutStore.getState().items).toEqual(DEFAULT_PLAYER_BAR_LAYOUT_ITEMS);
const state = usePlayerBarLayoutStore.getState();
expect(state.items).toEqual(DEFAULT_PLAYER_BAR_LAYOUT_ITEMS);
expect(state.trackInfoMode).toBe(DEFAULT_PLAYER_BAR_TRACK_INFO_MODE);
});
describe('rehydrate', () => {
it('adds items the stored layout predates, without losing the stored choices', () => {
// A layout persisted before 'stop' existed: it must come back, visible.
const state = rehydrate({
items: [
{ id: 'starRating', visible: true },
{ id: 'equalizer', visible: false },
],
});
expect(ids(state.items)).toContain('stop');
expect(state.items.find(i => i.id === 'stop')?.visible).toBe(true);
expect(state.items.find(i => i.id === 'equalizer')?.visible).toBe(false);
});
it('keeps the stored order rather than forcing the default one', () => {
const state = rehydrate({
items: [
{ id: 'miniPlayer', visible: true },
{ id: 'equalizer', visible: true },
],
});
expect(ids(state.items).slice(0, 2)).toEqual(['miniPlayer', 'equalizer']);
});
it('drops unknown and malformed entries', () => {
const state = rehydrate({
items: [{ id: 'ghostButton', visible: true }, null, { visible: true }],
});
expect(ids(state.items).sort()).toEqual(ids(DEFAULT_PLAYER_BAR_LAYOUT_ITEMS).sort());
});
it('falls back to the default track-info mode on an unknown or missing value', () => {
expect(rehydrate({ trackInfoMode: 'everything' }).trackInfoMode)
.toBe(DEFAULT_PLAYER_BAR_TRACK_INFO_MODE);
expect(rehydrate({ trackInfoMode: undefined }).trackInfoMode)
.toBe(DEFAULT_PLAYER_BAR_TRACK_INFO_MODE);
});
it('keeps a valid stored track-info mode', () => {
expect(rehydrate({ trackInfoMode: 'titleAlbum' }).trackInfoMode).toBe('titleAlbum');
});
});
});
@@ -2,6 +2,7 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export type PlayerBarLayoutItemId =
| 'stop'
| 'starRating'
| 'favorite'
// 'lastfmLove' is the enrichment-primary love button. The id is kept (not
@@ -12,12 +13,40 @@ export type PlayerBarLayoutItemId =
| 'equalizer'
| 'miniPlayer';
/**
* Which cluster of the bar an item lives in.
*
* `transport` items sit among the fixed playback controls, so only their
* visibility is configurable — their position is dictated by the transport row
* (the play button is a centred, special-cased element; letting users shuffle
* controls around it buys nothing and breaks the adaptive small-window layout).
* `actions` items are a plain right-hand row, so they are both toggleable and
* reorderable.
*/
export type PlayerBarLayoutZone = 'transport' | 'actions';
export const PLAYER_BAR_LAYOUT_ZONES: Record<PlayerBarLayoutItemId, PlayerBarLayoutZone> = {
stop: 'transport',
starRating: 'actions',
favorite: 'actions',
lastfmLove: 'actions',
playbackRate: 'actions',
equalizer: 'actions',
miniPlayer: 'actions',
};
/** What the track-info block shows under the title. */
export type PlayerBarTrackInfoMode = 'title' | 'titleAlbum';
export const PLAYER_BAR_TRACK_INFO_MODES: PlayerBarTrackInfoMode[] = ['title', 'titleAlbum'];
export interface PlayerBarLayoutItemConfig {
id: PlayerBarLayoutItemId;
visible: boolean;
}
export const DEFAULT_PLAYER_BAR_LAYOUT_ITEMS: PlayerBarLayoutItemConfig[] = [
{ id: 'stop', visible: true },
{ id: 'starRating', visible: true },
{ id: 'favorite', visible: true },
{ id: 'lastfmLove', visible: true },
@@ -26,10 +55,14 @@ export const DEFAULT_PLAYER_BAR_LAYOUT_ITEMS: PlayerBarLayoutItemConfig[] = [
{ id: 'miniPlayer', visible: true },
];
export const DEFAULT_PLAYER_BAR_TRACK_INFO_MODE: PlayerBarTrackInfoMode = 'title';
interface PlayerBarLayoutStore {
items: PlayerBarLayoutItemConfig[];
trackInfoMode: PlayerBarTrackInfoMode;
setItems: (items: PlayerBarLayoutItemConfig[]) => void;
toggleItem: (id: PlayerBarLayoutItemId) => void;
setTrackInfoMode: (mode: PlayerBarTrackInfoMode) => void;
reset: () => void;
}
@@ -37,6 +70,7 @@ export const usePlayerBarLayoutStore = create<PlayerBarLayoutStore>()(
persist(
(set) => ({
items: DEFAULT_PLAYER_BAR_LAYOUT_ITEMS,
trackInfoMode: DEFAULT_PLAYER_BAR_TRACK_INFO_MODE,
setItems: (items) => set({ items }),
@@ -44,7 +78,12 @@ export const usePlayerBarLayoutStore = create<PlayerBarLayoutStore>()(
items: s.items.map(it => it.id === id ? { ...it, visible: !it.visible } : it),
})),
reset: () => set({ items: DEFAULT_PLAYER_BAR_LAYOUT_ITEMS }),
setTrackInfoMode: (trackInfoMode) => set({ trackInfoMode }),
reset: () => set({
items: DEFAULT_PLAYER_BAR_LAYOUT_ITEMS,
trackInfoMode: DEFAULT_PLAYER_BAR_TRACK_INFO_MODE,
}),
}),
{
name: 'psysonic_player_bar_layout',
@@ -55,8 +94,14 @@ export const usePlayerBarLayoutStore = create<PlayerBarLayoutStore>()(
.filter((i): i is PlayerBarLayoutItemConfig =>
i != null && typeof i.id === 'string' && knownIds.has(i.id as PlayerBarLayoutItemId));
const seen = new Set(safe.map(i => i.id));
// Items added in a later version (e.g. 'stop') are absent from an older
// stored layout — append them with their default so the button does not
// silently vanish for existing users.
const missing = DEFAULT_PLAYER_BAR_LAYOUT_ITEMS.filter(i => !seen.has(i.id));
state.items = missing.length > 0 ? [...safe, ...missing] : safe;
if (!PLAYER_BAR_TRACK_INFO_MODES.includes(state.trackInfoMode)) {
state.trackInfoMode = DEFAULT_PLAYER_BAR_TRACK_INFO_MODE;
}
},
}
)
@@ -181,7 +181,6 @@ export function PersonalisationTab() {
<SettingsSubSection
title={t('settings.playerBarTitle')}
icon={<Disc3 size={16} />}
advanced
action={
<button
type="button"
@@ -1,13 +1,21 @@
import React from 'react';
import React, { useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Gauge, Heart, PictureInPicture2, SlidersVertical, Star } from 'lucide-react';
import { Gauge, GripVertical, Heart, PictureInPicture2, SlidersVertical, Square, Star } from 'lucide-react';
import LastfmIcon from '@/ui/LastfmIcon';
import {
usePlayerBarLayoutStore,
PLAYER_BAR_LAYOUT_ZONES,
type PlayerBarLayoutItemConfig,
type PlayerBarLayoutItemId,
type PlayerBarTrackInfoMode,
} from '@/features/playback/store/playerBarLayoutStore';
import { useListReorderDnd } from '@/lib/hooks/useListReorderDnd';
import { applyListReorderById, type ListReorderDropTarget } from '@/lib/util/listReorder';
import { ReorderGripHandle } from '@/features/settings/components/ReorderGripHandle';
import { SettingsSegmented } from '@/features/settings/components/SettingsSegmented';
const PLAYER_BAR_LAYOUT_LABEL_KEYS: Record<PlayerBarLayoutItemId, string> = {
stop: 'settings.playerBarStop',
starRating: 'settings.playerBarStarRating',
favorite: 'settings.playerBarFavorite',
lastfmLove: 'settings.playerBarLastfmLove',
@@ -17,6 +25,7 @@ const PLAYER_BAR_LAYOUT_LABEL_KEYS: Record<PlayerBarLayoutItemId, string> = {
};
const PLAYER_BAR_LAYOUT_ICONS: Record<PlayerBarLayoutItemId, React.ReactNode> = {
stop: <Square size={16} style={{ color: 'var(--text-muted)', flexShrink: 0 }} />,
starRating: <Star size={16} style={{ color: 'var(--text-muted)', flexShrink: 0 }} />,
favorite: <Heart size={16} style={{ color: 'var(--text-muted)', flexShrink: 0 }} />,
lastfmLove: (
@@ -29,26 +38,90 @@ const PLAYER_BAR_LAYOUT_ICONS: Record<PlayerBarLayoutItemId, React.ReactNode> =
miniPlayer: <PictureInPicture2 size={16} style={{ color: 'var(--text-muted)', flexShrink: 0 }} />,
};
const REORDER_TYPE = 'player_bar_layout_reorder';
export function PlayerBarLayoutCustomizer() {
const { t } = useTranslation();
const items = usePlayerBarLayoutStore(s => s.items);
const setItems = usePlayerBarLayoutStore(s => s.setItems);
const toggleItem = usePlayerBarLayoutStore(s => s.toggleItem);
const trackInfoMode = usePlayerBarLayoutStore(s => s.trackInfoMode);
const setTrackInfoMode = usePlayerBarLayoutStore(s => s.setTrackInfoMode);
const itemsRef = useRef(items);
// React Compiler refs rule: ref kept in sync with the latest value for use in handlers; not render data.
// eslint-disable-next-line react-hooks/refs
itemsRef.current = items;
const transportItems = items.filter(i => PLAYER_BAR_LAYOUT_ZONES[i.id] === 'transport');
const actionItems = items.filter(i => PLAYER_BAR_LAYOUT_ZONES[i.id] === 'actions');
// Reorder resolves by stable id against the FULL list, so the zone filter that
// decides which rows are shown can never share an index space with the move
// (the #1164 class of bug).
const apply = useCallback((draggedId: string, target: ListReorderDropTarget) => {
const next = applyListReorderById(itemsRef.current, draggedId, target);
if (next) setItems(next);
}, [setItems]);
const { isDragging, setContainer, onMouseMove, dropEdge } = useListReorderDnd({ type: REORDER_TYPE, apply });
const row = (it: PlayerBarLayoutItemConfig, draggable: boolean) => {
const label = t(PLAYER_BAR_LAYOUT_LABEL_KEYS[it.id]);
const edge = draggable && isDragging ? dropEdge(it.id) : null;
return (
<div
key={it.id}
data-reorder-id={draggable ? it.id : undefined}
className="sidebar-customizer-row"
style={{
borderTop: edge === 'before' ? '2px solid var(--accent)' : undefined,
borderBottom: edge === 'after' ? '2px solid var(--accent)' : undefined,
}}
>
{draggable ? (
<ReorderGripHandle id={it.id} type={REORDER_TYPE} label={label} />
) : (
// Transport items keep their fixed position, so they have no grip. The
// spacer carries the same icon so it occupies the same width — an empty
// span collapses and pulls the row out of line with the ones below.
<span className="sidebar-customizer-grip" style={{ visibility: 'hidden' }} aria-hidden>
<GripVertical size={16} />
</span>
)}
{PLAYER_BAR_LAYOUT_ICONS[it.id]}
<span style={{ flex: 1, fontSize: 14, opacity: it.visible ? 1 : 0.45 }}>{label}</span>
<label className="toggle-switch" aria-label={label}>
<input type="checkbox" checked={it.visible} onChange={() => toggleItem(it.id)} />
<span className="toggle-track" />
</label>
</div>
);
};
return (
<div style={{ padding: '4px 0' }}>
{items.map((it) => {
const label = t(PLAYER_BAR_LAYOUT_LABEL_KEYS[it.id]);
return (
<div key={it.id} className="sidebar-customizer-row">
{PLAYER_BAR_LAYOUT_ICONS[it.id]}
<span style={{ flex: 1, fontSize: 14, opacity: it.visible ? 1 : 0.45 }}>{label}</span>
<label className="toggle-switch" aria-label={label}>
<input type="checkbox" checked={it.visible} onChange={() => toggleItem(it.id)} />
<span className="toggle-track" />
</label>
</div>
);
})}
<div className="settings-group-title">{t('settings.playerBarTransportGroup')}</div>
{transportItems.map(it => row(it, false))}
<div className="settings-group-title" style={{ marginTop: '0.75rem' }}>
{t('settings.playerBarActionsGroup')}
</div>
<div ref={setContainer} onMouseMove={onMouseMove}>
{actionItems.map(it => row(it, true))}
</div>
<div className="settings-group-title" style={{ marginTop: '0.75rem' }}>
{t('settings.playerBarTrackInfo')}
</div>
<SettingsSegmented<PlayerBarTrackInfoMode>
options={[
{ id: 'title', label: t('settings.playerBarTrackInfoTitle') },
{ id: 'titleAlbum', label: t('settings.playerBarTrackInfoTitleAlbum') },
]}
value={trackInfoMode}
onChange={setTrackInfoMode}
/>
</div>
);
}
+6
View File
@@ -542,6 +542,12 @@ export const settings = {
playerBarPlaybackRate: 'Скорост на възпроизвеждане',
playerBarEqualizer: 'Еквалайзер',
playerBarMiniPlayer: 'Мини плейър',
playerBarStop: 'Бутон за спиране',
playerBarTransportGroup: 'Управление на възпроизвеждането',
playerBarActionsGroup: 'Действия',
playerBarTrackInfo: 'Информация за песента',
playerBarTrackInfoTitle: 'Само заглавие',
playerBarTrackInfoTitleAlbum: 'Заглавие + албум',
playbackRateTitle: 'Скорост на възпроизвеждане',
playbackRateEnabled: 'Включи скоростта на възпроизвеждане',
playbackRateEnabledDesc: 'Променя скоростта глобално за всички песни. Не се прилага за радио или предварителни прослушвания.',
+6
View File
@@ -498,6 +498,12 @@ export const settings = {
playerBarPlaybackRate: 'Wiedergabegeschwindigkeit',
playerBarEqualizer: 'Equalizer',
playerBarMiniPlayer: 'Mini-Player',
playerBarStop: 'Stopp-Schaltfläche',
playerBarTransportGroup: 'Wiedergabesteuerung',
playerBarActionsGroup: 'Aktionen',
playerBarTrackInfo: 'Titelinfo',
playerBarTrackInfoTitle: 'Nur Titel',
playerBarTrackInfoTitleAlbum: 'Titel + Album',
playbackRateTitle: 'Wiedergabegeschwindigkeit',
playbackRateEnabled: 'Wiedergabegeschwindigkeit aktivieren',
playbackRateEnabledDesc: 'Globale Geschwindigkeit für alle Titel. Gilt nicht für Radio und Vorschau.',
+6
View File
@@ -542,6 +542,12 @@ export const settings = {
playerBarPlaybackRate: 'Playback speed',
playerBarEqualizer: 'Equalizer',
playerBarMiniPlayer: 'Mini player',
playerBarStop: 'Stop button',
playerBarTransportGroup: 'Transport',
playerBarActionsGroup: 'Actions',
playerBarTrackInfo: 'Track info',
playerBarTrackInfoTitle: 'Title only',
playerBarTrackInfoTitleAlbum: 'Title + album',
playbackRateTitle: 'Playback speed',
playbackRateEnabled: 'Enable playback speed',
playbackRateEnabledDesc: 'Change speed globally for all tracks. Not applied to radio or previews.',
+6
View File
@@ -497,6 +497,12 @@ export const settings = {
playerBarPlaybackRate: 'Velocidad de reproducción',
playerBarEqualizer: 'Ecualizador',
playerBarMiniPlayer: 'Mini reproductor',
playerBarStop: 'Botón de parada',
playerBarTransportGroup: 'Transporte',
playerBarActionsGroup: 'Acciones',
playerBarTrackInfo: 'Información de la pista',
playerBarTrackInfoTitle: 'Solo el título',
playerBarTrackInfoTitleAlbum: 'Título + álbum',
playbackRateTitle: 'Velocidad de reproducción',
playbackRateEnabled: 'Activar velocidad de reproducción',
playbackRateEnabledDesc: 'Velocidad global para todas las pistas. No se aplica a la radio ni a las vistas previas.',
+6
View File
@@ -485,6 +485,12 @@ export const settings = {
playerBarPlaybackRate: 'Vitesse de lecture',
playerBarEqualizer: 'Égaliseur',
playerBarMiniPlayer: 'Mini-lecteur',
playerBarStop: 'Bouton darrêt',
playerBarTransportGroup: 'Transport',
playerBarActionsGroup: 'Actions',
playerBarTrackInfo: 'Infos du morceau',
playerBarTrackInfoTitle: 'Titre uniquement',
playerBarTrackInfoTitleAlbum: 'Titre + album',
playbackRateTitle: 'Vitesse de lecture',
playbackRateEnabled: 'Activer la vitesse de lecture',
playbackRateEnabledDesc: 'Vitesse globale pour toutes les pistes. Non appliquée à la radio ni aux aperçus.',
+6
View File
@@ -542,6 +542,12 @@ export const settings = {
playerBarPlaybackRate: 'Lejátszási sebesség',
playerBarEqualizer: 'Hangszínszabályzó',
playerBarMiniPlayer: 'Mini lejátszó',
playerBarStop: 'Leállítás gomb',
playerBarTransportGroup: 'Lejátszásvezérlők',
playerBarActionsGroup: 'Műveletek',
playerBarTrackInfo: 'Száminformáció',
playerBarTrackInfoTitle: 'Csak a cím',
playerBarTrackInfoTitleAlbum: 'Cím + album',
playbackRateTitle: 'Lejátszási sebesség',
playbackRateEnabled: 'Lejátszási sebesség engedélyezése',
playbackRateEnabledDesc: 'A sebesség globális módosítása minden számhoz. Nem érvényes a rádióra vagy az előnézetekre.',
+6
View File
@@ -543,6 +543,12 @@ export const settings = {
playerBarPlaybackRate: 'Velocità di riproduzione',
playerBarEqualizer: 'Equalizzatore',
playerBarMiniPlayer: 'Mini player',
playerBarStop: 'Pulsante di stop',
playerBarTransportGroup: 'Trasporto',
playerBarActionsGroup: 'Azioni',
playerBarTrackInfo: 'Info brano',
playerBarTrackInfoTitle: 'Solo titolo',
playerBarTrackInfoTitleAlbum: 'Titolo + album',
playbackRateTitle: 'Velocità di riproduzione',
playbackRateEnabled: 'Attiva velocità di riproduzione',
playbackRateEnabledDesc: 'Cambia la velocità globalmente per tutti i brani. Non si applica alla radio o alle anteprime.',
+6
View File
@@ -536,6 +536,12 @@ export const settings = {
playerBarPlaybackRate: '再生速度',
playerBarEqualizer: 'イコライザー',
playerBarMiniPlayer: 'ミニプレイヤー',
playerBarStop: '停止ボタン',
playerBarTransportGroup: '再生コントロール',
playerBarActionsGroup: 'アクション',
playerBarTrackInfo: '曲情報',
playerBarTrackInfoTitle: 'タイトルのみ',
playerBarTrackInfoTitleAlbum: 'タイトル + アルバム',
playbackRateTitle: '再生速度',
playbackRateEnabled: '再生速度を有効化',
playbackRateEnabledDesc: 'すべてのトラックに対してグローバルに速度を変更します。ラジオやプレビューには適用されません。',
+6
View File
@@ -484,6 +484,12 @@ export const settings = {
playerBarPlaybackRate: 'Avspillingshastighet',
playerBarEqualizer: 'Equalizer',
playerBarMiniPlayer: 'Miniavspiller',
playerBarStop: 'Stopp-knapp',
playerBarTransportGroup: 'Avspillingskontroller',
playerBarActionsGroup: 'Handlinger',
playerBarTrackInfo: 'Sporinfo',
playerBarTrackInfoTitle: 'Bare tittel',
playerBarTrackInfoTitleAlbum: 'Tittel + album',
playbackRateTitle: 'Avspillingshastighet',
playbackRateEnabled: 'Aktiver avspillingshastighet',
playbackRateEnabledDesc: 'Global hastighet for alle spor. Gjelder ikke radio og forhåndsvisninger.',
+6
View File
@@ -485,6 +485,12 @@ export const settings = {
playerBarPlaybackRate: 'Afspeelsnelheid',
playerBarEqualizer: 'Equalizer',
playerBarMiniPlayer: 'Mini-speler',
playerBarStop: 'Stopknop',
playerBarTransportGroup: 'Transport',
playerBarActionsGroup: 'Acties',
playerBarTrackInfo: 'Trackinfo',
playerBarTrackInfoTitle: 'Alleen titel',
playerBarTrackInfoTitleAlbum: 'Titel + album',
playbackRateTitle: 'Afspeelsnelheid',
playbackRateEnabled: 'Afspeelsnelheid inschakelen',
playbackRateEnabledDesc: 'Globale snelheid voor alle nummers. Niet van toepassing op radio en previews.',
+6
View File
@@ -542,6 +542,12 @@ export const settings = {
playerBarPlaybackRate: 'Prędkość odtwarzania',
playerBarEqualizer: 'Equalizer',
playerBarMiniPlayer: 'Miniodtwarzacz',
playerBarStop: 'Przycisk zatrzymania',
playerBarTransportGroup: 'Sterowanie odtwarzaniem',
playerBarActionsGroup: 'Akcje',
playerBarTrackInfo: 'Informacje o utworze',
playerBarTrackInfoTitle: 'Tylko tytuł',
playerBarTrackInfoTitleAlbum: 'Tytuł + album',
playbackRateTitle: 'Prędkość odtwarzania',
playbackRateEnabled: 'Włącz zmianę prędkości odtwarzania',
playbackRateEnabledDesc: 'Zmienia prędkość globalnie, dla wszystkich utworów. Nie dotyczy radia ani podglądu utworów.',
+6
View File
@@ -500,6 +500,12 @@ export const settings = {
playerBarPlaybackRate: 'Viteză redare',
playerBarEqualizer: 'Egalizator',
playerBarMiniPlayer: 'Mini player',
playerBarStop: 'Buton de oprire',
playerBarTransportGroup: 'Control redare',
playerBarActionsGroup: 'Acțiuni',
playerBarTrackInfo: 'Informații piesă',
playerBarTrackInfoTitle: 'Doar titlul',
playerBarTrackInfoTitleAlbum: 'Titlu + album',
playbackRateTitle: 'Viteză redare',
playbackRateEnabled: 'Activează viteza de redare',
playbackRateEnabledDesc: 'Viteză globală pentru toate piesele. Nu se aplică la radio și previzualizări.',
+6
View File
@@ -557,6 +557,12 @@ export const settings = {
playerBarPlaybackRate: 'Скорость',
playerBarEqualizer: 'Эквалайзер',
playerBarMiniPlayer: 'Мини-плеер',
playerBarStop: 'Кнопка «Стоп»',
playerBarTransportGroup: 'Управление воспроизведением',
playerBarActionsGroup: 'Действия',
playerBarTrackInfo: 'Информация о треке',
playerBarTrackInfoTitle: 'Только название',
playerBarTrackInfoTitleAlbum: 'Название + альбом',
playbackRateTitle: 'Скорость воспроизведения',
playbackRateEnabled: 'Включить изменение скорости',
playbackRateEnabledDesc: 'Глобальная скорость для всех треков. Не применяется к радио и превью.',
+6
View File
@@ -484,6 +484,12 @@ export const settings = {
playerBarPlaybackRate: '播放速度',
playerBarEqualizer: '均衡器',
playerBarMiniPlayer: '迷你播放器',
playerBarStop: '停止按钮',
playerBarTransportGroup: '播放控制',
playerBarActionsGroup: '操作',
playerBarTrackInfo: '曲目信息',
playerBarTrackInfoTitle: '仅标题',
playerBarTrackInfoTitleAlbum: '标题 + 专辑',
playbackRateTitle: '播放速度',
playbackRateEnabled: '启用播放速度',
playbackRateEnabledDesc: '全局调整所有曲目的播放速度。不适用于电台和预览。',
+9
View File
@@ -186,6 +186,15 @@ html[data-platform="windows"] .player-bar.floating {
text-decoration: underline;
}
/* Optional third line (Settings Personalisation Player bar). Sits below the
artist and is quieter than it, so the title stays the anchor of the block. */
.player-track-album {
font-size: 11px;
color: var(--player-artist);
opacity: 0.75;
margin-top: 1px;
}
.player-track-rating {
margin-top: 4px;
width: auto;