diff --git a/CHANGELOG.md b/CHANGELOG.md index ca54770f..214dda22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -384,6 +384,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Pressing Play, Shuffle or Add to queue on a playlist no longer reloads the whole page with a spinner — it just starts playback. Editing the playlist (adding or removing songs) still refreshes the list as before. +### Sidebar items jumped back when reordered + +**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1206](https://github.com/Psychotoxical/psysonic/pull/1206)**, reported by [@tummydummy](https://github.com/tummydummy) + +* In Settings → Personalization → Sidebar, dragging an item to a new position could snap it back or land it one place off, depending on which items were hidden. Reordering now tracks each item directly, so it stays exactly where you release it — both in the customizer and when long-pressing items in the sidebar itself. + ## [1.48.1] - 2026-06-15 ## Fixed diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 8dfa22bc..ed210dd4 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -147,11 +147,6 @@ export default function Sidebar({ // React Compiler refs rule: ref kept in sync with the latest value for use in effects/handlers/cleanup; not render data. // eslint-disable-next-line react-hooks/refs sidebarItemsRef.current = sidebarItems; - const randomNavModeRef = useRef(randomNavMode); - // React Compiler refs rule: ref kept in sync with the latest value for use in effects/handlers/cleanup; not render data. - // eslint-disable-next-line react-hooks/refs - randomNavModeRef.current = randomNavMode; - const { navDnd, navDndTrashHint, @@ -161,7 +156,6 @@ export default function Sidebar({ } = useSidebarNavDnd({ isCollapsed, sidebarItemsRef, - randomNavModeRef, setSidebarItems, }); const newReleasesUnreadCount = useSidebarNewReleasesUnread({ @@ -257,9 +251,7 @@ export default function Sidebar({ musicFolders={musicFolders} pickLibrary={pickLibrary} visibleLibraryConfigs={visibleLibraryConfigs} - libraryItemsForReorder={libraryItemsForReorder} visibleSystemConfigs={visibleSystemConfigs} - systemItemsForReorder={systemItemsForReorder} playlistsExpanded={playlistsExpanded} setPlaylistsExpanded={setPlaylistsExpanded} playlists={playlists} diff --git a/src/components/settings/SidebarCustomizer.tsx b/src/components/settings/SidebarCustomizer.tsx index 6f520a1c..ab0f0194 100644 --- a/src/components/settings/SidebarCustomizer.tsx +++ b/src/components/settings/SidebarCustomizer.tsx @@ -6,16 +6,16 @@ import { useAuthStore } from '../../store/authStore'; import { useSidebarStore, SidebarItemConfig, CONSERVED_SIDEBAR_NAV_IDS } from '../../store/sidebarStore'; import { useLuckyMixAvailable } from '../../hooks/useLuckyMixAvailable'; import { ALL_NAV_ITEMS } from '../../config/navItems'; -import { applySidebarDropReorder } from '../../utils/componentHelpers/sidebarNavReorder'; +import { applySidebarReorderById } from '../../utils/componentHelpers/sidebarNavReorder'; import { SettingsGroup } from './SettingsGroup'; import { SettingsToggle } from './SettingsToggle'; -type DropTarget = { idx: number; before: boolean; section: 'library' | 'system' } | null; +type DropTarget = { id: string; before: boolean; section: 'library' | 'system' } | null; -function SidebarGripHandle({ idx, section, label }: { idx: number; section: 'library' | 'system'; label: string }) { +function SidebarGripHandle({ id, section, label }: { id: string; section: 'library' | 'system'; label: string }) { const { t } = useTranslation(); const { onMouseDown } = useDragSource(() => ({ - data: JSON.stringify({ type: 'sidebar_reorder', index: idx, section }), + data: JSON.stringify({ type: 'sidebar_reorder', id, section }), label, })); return ( @@ -72,47 +72,48 @@ export function SidebarCustomizer() { const onPsyDrop = (e: Event) => { const detail = (e as CustomEvent).detail; if (!detail?.data) return; - let parsed: { type?: string; index?: number; section?: string }; + let parsed: { type?: string; id?: string; section?: string }; try { parsed = JSON.parse(detail.data); } catch { return; } - if (parsed.type !== 'sidebar_reorder' || parsed.index == null || !parsed.section) return; + if (parsed.type !== 'sidebar_reorder' || !parsed.id || !parsed.section) return; - const fromIdx = parsed.index; + const draggedId = parsed.id; const fromSection = parsed.section as 'library' | 'system'; const target = dropTargetRef.current; dropTargetRef.current = null; setDropTarget(null); - const next = applySidebarDropReorder(itemsRef.current, fromSection, fromIdx, target, randomNavMode); + const next = applySidebarReorderById(itemsRef.current, fromSection, draggedId, target); if (next) setItems(next); }; el.addEventListener('psy-drop', onPsyDrop); return () => el.removeEventListener('psy-drop', onPsyDrop); - }, [libraryItems, systemItems, setItems, randomNavMode]); + }, [setItems]); const handleMouseMove = (e: React.MouseEvent) => { if (!isPsyDragging || !containerRef.current) return; - const rows = containerRef.current.querySelectorAll('[data-sidebar-idx]'); + const rows = containerRef.current.querySelectorAll('[data-sidebar-id]'); let target: DropTarget = null; for (const row of rows) { const rect = row.getBoundingClientRect(); - const idx = Number(row.dataset.sidebarIdx); + const id = row.dataset.sidebarId; const section = row.dataset.sidebarSection as 'library' | 'system'; - if (e.clientY < rect.top + rect.height / 2) { target = { idx, before: true, section }; break; } - target = { idx, before: false, section }; + if (!id) continue; + if (e.clientY < rect.top + rect.height / 2) { target = { id, before: true, section }; break; } + target = { id, before: false, section }; } dropTargetRef.current = target; setDropTarget(target); }; - const renderRow = (cfg: SidebarItemConfig, localIdx: number, section: 'library' | 'system') => { + const renderRow = (cfg: SidebarItemConfig, section: 'library' | 'system') => { const meta = ALL_NAV_ITEMS[cfg.id]; if (!meta) return null; const Icon = meta.icon; - const isBefore = isPsyDragging && dropTarget?.section === section && dropTarget.idx === localIdx && dropTarget.before; - const isAfter = isPsyDragging && dropTarget?.section === section && dropTarget.idx === localIdx && !dropTarget.before; + const isBefore = isPsyDragging && dropTarget?.section === section && dropTarget.id === cfg.id && dropTarget.before; + const isAfter = isPsyDragging && dropTarget?.section === section && dropTarget.id === cfg.id && !dropTarget.before; return (
- + {t(meta.labelKey)}