From 5ddbfcbfeea8d21c96b5dedda767732543ad9cbc Mon Sep 17 00:00:00 2001 From: Norman Date: Tue, 23 Jun 2026 20:18:02 -0700 Subject: [PATCH] Highlight the playlist in the left panel on play (#2025) * Fixed bad smart playlist field s * first try to add playlist highlight * Simplified calls * Now works for grids too. * Derive the playlist highlight from the currently-playing track's origin instead of a stale global field. * addressed comments --- .../hooks/use-item-drag-drop-state.tsx | 14 +++-- .../player/context/player-context.tsx | 55 +++++++++++++++++-- .../sidebar-playlist-list.module.css | 4 ++ .../components/sidebar-playlist-list.tsx | 19 ++++++- src/renderer/store/player.store.ts | 6 ++ src/shared/types/domain-types.ts | 1 + 6 files changed, 89 insertions(+), 10 deletions(-) diff --git a/src/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state.tsx b/src/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state.tsx index b8b89e73c..adcaa7bea 100644 --- a/src/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state.tsx +++ b/src/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state.tsx @@ -64,6 +64,7 @@ export const useItemDragDropState = { if (!item || !isDataRow) { return; @@ -248,10 +249,15 @@ export const useItemDragDropState = 0) { - playerContext.addToQueueByData(sourceItems, { - edge: args.edge, - uniqueId: droppedOnUniqueId, - }); + const sourcePlaylistId = args.source.metadata?.playlistId as + | string + | undefined; + playerContext.addToQueueByData( + sourceItems, + { edge: args.edge, uniqueId: droppedOnUniqueId }, + undefined, + sourcePlaylistId ?? null, + ); } break; } diff --git a/src/renderer/features/player/context/player-context.tsx b/src/renderer/features/player/context/player-context.tsx index 24c1fe8c5..fba178c5a 100644 --- a/src/renderer/features/player/context/player-context.tsx +++ b/src/renderer/features/player/context/player-context.tsx @@ -39,7 +39,12 @@ import { import { Play, PlayerRepeat, PlayerShuffle } from '/@/shared/types/types'; export interface PlayerContext { - addToQueueByData: (data: Song[], type: AddToQueueType, playSongId?: string) => void; + addToQueueByData: ( + data: Song[], + type: AddToQueueType, + playSongId?: string, + contextPlaylistId?: null | string, + ) => void; addToQueueByFetch: ( serverId: string, id: string[], @@ -137,6 +142,23 @@ const getRootQueryKey = (itemType: LibraryItem, serverId: string) => { } }; +const isReplaceQueueType = (type: AddToQueueType): boolean => { + if (typeof type === 'object') return false; + return type === Play.NOW || type === Play.SHUFFLE; +}; + +// HashRouter puts the route in location.hash, not pathname. +const inferPlaylistContextFromUrl = (): null | string => { + const route = window.location.hash.replace(/^#/, ''); + const match = route.match(/^\/playlists\/([^/]+)/); + return match ? match[1] : null; +}; + +// Stamps each song with the playlist it was queued from, so the sidebar highlight +// can be derived from whichever song is currently playing (see useCurrentPlaylistContextId). +const tagPlaylistContext = (songs: Song[], contextPlaylistId: string): Song[] => + songs.map((song) => ({ ...song, _contextPlaylistId: contextPlaylistId })); + export const PlayerProvider = ({ children }: { children: React.ReactNode }) => { const { t } = useTranslation(); const queryClient = useQueryClient(); @@ -187,9 +209,20 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => { }, [doNotShowAgain, setDoNotShowAgain, t]); const addToQueueByData = useCallback( - (data: Song[], type: AddToQueueType, playSongId?: string) => { + ( + data: Song[], + type: AddToQueueType, + playSongId?: string, + contextPlaylistId?: null | string, + ) => { const filters = useSettingsStore.getState().playback.filters; - const filteredData = filterSongsByPlayerFilters(data, filters); + let filteredData = filterSongsByPlayerFilters(data, filters); + const resolvedContextId = + contextPlaylistId ?? + (isReplaceQueueType(type) ? inferPlaylistContextFromUrl() : null); + if (resolvedContextId) { + filteredData = tagPlaylistContext(filteredData, resolvedContextId); + } if (typeof type === 'object' && 'edge' in type && type.edge !== null) { const edge = type.edge === 'top' ? 'top' : 'bottom'; @@ -279,7 +312,21 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => { } const filters = useSettingsStore.getState().playback.filters; - const filteredSongs = filterSongsByPlayerFilters(sortedSongs, filters); + let filteredSongs = filterSongsByPlayerFilters(sortedSongs, filters); + + // Songs from multiple playlists are merged together, so there is no single + // playlist to attribute them to: skip tagging (and URL inference) entirely. + const isMultiPlaylist = itemType === LibraryItem.PLAYLIST && id.length > 1; + const explicitId = + itemType === LibraryItem.PLAYLIST && id.length === 1 ? id[0] : null; + const resolvedContextId = + explicitId ?? + (!isMultiPlaylist && isReplaceQueueType(type) + ? inferPlaylistContextFromUrl() + : null); + if (resolvedContextId) { + filteredSongs = tagPlaylistContext(filteredSongs, resolvedContextId); + } if (typeof type === 'object' && 'edge' in type && type.edge !== null) { const edge = type.edge === 'top' ? 'top' : 'bottom'; diff --git a/src/renderer/features/sidebar/components/sidebar-playlist-list.module.css b/src/renderer/features/sidebar/components/sidebar-playlist-list.module.css index d8d8bbed5..7134c543e 100644 --- a/src/renderer/features/sidebar/components/sidebar-playlist-list.module.css +++ b/src/renderer/features/sidebar/components/sidebar-playlist-list.module.css @@ -136,6 +136,10 @@ white-space: nowrap; } +.name-active { + color: var(--theme-colors-primary); +} + .image-container { flex-shrink: 0; width: 3rem; diff --git a/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx b/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx index a31c4153e..69b9a2695 100644 --- a/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx +++ b/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx @@ -28,6 +28,7 @@ import { useDragDrop } from '/@/renderer/hooks/use-drag-drop'; import { useDragMonitor } from '/@/renderer/hooks/use-drag-monitor'; import { AppRoute } from '/@/renderer/router/routes'; import { + useCurrentPlaylistContextId, useCurrentServer, useCurrentServerId, usePermissions, @@ -116,6 +117,8 @@ export const PlaylistRowButton = memo( const sidebarPlaylistSorting = useSidebarPlaylistSorting(); const sidebarPlaylistMode = useSidebarPlaylistMode(); const isCompact = sidebarPlaylistMode === 'compact'; + const activePlaylistId = useCurrentPlaylistContextId(); + const isActive = activePlaylistId === item.id; const [isHovered, setIsHovered] = useState(false); const isSmartPlaylist = Boolean(item.rules); @@ -292,7 +295,13 @@ export const PlaylistRowButton = memo( > {isCompact ? ( <> - + {name} {isHovered && ( @@ -307,7 +316,13 @@ export const PlaylistRowButton = memo(
- + {name}
diff --git a/src/renderer/store/player.store.ts b/src/renderer/store/player.store.ts index 06ea44b33..d0b26e4c7 100644 --- a/src/renderer/store/player.store.ts +++ b/src/renderer/store/player.store.ts @@ -1640,6 +1640,7 @@ export const usePlayerStoreBase = createWithEqualityFn()( const excludedPlayerKeys = ['playerNum', 'seekToTimestamp', 'status']; // If we're not restoring the play queue, we don't need the index property + // (it is meaningless without the queue) if (!shouldRestorePlayQueue) { excludedPlayerKeys.push('index'); } @@ -2076,6 +2077,7 @@ export const updateQueueSong = (songId: string, updatedSong: Song) => { const uniqueId = song._uniqueId; state.queue.songs[song._uniqueId] = { ...updatedSong, + _contextPlaylistId: song._contextPlaylistId, _uniqueId: uniqueId, }; } @@ -2083,6 +2085,10 @@ export const updateQueueSong = (songId: string, updatedSong: Song) => { }); }; +export const useCurrentPlaylistContextId = () => { + return usePlayerStoreBase((state) => state.getCurrentSong()?._contextPlaylistId ?? null); +}; + export const usePlayerMuted = () => { return usePlayerStoreBase((state) => state.player.muted); }; diff --git a/src/shared/types/domain-types.ts b/src/shared/types/domain-types.ts index dbf2d1e1e..18205e670 100644 --- a/src/shared/types/domain-types.ts +++ b/src/shared/types/domain-types.ts @@ -73,6 +73,7 @@ export interface QueueData { } export type QueueSong = Song & { + _contextPlaylistId?: null | string; _uniqueId: string; };