diff --git a/src/main/index.ts b/src/main/index.ts index 19cd9d3..8add8c6 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1336,6 +1336,51 @@ app.whenReady().then(async () => { } ) + ipcMain.handle( + 'voice:showSourceContextMenu', + ( + event, + reference: { sessionId: string; localId: number; createTime: number; svrId?: string | number } + ) => { + const window = BrowserWindow.fromWebContents(event.sender) + if (!window || !voiceService) return { success: false, error: '语音服务未初始化' } + + const revealSource = async (): Promise => { + try { + const result = await voiceService!.resolveSource( + reference.sessionId, + reference.localId, + reference.createTime, + reference.svrId + ) + if (!result.success) throw new Error(result.error) + + const directory = join(app.getPath('userData'), 'voice-sources') + await fsPromises.mkdir(directory, { recursive: true }) + const fileName = `voice_${reference.localId}_${reference.createTime}_${result.source.sourceHash.slice(0, 12)}.silk` + const filePath = join(directory, fileName) + await fsPromises.writeFile(filePath, result.source.data) + shell.showItemInFolder(filePath) + } catch (error) { + await dialog.showMessageBox(window, { + type: 'error', + title: '查找语音源文件失败', + message: '无法提取这条语音的源文件', + detail: error instanceof Error ? error.message : String(error) + }) + } + } + + Menu.buildFromTemplate([ + { + label: '查找语音源文件', + click: () => void revealSource() + } + ]).popup({ window }) + return { success: true } + } + ) + ipcMain.handle('voice:getModelStatus', async () => { if (!voiceRecognition) throw new Error('Voice recognition is not initialized') return voiceRecognition.getModelStatus() diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 9740bee..c444aaa 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -307,6 +307,9 @@ declare global { createTime: number, svrId?: string | number ) => Promise<{ success: boolean; data?: string; error?: string }> + showVoiceSourceContextMenu: ( + reference: VoiceMessageReference + ) => Promise<{ success: boolean; error?: string }> getVoiceModelStatus: () => Promise downloadVoiceModel: () => Promise cancelVoiceModelDownload: () => Promise<{ success: boolean }> diff --git a/src/preload/index.ts b/src/preload/index.ts index 4914e28..6144a6d 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -178,6 +178,8 @@ const api = { copyImage: (base64String) => ipcRenderer.invoke('copy-image', base64String), getVoiceData: (sessionId: string, localId: number, createTime: number, svrId?: string | number) => ipcRenderer.invoke('db:getVoiceData', sessionId, localId, createTime, svrId), + showVoiceSourceContextMenu: (reference: VoiceMessageReference) => + ipcRenderer.invoke('voice:showSourceContextMenu', reference), getVoiceModelStatus: (): Promise => ipcRenderer.invoke('voice:getModelStatus'), downloadVoiceModel: (): Promise => ipcRenderer.invoke('voice:downloadModel'), diff --git a/src/renderer/src/components/VoicePlayer.tsx b/src/renderer/src/components/VoicePlayer.tsx index 0fa4988..385a63a 100644 --- a/src/renderer/src/components/VoicePlayer.tsx +++ b/src/renderer/src/components/VoicePlayer.tsx @@ -193,6 +193,15 @@ export function VoicePlayer({ window.dispatchEvent(new Event('wxe:open-voice-recognition-settings')) }, []) + const handleContextMenu = useCallback( + (event: ReactMouseEvent) => { + event.preventDefault() + event.stopPropagation() + void window.api.showVoiceSourceContextMenu(voiceReference) + }, + [voiceReference] + ) + const formatDuration = (seconds: number | undefined): string => { if (!seconds || !isFinite(seconds)) return '0:00' const mins = Math.floor(seconds / 60) @@ -205,6 +214,7 @@ export function VoicePlayer({
{isPlaying ? '⏸' : '▶'} {loading ? (