diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index c8f785ac..5988afa5 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -157,6 +157,7 @@ "loginRateError": "too many login attempts, please try again in a few seconds", "mpvRequired": "MPV required", "networkError": "a network error occurred", + "openError": "could not open file", "playbackError": "an error occurred when trying to play the media", "remoteDisableError": "an error occurred when trying to $t(common.disable) the remote server", "remoteEnableError": "an error occurred when trying to $t(common.enable) the remote server", @@ -352,6 +353,11 @@ "recentlyPlayed": "recently played", "title": "$t(common.home)" }, + "itemDetail": { + "copyPath": "copy path to clipboard", + "copiedPath": "path copied successfully", + "openFile": "show track in file manager" + }, "playlistList": { "title": "$t(entity.playlist_other)" }, diff --git a/src/main/main.ts b/src/main/main.ts index 107c6562..8775dfde 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -342,6 +342,20 @@ const createWindow = async (first = true) => { } }); + ipcMain.handle('open-item', async (_event, path: string) => { + return new Promise((resolve, reject) => { + access(path, constants.F_OK, (error) => { + if (error) { + reject(error); + return; + } + + shell.showItemInFolder(path); + resolve(); + }); + }); + }); + const globalMediaKeysEnabled = store.get('global_media_hotkeys', true) as boolean; if (globalMediaKeysEnabled) { diff --git a/src/main/preload/utils.ts b/src/main/preload/utils.ts index 12f83ba7..dc4a4132 100644 --- a/src/main/preload/utils.ts +++ b/src/main/preload/utils.ts @@ -10,6 +10,10 @@ const restoreQueue = () => { ipcRenderer.send('player-restore-queue'); }; +const openItem = async (path: string) => { + return ipcRenderer.invoke('open-item', path); +}; + const onSaveQueue = (cb: (event: IpcRendererEvent) => void) => { ipcRenderer.on('renderer-save-queue', cb); }; @@ -51,6 +55,7 @@ export const utils = { mainMessageListener, onRestoreQueue, onSaveQueue, + openItem, playerErrorListener, restoreQueue, saveQueue, diff --git a/src/renderer/features/item-details/components/item-details-modal.tsx b/src/renderer/features/item-details/components/item-details-modal.tsx index 89dcb186..9126a2c3 100644 --- a/src/renderer/features/item-details/components/item-details-modal.tsx +++ b/src/renderer/features/item-details/components/item-details-modal.tsx @@ -9,6 +9,7 @@ import { formatSizeString } from '/@/renderer/utils/format-size-string'; import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify'; import { Rating, Spoiler } from '/@/renderer/components'; import { sanitize } from '/@/renderer/utils/sanitize'; +import { SongPath } from '/@/renderer/features/item-details/components/song-path'; export type ItemDetailsModalProps = { item: Album | AlbumArtist | Song; @@ -156,7 +157,7 @@ const AlbumArtistPropertyMapping: ItemDetailRow[] = [ const SongPropertyMapping: ItemDetailRow[] = [ { key: 'name', label: 'common.title' }, - { key: 'path', label: 'common.path' }, + { key: 'path', label: 'common.path', render: SongPath }, { label: 'entity.albumArtist_one', render: formatArtists }, { key: 'album', label: 'entity.album_one' }, { key: 'discNumber', label: 'common.disc' }, diff --git a/src/renderer/features/item-details/components/song-path.tsx b/src/renderer/features/item-details/components/song-path.tsx new file mode 100644 index 00000000..33f66307 --- /dev/null +++ b/src/renderer/features/item-details/components/song-path.tsx @@ -0,0 +1,67 @@ +import { ActionIcon, CopyButton, Group } from '@mantine/core'; +import isElectron from 'is-electron'; +import { useTranslation } from 'react-i18next'; +import { RiCheckFill, RiClipboardFill, RiExternalLinkFill } from 'react-icons/ri'; +import { Tooltip, toast } from '/@/renderer/components'; +import styled from 'styled-components'; + +const util = isElectron() ? window.electron.utils : null; + +export type SongPathProps = { + path: string | null; +}; + +const PathText = styled.div` + user-select: all; +`; + +export const SongPath = ({ path }: SongPathProps) => { + const { t } = useTranslation(); + + if (!path) return null; + + return ( + + + {({ copied, copy }) => ( + + + {copied ? : } + + + )} + + {util && ( + + + { + util.openItem(path).catch((error) => { + toast.error({ + message: (error as Error).message, + title: t('error.openError', { + postProcess: 'sentenceCase', + }), + }); + }); + }} + /> + + + )} + {path} + + ); +};