From 55ebc7d74a163f2c06a0bfb588c9bce3b8121b2d Mon Sep 17 00:00:00 2001 From: jeffvli Date: Wed, 3 Dec 2025 16:25:28 -0800 Subject: [PATCH] improve image column play handler to support long press --- .../item-list/helpers/item-list-controls.ts | 6 +- .../item-table-list/columns/image-column.tsx | 7 +- .../components/album-detail-content.tsx | 7 +- .../components/expanded-album-list-item.tsx | 21 ++--- .../album-artist-detail-content.tsx | 6 +- ...bum-artist-detail-top-songs-list-route.tsx | 6 +- .../components/folder-list-content.tsx | 25 ++++-- .../player/context/player-context.tsx | 8 +- .../playlist-detail-song-list-table.tsx | 6 +- src/renderer/store/player.store.ts | 86 ++++++++++++++++++- 10 files changed, 127 insertions(+), 51 deletions(-) diff --git a/src/renderer/components/item-list/helpers/item-list-controls.ts b/src/renderer/components/item-list/helpers/item-list-controls.ts index 4f8faff38..fa18dc52f 100644 --- a/src/renderer/components/item-list/helpers/item-list-controls.ts +++ b/src/renderer/components/item-list/helpers/item-list-controls.ts @@ -245,11 +245,7 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs return; } - player.addToQueueByData(songsToAdd, Play.NOW); - - const targetIndex = clickedIndex - startIndex; - - player.mediaPlayByIndex(targetIndex); + player.addToQueueByData(songsToAdd, Play.NOW, item.id); return; } diff --git a/src/renderer/components/item-list/item-table-list/columns/image-column.tsx b/src/renderer/components/item-list/item-table-list/columns/image-column.tsx index eed3bc1ae..b381c3c03 100644 --- a/src/renderer/components/item-list/item-table-list/columns/image-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/image-column.tsx @@ -35,7 +35,9 @@ export const ImageColumn = (props: ItemTableListInnerColumn) => { // For SONG items, use double click behavior if ( - (props.itemType === LibraryItem.SONG || props.itemType === LibraryItem.PLAYLIST_SONG) && + (props.itemType === LibraryItem.SONG || + props.itemType === LibraryItem.PLAYLIST_SONG || + item._itemType === LibraryItem.SONG) && props.controls?.onDoubleClick ) { // Calculate the index based on rowIndex, accounting for header if enabled @@ -48,6 +50,9 @@ export const ImageColumn = (props: ItemTableListInnerColumn) => { internalState, item, itemType: props.itemType, + meta: { + playType, + }, }); return; } diff --git a/src/renderer/features/albums/components/album-detail-content.tsx b/src/renderer/features/albums/components/album-detail-content.tsx index 47a7c9773..955875c90 100644 --- a/src/renderer/features/albums/components/album-detail-content.tsx +++ b/src/renderer/features/albums/components/album-detail-content.tsx @@ -568,16 +568,17 @@ const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => { const overrideControls: Partial = useMemo(() => { return { - onDoubleClick: ({ index, internalState, item }) => { + onDoubleClick: ({ index, internalState, item, meta }) => { if (!item) { return; } + const playType = (meta?.playType as Play) || Play.NOW; + const items = internalState?.getData() as Song[]; if (index !== undefined) { - player.addToQueueByData(items, Play.NOW); - player.mediaPlayByIndex(index); + player.addToQueueByData(items, playType, item.id); } }, }; diff --git a/src/renderer/features/albums/components/expanded-album-list-item.tsx b/src/renderer/features/albums/components/expanded-album-list-item.tsx index 9c0f6d2e1..1b427f8e0 100644 --- a/src/renderer/features/albums/components/expanded-album-list-item.tsx +++ b/src/renderer/features/albums/components/expanded-album-list-item.tsx @@ -56,19 +56,10 @@ interface TrackRowProps { player: ReturnType; serverId: string; song: NonNullable[0]; - songIndex: number; songs: Song[]; } -const TrackRow = ({ - controls, - internalState, - player, - serverId, - song, - songIndex, - songs, -}: TrackRowProps) => { +const TrackRow = ({ controls, internalState, player, serverId, song, songs }: TrackRowProps) => { const rowId = internalState.extractRowId(song); const isSelected = useItemSelectionState(internalState, rowId); const isDraggingState = useItemDraggingState(internalState, rowId); @@ -122,11 +113,10 @@ const TrackRow = ({ const mergedRef = useMergedRef(containerRef, dragRef); const handleDoubleClick = useCallback(() => { - if (songs && songIndex !== undefined) { - player.addToQueueByData(songs, Play.NOW); - player.mediaPlayByIndex(songIndex); + if (songs && song.id) { + player.addToQueueByData(songs, Play.NOW, song.id); } - }, [player, songs, songIndex]); + }, [player, songs, song.id]); return (
- {songs?.map((song, index) => ( + {songs?.map((song) => ( player={player} serverId={serverId} song={song} - songIndex={index} songs={fullSongs || []} /> ))} diff --git a/src/renderer/features/artists/components/album-artist-detail-content.tsx b/src/renderer/features/artists/components/album-artist-detail-content.tsx index f2d218c7b..7dcec66c5 100644 --- a/src/renderer/features/artists/components/album-artist-detail-content.tsx +++ b/src/renderer/features/artists/components/album-artist-detail-content.tsx @@ -221,16 +221,16 @@ const AlbumArtistMetadataTopSongs = ({ const overrideControls: Partial = useMemo(() => { return { - onDoubleClick: ({ index, internalState, item }) => { + onDoubleClick: ({ index, internalState, item, meta }) => { if (!item) { return; } + const playType = (meta?.playType as Play) || Play.NOW; const items = internalState?.getData() as Song[]; if (index !== undefined) { - player.addToQueueByData(items, Play.NOW); - player.mediaPlayByIndex(index); + player.addToQueueByData(items, playType, item.id); } }, }; diff --git a/src/renderer/features/artists/routes/album-artist-detail-top-songs-list-route.tsx b/src/renderer/features/artists/routes/album-artist-detail-top-songs-list-route.tsx index a8f08b69a..30e645cd1 100644 --- a/src/renderer/features/artists/routes/album-artist-detail-top-songs-list-route.tsx +++ b/src/renderer/features/artists/routes/album-artist-detail-top-songs-list-route.tsx @@ -65,16 +65,16 @@ const AlbumArtistDetailTopSongsListRoute = () => { const overrideControls: Partial = useMemo(() => { return { - onDoubleClick: ({ index, internalState, item }) => { + onDoubleClick: ({ index, internalState, item, meta }) => { if (!item) { return; } + const playType = (meta?.playType as Play) || Play.NOW; const items = internalState?.getData() as Song[]; if (index !== undefined) { - player.addToQueueByData(items, Play.NOW); - player.mediaPlayByIndex(index); + player.addToQueueByData(items, playType, item.id); } }, }; diff --git a/src/renderer/features/folders/components/folder-list-content.tsx b/src/renderer/features/folders/components/folder-list-content.tsx index 19d57f393..55970ff8c 100644 --- a/src/renderer/features/folders/components/folder-list-content.tsx +++ b/src/renderer/features/folders/components/folder-list-content.tsx @@ -117,7 +117,7 @@ export const FolderListView = ({ folderQuery }: FolderListViewProps) => { const overrideControls = useMemo(() => { return { - onDoubleClick: ({ index, internalState, item }: DefaultItemControlProps) => { + onDoubleClick: ({ internalState, item, meta }: DefaultItemControlProps) => { if (!item) { return; } @@ -127,17 +127,24 @@ export const FolderListView = ({ folderQuery }: FolderListViewProps) => { return navigateToFolder(folder.id, folder.name); } - const items = internalState?.getData() as Song[]; + const playType = (meta?.playType as Play) || Play.NOW; - const songCount = items.filter( - (item) => item._itemType === LibraryItem.SONG, - ).length; + const data = internalState?.getData(); + if (!data) { + return; + } - const indexesToSkip = items.length - songCount; + const validSongs = data.filter((d): d is Song => { + return ( + (d as unknown as { _itemType: LibraryItem })._itemType === LibraryItem.SONG + ); + }) as Song[]; - const startIndex = indexesToSkip + (index ?? 0); - player.addToQueueByData(items, Play.NOW); - player.mediaPlayByIndex(startIndex); + if (validSongs.length === 0) { + return; + } + + player.addToQueueByData(validSongs, playType, item.id); }, }; }, [navigateToFolder, player]); diff --git a/src/renderer/features/player/context/player-context.tsx b/src/renderer/features/player/context/player-context.tsx index b15c375ff..1c44d51a7 100644 --- a/src/renderer/features/player/context/player-context.tsx +++ b/src/renderer/features/player/context/player-context.tsx @@ -39,7 +39,7 @@ import { import { Play, PlayerRepeat, PlayerShuffle } from '/@/shared/types/types'; export interface PlayerContext { - addToQueueByData: (data: Song[], type: AddToQueueType) => void; + addToQueueByData: (data: Song[], type: AddToQueueType, playSongId?: string) => void; addToQueueByFetch: ( serverId: string, id: string[], @@ -208,7 +208,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => { ); const addToQueueByData = useCallback( - (data: Song[], type: AddToQueueType) => { + (data: Song[], type: AddToQueueType, playSongId?: string) => { if (typeof type === 'object' && 'edge' in type && type.edge !== null) { const edge = type.edge === 'top' ? 'top' : 'bottom'; @@ -217,14 +217,14 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => { meta: { data: data.length, edge, type, uniqueId: type.uniqueId }, }); - storeActions.addToQueueByUniqueId(data, type.uniqueId, edge); + storeActions.addToQueueByUniqueId(data, type.uniqueId, edge, playSongId); } else { logFn.debug(logMsg[LogCategory.PLAYER].addToQueueByType, { category: LogCategory.PLAYER, meta: { data: data.length, type }, }); - storeActions.addToQueueByType(data, type as Play); + storeActions.addToQueueByType(data, type as Play, playSongId); } }, [storeActions], diff --git a/src/renderer/features/playlists/components/playlist-detail-song-list-table.tsx b/src/renderer/features/playlists/components/playlist-detail-song-list-table.tsx index 1ea3b8a5d..0cde2fd6b 100644 --- a/src/renderer/features/playlists/components/playlist-detail-song-list-table.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-song-list-table.tsx @@ -76,16 +76,16 @@ export const PlaylistDetailSongListTable = forwardRef = useMemo(() => { return { - onDoubleClick: ({ index, internalState, item }) => { + onDoubleClick: ({ index, internalState, item, meta }) => { if (!item) { return; } + const playType = (meta?.playType as Play) || Play.NOW; const items = internalState?.getData() as Song[]; if (index !== undefined) { - player.addToQueueByData(items, Play.NOW); - player.mediaPlayByIndex(index); + player.addToQueueByData(items, playType, item.id); } }, }; diff --git a/src/renderer/store/player.store.ts b/src/renderer/store/player.store.ts index f688eb025..eebae1ad8 100644 --- a/src/renderer/store/player.store.ts +++ b/src/renderer/store/player.store.ts @@ -29,8 +29,13 @@ export interface PlayerState extends Actions, State {} export type QueueGroupingProperty = keyof QueueSong; interface Actions { - addToQueueByType: (items: Song[], playType: Play) => void; - addToQueueByUniqueId: (items: Song[], uniqueId: string, edge: 'bottom' | 'top') => void; + addToQueueByType: (items: Song[], playType: Play, playSongId?: string) => void; + addToQueueByUniqueId: ( + items: Song[], + uniqueId: string, + edge: 'bottom' | 'top', + playSongId?: string, + ) => void; clearQueue: () => void; clearSelected: (items: QueueSong[]) => void; decreaseVolume: (value: number) => void; @@ -260,10 +265,15 @@ export const usePlayerStoreBase = createWithEqualityFn()( persist( subscribeWithSelector( immer((set, get) => ({ - addToQueueByType: (items, playType) => { + addToQueueByType: (items, playType, playSongId) => { const newItems = items.map(toQueueSong); const newUniqueIds = newItems.map((item) => item._uniqueId); + // Find the target song's uniqueId if playSongId is provided + const targetSongUniqueId = playSongId + ? newItems.find((item) => item.id === playSongId)?._uniqueId + : undefined; + const queueType = getQueueType(); switch (queueType) { @@ -760,10 +770,47 @@ export const usePlayerStoreBase = createWithEqualityFn()( break; } } + + // If playSongId is provided, find the song and start playback on it + if (targetSongUniqueId) { + set((state) => { + const queue = state.getQueue(); + const queueIndex = queue.items.findIndex( + (item) => item._uniqueId === targetSongUniqueId, + ); + + if (queueIndex !== -1) { + if ( + state.player.shuffle === PlayerShuffle.TRACK && + state.queue.shuffled.length > 0 + ) { + // Find the shuffled position for this queue index + const shuffledPosition = state.queue.shuffled.findIndex( + (idx) => idx === queueIndex, + ); + if (shuffledPosition !== -1) { + state.player.index = shuffledPosition; + } else { + state.player.index = queueIndex; + } + } else { + state.player.index = queueIndex; + } + state.player.status = PlayerStatus.PLAYING; + setTimestampStore(0); + } + }); + } }, - addToQueueByUniqueId: (items, uniqueId, edge) => { + addToQueueByUniqueId: (items, uniqueId, edge, playSongId) => { const newItems = items.map(toQueueSong); const newUniqueIds = newItems.map((item) => item._uniqueId); + + // Find the target song's uniqueId if playSongId is provided + const targetSongUniqueId = playSongId + ? newItems.find((item) => item.id === playSongId)?._uniqueId + : undefined; + const queueType = getQueueType(); set((state) => { @@ -888,6 +935,37 @@ export const usePlayerStoreBase = createWithEqualityFn()( } } }); + + // If playSongId is provided, find the song and start playback on it + if (targetSongUniqueId) { + set((state) => { + const queue = state.getQueue(); + const queueIndex = queue.items.findIndex( + (item) => item._uniqueId === targetSongUniqueId, + ); + + if (queueIndex !== -1) { + if ( + state.player.shuffle === PlayerShuffle.TRACK && + state.queue.shuffled.length > 0 + ) { + // Find the shuffled position for this queue index + const shuffledPosition = state.queue.shuffled.findIndex( + (idx) => idx === queueIndex, + ); + if (shuffledPosition !== -1) { + state.player.index = shuffledPosition; + } else { + state.player.index = queueIndex; + } + } else { + state.player.index = queueIndex; + } + state.player.status = PlayerStatus.PLAYING; + setTimestampStore(0); + } + }); + } }, clearQueue: () => { set((state) => {