diff --git a/CHANGELOG.md b/CHANGELOG.md index 5654e296..586580e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.48.0] +## Added + +### Sidebar — pin Now Playing to the top + +**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1000](https://github.com/Psychotoxical/psysonic/pull/1000), suggested by [@PHLAK](https://github.com/PHLAK)** + +* New **Settings → Sidebar** toggle moves the "Now Playing" entry to the top of the sidebar instead of the bottom (off by default). + + + ## Changed ### Dependencies — npm and Rust refresh diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index bfa3e42d..da36c8c1 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -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} diff --git a/src/components/settings/SidebarCustomizer.tsx b/src/components/settings/SidebarCustomizer.tsx index b0ae97bb..17da0292 100644 --- a/src/components/settings/SidebarCustomizer.tsx +++ b/src/components/settings/SidebarCustomizer.tsx @@ -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() { +
+
+
{t('settings.nowPlayingTopTitle')}
+
{t('settings.nowPlayingTopDesc')}
+
+ +
{/* Library block */} diff --git a/src/components/sidebar/SidebarNavBody.tsx b/src/components/sidebar/SidebarNavBody.tsx index 59297316..69a74f21 100644 --- a/src/components/sidebar/SidebarNavBody.tsx +++ b/src/components/sidebar/SidebarNavBody.tsx @@ -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 = ( + `nav-link nav-link-nowplaying ${isActive ? 'active' : ''}`} + data-tooltip={isCollapsed ? t('sidebar.nowPlaying') : undefined} + data-tooltip-pos="bottom" + > + + + {isPlaying && hasNowPlayingTrack && } + + {!isCollapsed && {t('sidebar.nowPlaying')}} + + ); + return ( <> + {nowPlayingAtTop && nowPlayingLink} {!isCollapsed && (showLibraryPicker ? ( - {/* Now Playing — fixed, always visible */} - `nav-link nav-link-nowplaying ${isActive ? 'active' : ''}`} - data-tooltip={isCollapsed ? t('sidebar.nowPlaying') : undefined} - data-tooltip-pos="bottom" - > - - - {isPlaying && hasNowPlayingTrack && } - - {!isCollapsed && {t('sidebar.nowPlaying')}} - + {/* Now Playing — pinned at the bottom unless the user moved it to the top. */} + {!nowPlayingAtTop && nowPlayingLink} {hasOfflineContent && ( { @@ -32,6 +33,7 @@ export function createDiscoveryActions(set: SetState): Pick< setRandomMixSize: (v) => set({ randomMixSize: clampRandomMixSize(v) }), setShowLuckyMixMenu: (v) => set({ showLuckyMixMenu: v }), setRandomNavMode: (v) => set({ randomNavMode: v }), + setNowPlayingAtTop: (v) => set({ nowPlayingAtTop: v }), setInfiniteQueueEnabled: (v) => set({ infiniteQueueEnabled: v }), setPreservePlayNextOrder: (v) => set({ preservePlayNextOrder: v }), }; diff --git a/src/store/authStore.ts b/src/store/authStore.ts index 94dac15c..b67bd62b 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -116,6 +116,7 @@ export const useAuthStore = create()( randomMixSize: 50, showLuckyMixMenu: true, randomNavMode: 'hub', + nowPlayingAtTop: false, musicFolders: [], musicLibraryFilterByServer: {}, musicLibraryFilterVersion: 0, diff --git a/src/store/authStoreTypes.ts b/src/store/authStoreTypes.ts index 41bfcd0b..90d762a3 100644 --- a/src/store/authStoreTypes.ts +++ b/src/store/authStoreTypes.ts @@ -373,6 +373,10 @@ export interface AuthState { randomNavMode: 'hub' | 'separate'; setRandomNavMode: (v: 'hub' | 'separate') => void; + /** Pin the fixed "Now Playing" sidebar entry to the top instead of the bottom. */ + nowPlayingAtTop: boolean; + setNowPlayingAtTop: (v: boolean) => void; + logout: () => void; // Derived