From e696c0c6367679518a2f10583d5e6f37f7a3c057 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Fri, 2 Jan 2026 01:32:51 -0800 Subject: [PATCH] add context menu item to show song in file manager (#1397) --- .../actions/show-in-file-explorer-action.tsx | 53 +++++++++++++++++++ .../menus/playlist-song-context-menu.tsx | 2 + .../context-menu/menus/queue-context-menu.tsx | 2 + .../context-menu/menus/song-context-menu.tsx | 2 + 4 files changed, 59 insertions(+) create mode 100644 src/renderer/features/context-menu/actions/show-in-file-explorer-action.tsx diff --git a/src/renderer/features/context-menu/actions/show-in-file-explorer-action.tsx b/src/renderer/features/context-menu/actions/show-in-file-explorer-action.tsx new file mode 100644 index 000000000..423d6cb9d --- /dev/null +++ b/src/renderer/features/context-menu/actions/show-in-file-explorer-action.tsx @@ -0,0 +1,53 @@ +import isElectron from 'is-electron'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { ContextMenu } from '/@/shared/components/context-menu/context-menu'; +import { toast } from '/@/shared/components/toast/toast'; +import { QueueSong, Song } from '/@/shared/types/domain-types'; + +interface ShowInFileExplorerActionProps { + items: QueueSong[] | Song[]; +} + +const utils = isElectron() ? window.api.utils : null; + +export const ShowInFileExplorerAction = ({ items }: ShowInFileExplorerActionProps) => { + const { t } = useTranslation(); + + const onSelect = useCallback(async () => { + if (!utils) { + return; + } + + const firstItem = items[0]; + if (!firstItem?.path) { + return; + } + + try { + await utils.openItem(firstItem.path); + } catch (error) { + toast.error({ + message: (error as Error).message, + title: t('error.openError', { + postProcess: 'sentenceCase', + }), + }); + } + }, [items, t]); + + if (!utils) { + return null; + } + + const firstItem = items[0]; + const hasPath = firstItem?.path !== null; + const isDisabled = items.length > 1 || !hasPath; + + return ( + + {t('page.itemDetail.openFile', { postProcess: 'sentenceCase' })} + + ); +}; diff --git a/src/renderer/features/context-menu/menus/playlist-song-context-menu.tsx b/src/renderer/features/context-menu/menus/playlist-song-context-menu.tsx index a55e59855..34cdab328 100644 --- a/src/renderer/features/context-menu/menus/playlist-song-context-menu.tsx +++ b/src/renderer/features/context-menu/menus/playlist-song-context-menu.tsx @@ -10,6 +10,7 @@ import { RemoveFromPlaylistAction } from '/@/renderer/features/context-menu/acti import { SetFavoriteAction } from '/@/renderer/features/context-menu/actions/set-favorite-action'; import { SetRatingAction } from '/@/renderer/features/context-menu/actions/set-rating-action'; import { ShareAction } from '/@/renderer/features/context-menu/actions/share-action'; +import { ShowInFileExplorerAction } from '/@/renderer/features/context-menu/actions/show-in-file-explorer-action'; import { ContextMenuPreview } from '/@/renderer/features/context-menu/components/context-menu-preview'; import { ContextMenu } from '/@/shared/components/context-menu/context-menu'; import { LibraryItem, Song } from '/@/shared/types/domain-types'; @@ -43,6 +44,7 @@ export const PlaylistSongContextMenu = ({ items, type }: PlaylistSongContextMenu + diff --git a/src/renderer/features/context-menu/menus/queue-context-menu.tsx b/src/renderer/features/context-menu/menus/queue-context-menu.tsx index caac1be4c..0e050325b 100644 --- a/src/renderer/features/context-menu/menus/queue-context-menu.tsx +++ b/src/renderer/features/context-menu/menus/queue-context-menu.tsx @@ -10,6 +10,7 @@ import { RemoveFromQueueAction } from '/@/renderer/features/context-menu/actions import { SetFavoriteAction } from '/@/renderer/features/context-menu/actions/set-favorite-action'; import { SetRatingAction } from '/@/renderer/features/context-menu/actions/set-rating-action'; import { ShareAction } from '/@/renderer/features/context-menu/actions/share-action'; +import { ShowInFileExplorerAction } from '/@/renderer/features/context-menu/actions/show-in-file-explorer-action'; import { ShuffleItemsAction } from '/@/renderer/features/context-menu/actions/shuffle-items-action'; import { ContextMenuPreview } from '/@/renderer/features/context-menu/components/context-menu-preview'; import { ContextMenu } from '/@/shared/components/context-menu/context-menu'; @@ -45,6 +46,7 @@ export const QueueContextMenu = ({ items }: QueueContextMenuProps) => { + diff --git a/src/renderer/features/context-menu/menus/song-context-menu.tsx b/src/renderer/features/context-menu/menus/song-context-menu.tsx index 56fd465f1..84865e391 100644 --- a/src/renderer/features/context-menu/menus/song-context-menu.tsx +++ b/src/renderer/features/context-menu/menus/song-context-menu.tsx @@ -9,6 +9,7 @@ import { PlayTrackRadioAction } from '/@/renderer/features/context-menu/actions/ import { SetFavoriteAction } from '/@/renderer/features/context-menu/actions/set-favorite-action'; import { SetRatingAction } from '/@/renderer/features/context-menu/actions/set-rating-action'; import { ShareAction } from '/@/renderer/features/context-menu/actions/share-action'; +import { ShowInFileExplorerAction } from '/@/renderer/features/context-menu/actions/show-in-file-explorer-action'; import { ContextMenuPreview } from '/@/renderer/features/context-menu/components/context-menu-preview'; import { ContextMenu } from '/@/shared/components/context-menu/context-menu'; import { LibraryItem, Song } from '/@/shared/types/domain-types'; @@ -40,6 +41,7 @@ export const SongContextMenu = ({ items, type }: SongContextMenuProps) => { +