This commit is contained in:
1600822305 2025-04-10 12:49:14 +08:00
parent fc77db3b91
commit 4e5e7f6248
14 changed files with 149 additions and 22 deletions

View File

@ -64,6 +64,11 @@ export enum IpcChannel {
Aes_Encrypt = 'aes:encrypt',
Aes_Decrypt = 'aes:decrypt',
// search window
SearchWindow_Open = 'search-window:open',
SearchWindow_Close = 'search-window:close',
SearchWindow_OpenUrl = 'search-window:open-url',
Gemini_UploadFile = 'gemini:upload-file',
Gemini_Base64File = 'gemini:base64-file',
Gemini_RetrieveFile = 'gemini:retrieve-file',

View File

@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edge ASR (External)</title>
<title>Browser ASR (External)</title>
<style>
body {
font-family: sans-serif;
@ -28,8 +28,8 @@
</head>
<body>
<h1>Edge ASR 中继页面</h1>
<p>这个页面需要在 Edge 浏览器中保持打开,以便 Electron 应用使用其语音识别功能。</p>
<h1>浏览器语音识别中继页面</h1>
<p>这个页面需要在浏览器中保持打开,以便应用使用其语音识别功能。</p>
<div id="status">正在连接到服务器...</div>
<div id="result"></div>

View File

@ -6,7 +6,7 @@ const path = require('path') // Need path module
const app = express()
const port = 8080 // Define the port
// 提供网页给 Edge 浏览器
// 提供网页给浏览器
app.get('/', (req, res) => {
// Use path.join for cross-platform compatibility
res.sendFile(path.join(__dirname, 'index.html'))

View File

@ -24,6 +24,7 @@ import * as NutstoreService from './services/NutstoreService'
import ObsidianVaultService from './services/ObsidianVaultService'
import { ProxyConfig, proxyManager } from './services/ProxyManager'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
import { searchService } from './services/SearchService'
import { TrayService } from './services/TrayService'
import { windowService } from './services/WindowService'
import { getResourcePath } from './utils'
@ -297,6 +298,17 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
NutstoreService.getDirectoryContents(token, path)
)
// search window
ipcMain.handle(IpcChannel.SearchWindow_Open, async (_, uid: string) => {
await searchService.openSearchWindow(uid)
})
ipcMain.handle(IpcChannel.SearchWindow_Close, async (_, uid: string) => {
await searchService.closeSearchWindow(uid)
})
ipcMain.handle(IpcChannel.SearchWindow_OpenUrl, async (_, uid: string, url: string) => {
return await searchService.openUrlInSearchWindow(uid, url)
})
// 启动ASR服务器
ipcMain.handle('start-asr-server', async () => {
try {

View File

@ -0,0 +1,100 @@
import { BrowserWindow } from 'electron'
import { join } from 'path'
import { is } from '@electron-toolkit/utils'
import { isMac } from '@main/constant'
class SearchService {
private searchWindows: Map<string, BrowserWindow> = new Map()
/**
*
* @param uid
*/
public async openSearchWindow(uid: string): Promise<void> {
// 如果窗口已经存在,则激活它
if (this.searchWindows.has(uid)) {
const existingWindow = this.searchWindows.get(uid)
if (existingWindow && !existingWindow.isDestroyed()) {
if (existingWindow.isMinimized()) {
existingWindow.restore()
}
existingWindow.focus()
return
}
// 如果窗口已销毁则从Map中移除
this.searchWindows.delete(uid)
}
// 创建新窗口
const searchWindow = new BrowserWindow({
width: 800,
height: 600,
show: false,
autoHideMenuBar: true,
...(isMac ? { titleBarStyle: 'hidden' } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
webSecurity: false
}
})
// 设置窗口标题
searchWindow.setTitle(`搜索窗口 - ${uid}`)
// 加载搜索页面
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
searchWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/#/search?uid=${uid}`)
} else {
searchWindow.loadFile(join(__dirname, '../renderer/index.html'), {
hash: `/search?uid=${uid}`
})
}
// 窗口准备好后显示
searchWindow.once('ready-to-show', () => {
searchWindow.show()
})
// 窗口关闭时从Map中移除
searchWindow.on('closed', () => {
this.searchWindows.delete(uid)
})
// 存储窗口引用
this.searchWindows.set(uid, searchWindow)
}
/**
*
* @param uid
*/
public async closeSearchWindow(uid: string): Promise<void> {
const window = this.searchWindows.get(uid)
if (window && !window.isDestroyed()) {
window.close()
}
this.searchWindows.delete(uid)
}
/**
* URL
* @param uid
* @param url URL
*/
public async openUrlInSearchWindow(uid: string, url: string): Promise<boolean> {
const window = this.searchWindows.get(uid)
if (window && !window.isDestroyed()) {
try {
await window.loadURL(url)
return true
} catch (error) {
console.error(`Failed to load URL in search window: ${error}`)
return false
}
}
return false
}
}
export const searchService = new SearchService()

View File

@ -175,6 +175,11 @@ declare global {
decryptToken: (token: string) => Promise<{ username: string; access_token: string }>
getDirectoryContents: (token: string, path: string) => Promise<any>
}
searchWindow: {
open: (uid: string) => Promise<void>
close: (uid: string) => Promise<void>
openUrl: (uid: string, url: string) => Promise<boolean>
}
}
}
}

View File

@ -169,6 +169,11 @@ const api = {
decryptToken: (token: string) => ipcRenderer.invoke(IpcChannel.Nutstore_DecryptToken, token),
getDirectoryContents: (token: string, path: string) =>
ipcRenderer.invoke(IpcChannel.Nutstore_GetDirectoryContents, token, path)
},
searchWindow: {
open: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Open, uid),
close: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Close, uid),
openUrl: (uid: string, url: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_OpenUrl, uid, url)
}
}

