import React, { useState, useRef, useLayoutEffect, useEffect, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { usePlayerStore } from '../store/playerStore'; import { useOfflineStore } from '../store/offlineStore'; import { useOfflineJobStore } from '../store/offlineJobStore'; import { useAuthStore } from '../store/authStore'; import { useSidebarStore } from '../store/sidebarStore'; import { NavLink } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Disc3, Users, Music4, Radio, Settings, Heart, BarChart3, Shuffle, PanelLeftClose, PanelLeft, HelpCircle, Dices, AudioLines, HardDriveDownload, Tags, ListMusic, Cast, ChevronDown, Check, Music2, TrendingUp, FolderOpen, X, } from 'lucide-react'; import PsysonicLogo from './PsysonicLogo'; import PSmallLogo from './PSmallLogo'; // All configurable nav items — order and visibility controlled by sidebarStore. // Exported so Settings can render the same item metadata. export const ALL_NAV_ITEMS: Record = { mainstage: { icon: Disc3, labelKey: 'sidebar.mainstage', to: '/', section: 'library' }, newReleases: { icon: Radio, labelKey: 'sidebar.newReleases', to: '/new-releases', section: 'library' }, allAlbums: { icon: Music4, labelKey: 'sidebar.allAlbums', to: '/albums', section: 'library' }, randomAlbums: { icon: Dices, labelKey: 'sidebar.randomAlbums', to: '/random-albums', section: 'library' }, artists: { icon: Users, labelKey: 'sidebar.artists', to: '/artists', section: 'library' }, genres: { icon: Tags, labelKey: 'sidebar.genres', to: '/genres', section: 'library' }, randomMix: { icon: Shuffle, labelKey: 'sidebar.randomMix', to: '/random-mix', section: 'library' }, favorites: { icon: Heart, labelKey: 'sidebar.favorites', to: '/favorites', section: 'library' }, playlists: { icon: ListMusic, labelKey: 'sidebar.playlists', to: '/playlists', section: 'library' }, mostPlayed: { icon: TrendingUp, labelKey: 'sidebar.mostPlayed', to: '/most-played', section: 'library' }, radio: { icon: Cast, labelKey: 'sidebar.radio', to: '/radio', section: 'library' }, folderBrowser: { icon: FolderOpen, labelKey: 'sidebar.folderBrowser', to: '/folders', section: 'library' }, statistics: { icon: BarChart3, labelKey: 'sidebar.statistics', to: '/statistics', section: 'system' }, help: { icon: HelpCircle, labelKey: 'sidebar.help', to: '/help', section: 'system' }, }; export default function Sidebar({ isCollapsed = false, toggleCollapse, }: { isCollapsed?: boolean; toggleCollapse?: () => void; }) { const { t } = useTranslation(); const isPlaying = usePlayerStore(s => s.isPlaying); const currentTrack = usePlayerStore(s => s.currentTrack); const offlineJobs = useOfflineJobStore(s => s.jobs); const cancelAllDownloads = useOfflineJobStore(s => s.cancelAllDownloads); const activeJobs = offlineJobs.filter(j => j.status === 'queued' || j.status === 'downloading'); const offlineAlbums = useOfflineStore(s => s.albums); const serverId = useAuthStore(s => s.activeServerId ?? ''); const isLoggedIn = useAuthStore(s => s.isLoggedIn); const musicFolders = useAuthStore(s => s.musicFolders); const musicLibraryFilterByServer = useAuthStore(s => s.musicLibraryFilterByServer); const setMusicLibraryFilter = useAuthStore(s => s.setMusicLibraryFilter); const hasOfflineContent = Object.values(offlineAlbums).some(a => a.serverId === serverId); const sidebarItems = useSidebarStore(s => s.items); const [libraryDropdownOpen, setLibraryDropdownOpen] = useState(false); const [dropdownRect, setDropdownRect] = useState({ top: 0, left: 0, width: 0 }); const libraryTriggerRef = useRef(null); const showLibraryPicker = !isCollapsed && isLoggedIn && musicFolders.length > 1; const filterId = serverId ? (musicLibraryFilterByServer[serverId] ?? 'all') : 'all'; const selectedFolderName = filterId === 'all' ? null : musicFolders.find(f => f.id === filterId)?.name ?? null; const libraryTriggerPlain = filterId === 'all'; const updateDropdownPosition = useCallback(() => { const el = libraryTriggerRef.current; if (!el) return; const r = el.getBoundingClientRect(); setDropdownRect({ top: r.bottom + 4, left: r.left, width: r.width, }); }, []); useLayoutEffect(() => { if (!libraryDropdownOpen) return; updateDropdownPosition(); const onWin = () => updateDropdownPosition(); window.addEventListener('resize', onWin); window.addEventListener('scroll', onWin, true); return () => { window.removeEventListener('resize', onWin); window.removeEventListener('scroll', onWin, true); }; }, [libraryDropdownOpen, updateDropdownPosition]); useEffect(() => { if (!libraryDropdownOpen) return; const onDown = (e: MouseEvent) => { const t = e.target as Node; if (libraryTriggerRef.current?.contains(t)) return; const panel = document.querySelector('.nav-library-dropdown-panel'); if (panel?.contains(t)) return; setLibraryDropdownOpen(false); }; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setLibraryDropdownOpen(false); }; document.addEventListener('mousedown', onDown); document.addEventListener('keydown', onKey); return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey); }; }, [libraryDropdownOpen]); const pickLibrary = (id: 'all' | string) => { setMusicLibraryFilter(id); setLibraryDropdownOpen(false); }; // Resolve ordered, visible items per section from store config const visibleLibrary = sidebarItems .filter(cfg => cfg.visible && ALL_NAV_ITEMS[cfg.id]?.section === 'library') .map(cfg => ALL_NAV_ITEMS[cfg.id]); const visibleSystem = sidebarItems .filter(cfg => cfg.visible && ALL_NAV_ITEMS[cfg.id]?.section === 'system') .map(cfg => ALL_NAV_ITEMS[cfg.id]); return ( ); }