mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-06 13:54:10 +08:00
feat: 增加一个查找语音文件
This commit is contained in:
@@ -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<void> => {
|
||||||
|
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 () => {
|
ipcMain.handle('voice:getModelStatus', async () => {
|
||||||
if (!voiceRecognition) throw new Error('Voice recognition is not initialized')
|
if (!voiceRecognition) throw new Error('Voice recognition is not initialized')
|
||||||
return voiceRecognition.getModelStatus()
|
return voiceRecognition.getModelStatus()
|
||||||
|
|||||||
Vendored
+3
@@ -307,6 +307,9 @@ declare global {
|
|||||||
createTime: number,
|
createTime: number,
|
||||||
svrId?: string | number
|
svrId?: string | number
|
||||||
) => Promise<{ success: boolean; data?: string; error?: string }>
|
) => Promise<{ success: boolean; data?: string; error?: string }>
|
||||||
|
showVoiceSourceContextMenu: (
|
||||||
|
reference: VoiceMessageReference
|
||||||
|
) => Promise<{ success: boolean; error?: string }>
|
||||||
getVoiceModelStatus: () => Promise<VoiceModelStatus>
|
getVoiceModelStatus: () => Promise<VoiceModelStatus>
|
||||||
downloadVoiceModel: () => Promise<VoiceModelDownloadResult>
|
downloadVoiceModel: () => Promise<VoiceModelDownloadResult>
|
||||||
cancelVoiceModelDownload: () => Promise<{ success: boolean }>
|
cancelVoiceModelDownload: () => Promise<{ success: boolean }>
|
||||||
|
|||||||
@@ -178,6 +178,8 @@ const api = {
|
|||||||
copyImage: (base64String) => ipcRenderer.invoke('copy-image', base64String),
|
copyImage: (base64String) => ipcRenderer.invoke('copy-image', base64String),
|
||||||
getVoiceData: (sessionId: string, localId: number, createTime: number, svrId?: string | number) =>
|
getVoiceData: (sessionId: string, localId: number, createTime: number, svrId?: string | number) =>
|
||||||
ipcRenderer.invoke('db:getVoiceData', sessionId, localId, createTime, svrId),
|
ipcRenderer.invoke('db:getVoiceData', sessionId, localId, createTime, svrId),
|
||||||
|
showVoiceSourceContextMenu: (reference: VoiceMessageReference) =>
|
||||||
|
ipcRenderer.invoke('voice:showSourceContextMenu', reference),
|
||||||
getVoiceModelStatus: (): Promise<VoiceModelStatus> => ipcRenderer.invoke('voice:getModelStatus'),
|
getVoiceModelStatus: (): Promise<VoiceModelStatus> => ipcRenderer.invoke('voice:getModelStatus'),
|
||||||
downloadVoiceModel: (): Promise<VoiceModelDownloadResult> =>
|
downloadVoiceModel: (): Promise<VoiceModelDownloadResult> =>
|
||||||
ipcRenderer.invoke('voice:downloadModel'),
|
ipcRenderer.invoke('voice:downloadModel'),
|
||||||
|
|||||||
@@ -193,6 +193,15 @@ export function VoicePlayer({
|
|||||||
window.dispatchEvent(new Event('wxe:open-voice-recognition-settings'))
|
window.dispatchEvent(new Event('wxe:open-voice-recognition-settings'))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleContextMenu = useCallback(
|
||||||
|
(event: ReactMouseEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
void window.api.showVoiceSourceContextMenu(voiceReference)
|
||||||
|
},
|
||||||
|
[voiceReference]
|
||||||
|
)
|
||||||
|
|
||||||
const formatDuration = (seconds: number | undefined): string => {
|
const formatDuration = (seconds: number | undefined): string => {
|
||||||
if (!seconds || !isFinite(seconds)) return '0:00'
|
if (!seconds || !isFinite(seconds)) return '0:00'
|
||||||
const mins = Math.floor(seconds / 60)
|
const mins = Math.floor(seconds / 60)
|
||||||
@@ -205,6 +214,7 @@ export function VoicePlayer({
|
|||||||
<div
|
<div
|
||||||
className={`voice-message ${loading ? 'voice-loading' : ''} ${error && !audioUrl ? 'voice-error' : ''}`}
|
className={`voice-message ${loading ? 'voice-loading' : ''} ${error && !audioUrl ? 'voice-error' : ''}`}
|
||||||
onClick={handlePlayPause}
|
onClick={handlePlayPause}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
>
|
>
|
||||||
<span className={`voice-icon ${isPlaying ? 'playing' : ''}`}>{isPlaying ? '⏸' : '▶'}</span>
|
<span className={`voice-icon ${isPlaying ? 'playing' : ''}`}>{isPlaying ? '⏸' : '▶'}</span>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user