View File

@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edge ASR (External)</title>
<title>Browser ASR (External)</title>
<style>
body {
font-family: sans-serif;
@ -28,8 +28,8 @@
</head>
<body>
<h1>Edge ASR 中继页面</h1>
<p>这个页面需要在 Edge 浏览器中保持打开,以便 Electron 应用使用其语音识别功能。</p>
<h1>浏览器语音识别中继页面</h1>
<p>这个页面需要在浏览器中保持打开,以便应用使用其语音识别功能。</p>
<div id="status">正在连接到服务器...</div>
<div id="result"></div>

View File

@ -31,7 +31,7 @@ function getIndexHtmlPath() {
return path.join(process.cwd(), 'index.html');
}
// 提供网页给 Edge 浏览器
// 提供网页给浏览器
app.get('/', (req, res) => {
const indexPath = getIndexHtmlPath();
console.log(`Serving index.html from: ${indexPath}`);

View File

@ -1347,7 +1347,7 @@
"api_settings": "API Settings",
"service_type": "Service Type",
"service_type.openai": "OpenAI",
"service_type.edge": "Edge TTS",
"service_type.edge": "Browser TTS",
"service_type.refresh": "Refresh TTS service type settings",
"service_type.refreshed": "TTS service type settings refreshed",
"api_key": "API Key",
@ -1393,7 +1393,7 @@
"model": "Model",
"browser.info": "Use the browser's built-in speech recognition feature, no additional setup required",
"local.info": "Use local server and browser for speech recognition, need to start the server and open the browser page first",
"local.browser_tip": "Please open this page in Edge browser and keep the browser window open",
"local.browser_tip": "Please open this page in your browser and keep the browser window open",
"local.test_connection": "Test Connection",
"local.connection_success": "Connection successful",
"local.connection_failed": "Connection failed, please make sure the server is running",

View File

@ -1347,14 +1347,14 @@
"api_settings": "API設定",
"service_type": "サービスタイプ",
"service_type.openai": "OpenAI",
"service_type.edge": "Edge TTS",
"service_type.edge": "ブラウザ TTS",
"test": "テスト",
"error": {
"failed": "音声合成に失敗しました",
"not_enabled": "音声合成が有効になっていません",
"no_edge_voice": "Edge TTSの音声が選択されていません"
"no_edge_voice": "ブラウザ TTSの音声が選択されていません"
},
"help": "OpenAIのTTS APIを使用するには、APIキーが必要です。Edge TTSはブラウザの機能を使用するため、APIキーは不要です。",
"help": "OpenAIのTTS APIを使用するには、APIキーが必要です。ブラウザ TTSはブラウザの機能を使用するため、APIキーは不要です。",
"learn_more": "詳細はこちら"
},
"asr": {
@ -1372,7 +1372,7 @@
"model": "モデル",
"browser.info": "ブラウザの内蔵音声認識機能を使用します。追加設定は不要です",
"local.info": "ローカルサーバーとブラウザを使用して音声認識を行います。サーバーを起動してブラウザページを開く必要があります",
"local.browser_tip": "このページをEdgeブラウザで開き、ブラウザウィンドウを開いたままにしてください",
"local.browser_tip": "このページをブラウザで開き、ブラウザウィンドウを開いたままにしてください",
"local.test_connection": "接続テスト",
"local.connection_success": "接続成功",
"local.connection_failed": "接続失敗。サーバーが起動していることを確認してください",

View File

@ -1353,14 +1353,14 @@
"api_settings": "API设置",
"service_type": "服务类型",
"service_type.openai": "OpenAI",
"service_type.edge": "Edge TTS",
"service_type.edge": "浏览器 TTS",
"service_type.refresh": "刷新TTS服务类型设置",
"service_type.refreshed": "已刷新TTS服务类型设置",
"api_key": "API密钥",
"api_key.placeholder": "请输入OpenAI API密钥",
"api_url": "API地址",
"api_url.placeholder": "例如https://api.openai.com/v1/audio/speech",
"edge_voice": "Edge TTS音色",
"edge_voice": "浏览器 TTS音色",
"edge_voice.loading": "加载中...",
"edge_voice.refresh": "刷新可用音色列表",
"edge_voice.not_found": "未找到匹配的音色",
@ -1386,7 +1386,7 @@
"error": {
"not_enabled": "语音合成功能未启用",
"no_api_key": "未设置API密钥",
"no_edge_voice": "未选择Edge TTS音色",
"no_edge_voice": "未选择浏览器 TTS音色",
"browser_not_support": "浏览器不支持语音合成"
}
},
@ -1405,7 +1405,7 @@
"model": "模型",
"browser.info": "使用浏览器内置的语音识别功能,无需额外设置",
"local.info": "使用本地服务器和浏览器进行语音识别,需要先启动服务器并打开浏览器页面",
"local.browser_tip": "请在Edge浏览器中打开此页面,并保持浏览器窗口打开",
"local.browser_tip": "请在浏览器中打开此页面,并保持浏览器窗口打开",
"local.test_connection": "测试连接",
"local.connection_success": "连接成功",
"local.connection_failed": "连接失败,请确保服务器已启动",

View File

@ -121,7 +121,7 @@ const TTSSettings: FC = () => {
// 浏览器可用的语音列表
const [availableVoices, setAvailableVoices] = useState<{ label: string; value: string }[]>([])
// 预定义的Edge TTS音色列表
// 预定义的浏览器 TTS音色列表
const predefinedVoices = [
{ label: '小晓 (女声, 中文)', value: 'zh-CN-XiaoxiaoNeural' },
{ label: '云扬 (男声, 中文)', value: 'zh-CN-YunyangNeural' },
@ -494,7 +494,7 @@ const TTSSettings: FC = () => {
</>
)}
{/* Edge TTS设置 */}
{/* 浏览器 TTS设置 */}
{ttsServiceType === 'edge' && (
<Form.Item label={t('settings.tts.edge_voice')} style={{ marginBottom: 16 }}>
<VoiceSelectContainer>

View File

@ -112,14 +112,14 @@ export interface SettingsState {
enableDataCollection: boolean
// TTS配置
ttsEnabled: boolean
ttsServiceType: string // TTS服务类型openai或edge
ttsServiceType: string // TTS服务类型openai或浏览器
ttsApiKey: string
ttsApiUrl: string
ttsVoice: string
ttsModel: string
ttsCustomVoices: string[]
ttsCustomModels: string[]
// Edge TTS配置
// 浏览器 TTS配置
ttsEdgeVoice: string
// TTS过滤选项
ttsFilterOptions: {