mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
feat(sidebar): toggle to pin Now Playing to the top (#1000)
* feat(sidebar): add toggle to pin Now Playing to the top The fixed Now Playing entry can now be pinned to the very top of the sidebar (above the Library label) instead of sitting above the bottom spacer. Adds a persisted `nowPlayingAtTop` setting (default off, so existing layouts are unchanged) and a toggle in the sidebar customizer next to the Mix-navigation split. The entry stays non-hideable and keeps its playing indicator in both positions. * i18n(settings): Now Playing-at-top toggle strings (9 locales) * docs(changelog): note Now Playing top toggle (#1000)
This commit is contained in:
committed by
GitHub
parent
5f345dc7aa
commit
c39465dfad
@@ -65,6 +65,7 @@ export default function Sidebar({
|
||||
const sidebarItems = useSidebarStore(s => s.items);
|
||||
const setSidebarItems = useSidebarStore(s => s.setItems);
|
||||
const randomNavMode = useAuthStore(s => s.randomNavMode);
|
||||
const nowPlayingAtTop = useAuthStore(s => s.nowPlayingAtTop);
|
||||
const luckyMixBase = useLuckyMixAvailable();
|
||||
// Sidebar surfaces Lucky Mix as its own entry only in "separate" nav mode —
|
||||
// in hub mode it lives inside the Build-a-Mix landing page instead.
|
||||
@@ -227,6 +228,7 @@ export default function Sidebar({
|
||||
handleNavRowPointerDown={handleNavRowPointerDown}
|
||||
isPlaying={isPlaying}
|
||||
hasNowPlayingTrack={!!currentTrack}
|
||||
nowPlayingAtTop={nowPlayingAtTop}
|
||||
hasOfflineContent={hasOfflineContent}
|
||||
activeJobsCount={activeJobs.length}
|
||||
cancelAllDownloads={cancelAllDownloads}
|
||||
|
||||
@@ -39,6 +39,8 @@ export function SidebarCustomizer() {
|
||||
itemsRef.current = items;
|
||||
const randomNavMode = useAuthStore(s => s.randomNavMode);
|
||||
const setRandomNavMode = useAuthStore(s => s.setRandomNavMode);
|
||||
const nowPlayingAtTop = useAuthStore(s => s.nowPlayingAtTop);
|
||||
const setNowPlayingAtTop = useAuthStore(s => s.setNowPlayingAtTop);
|
||||
const luckyMixBase = useLuckyMixAvailable();
|
||||
const luckyMixAvailable = luckyMixBase && randomNavMode === 'separate';
|
||||
|
||||
@@ -138,6 +140,20 @@ export function SidebarCustomizer() {
|
||||
<span className="toggle-track" />
|
||||
</label>
|
||||
</div>
|
||||
<div className="settings-toggle-row" data-settings-search={t('settings.nowPlayingTopTitle')}>
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{t('settings.nowPlayingTopTitle')}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.nowPlayingTopDesc')}</div>
|
||||
</div>
|
||||
<label className="toggle-switch" aria-label={t('settings.nowPlayingTopTitle')}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={nowPlayingAtTop}
|
||||
onChange={e => setNowPlayingAtTop(e.target.checked)}
|
||||
/>
|
||||
<span className="toggle-track" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div ref={containerRef} onMouseMove={handleMouseMove} style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{/* Library block */}
|
||||
|
||||
@@ -41,6 +41,7 @@ interface Props {
|
||||
handleNavRowPointerDown: (e: React.PointerEvent, section: 'library' | 'system', sectionIdx: number) => void;
|
||||
isPlaying: boolean;
|
||||
hasNowPlayingTrack: boolean;
|
||||
nowPlayingAtTop: boolean;
|
||||
hasOfflineContent: boolean;
|
||||
activeJobsCount: number;
|
||||
cancelAllDownloads: () => void;
|
||||
@@ -60,14 +61,32 @@ export default function SidebarNavBody(props: Props) {
|
||||
visibleSystemConfigs, systemItemsForReorder,
|
||||
playlistsExpanded, setPlaylistsExpanded, playlists, playlistsLoading,
|
||||
newReleasesUnreadCount, navDnd, navDndRowClass, handleNavRowPointerDown,
|
||||
isPlaying, hasNowPlayingTrack, hasOfflineContent,
|
||||
isPlaying, hasNowPlayingTrack, nowPlayingAtTop, hasOfflineContent,
|
||||
activeJobsCount, cancelAllDownloads,
|
||||
isSyncing, syncJobDone, syncJobSkip, syncJobFail, syncJobTotal,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Now Playing — fixed entry (not hideable). Rendered either pinned at the
|
||||
// very top of the sidebar or after the bottom spacer, per the user setting.
|
||||
const nowPlayingLink = (
|
||||
<NavLink
|
||||
to="/now-playing"
|
||||
className={({ isActive }) => `nav-link nav-link-nowplaying ${isActive ? 'active' : ''}`}
|
||||
data-tooltip={isCollapsed ? t('sidebar.nowPlaying') : undefined}
|
||||
data-tooltip-pos="bottom"
|
||||
>
|
||||
<span className="nav-np-icon-wrap">
|
||||
<AudioLines size={isCollapsed ? 22 : 18} />
|
||||
{isPlaying && hasNowPlayingTrack && <span className="nav-np-dot" />}
|
||||
</span>
|
||||
{!isCollapsed && <span>{t('sidebar.nowPlaying')}</span>}
|
||||
</NavLink>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{nowPlayingAtTop && nowPlayingLink}
|
||||
{!isCollapsed && (showLibraryPicker ? (
|
||||
<SidebarLibraryPicker
|
||||
filterId={filterId}
|
||||
@@ -195,19 +214,8 @@ export default function SidebarNavBody(props: Props) {
|
||||
{/* What's New banner — only visible while the current release hasn't been seen. */}
|
||||
<WhatsNewBanner collapsed={isCollapsed} />
|
||||
|
||||
{/* Now Playing — fixed, always visible */}
|
||||
<NavLink
|
||||
to="/now-playing"
|
||||
className={({ isActive }) => `nav-link nav-link-nowplaying ${isActive ? 'active' : ''}`}
|
||||
data-tooltip={isCollapsed ? t('sidebar.nowPlaying') : undefined}
|
||||
data-tooltip-pos="bottom"
|
||||
>
|
||||
<span className="nav-np-icon-wrap">
|
||||
<AudioLines size={isCollapsed ? 22 : 18} />
|
||||
{isPlaying && hasNowPlayingTrack && <span className="nav-np-dot" />}
|
||||
</span>
|
||||
{!isCollapsed && <span>{t('sidebar.nowPlaying')}</span>}
|
||||
</NavLink>
|
||||
{/* Now Playing — pinned at the bottom unless the user moved it to the top. */}
|
||||
{!nowPlayingAtTop && nowPlayingLink}
|
||||
|
||||
{hasOfflineContent && (
|
||||
<NavLink
|
||||
|
||||
Reference in New Issue
Block a user