├── .gitignore ├── README.md ├── delete-icon.svg ├── edit-icon.svg ├── images ├── error.jpg └── tool.png ├── index.html ├── main.js ├── package.json ├── preload.js ├── renderer.js ├── scripts └── notarize.js └── styles.css /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | npm-debug.log 4 | yarn-debug.log 5 | yarn-error.log 6 | package-lock.json 7 | yarn.lock 8 | 9 | # Build outputs 10 | dist/ 11 | build/ 12 | out/ 13 | release/ 14 | release_new/ 15 | *.exe 16 | *.dmg 17 | *.AppImage 18 | *.deb 19 | *.rpm 20 | 21 | # Environment variables and secrets 22 | .env 23 | .env.local 24 | .env.development 25 | .env.production 26 | config.json 27 | secrets.json 28 | 29 | # API Keys - never commit these! 30 | apikeys.json 31 | api_keys.js 32 | api_config.js 33 | 34 | # OS specific files 35 | .DS_Store 36 | Thumbs.db 37 | Desktop.ini 38 | $RECYCLE.BIN/ 39 | 40 | # Editor folders and files 41 | .idea/ 42 | .vscode/ 43 | *.sublime-project 44 | *.sublime-workspace 45 | *.swp 46 | *.swo 47 | .project 48 | .classpath 49 | .settings/ 50 | 51 | # Logs 52 | logs/ 53 | *.log 54 | npm-debug.log* 55 | yarn-debug.log* 56 | yarn-error.log* 57 | 58 | # Temporary files 59 | .tmp/ 60 | .temp/ 61 | tmp/ 62 | temp/ 63 | *.tmp 64 | 65 | # Coverage directory used by tools like istanbul 66 | coverage/ 67 | 68 | # Electron specific 69 | .electron-builder.env 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Copy2Translate 2 | 3 | [中文](#chinese) | [English](#english) 4 | 5 | 6 | Copy2Translate 是一个简单高效的翻译工具,旨在通过复制文本和按下快捷键来简化翻译过程。无论您是学生、专业人士,还是经常需要翻译的人,Copy2Translate 都能简化流程,为您节省时间和精力。 7 | 8 | ## 功能特点 9 | 10 | - **快捷键翻译**: 通过简单的快捷键组合即可快速翻译文本,提升工作效率。 11 | - **多种翻译提示词**: 支持多种预设翻译提示词,满足不同场景的翻译需求。 12 | - **OpenRouter API 支持**: 利用 OpenRouter API 提供的强大翻译能力。 13 | - **DeepSeek API 支持**: 集成 DeepSeek API,提供更精准的翻译结果。 14 | - **自动复制到剪贴板**: 翻译结果自动复制到剪贴板,方便后续使用。 15 | 16 | 17 | ![Copy2Translate界面截图](./images/tool.png) 18 | 19 | 20 | 21 | ## 开发环境设置 22 | 23 | 如果您想在本地开发或修改此项目,请确保已安装 Node.js 和 npm。然后按照以下步骤操作: 24 | 25 | 1. 克隆仓库 26 | ```bash 27 | git clone https://github.com/LizabethLi/Copy2Translate.git 28 | cd copy2translate 29 | ``` 30 | 31 | 2. 安装依赖 32 | ```bash 33 | npm install 34 | ``` 35 | 36 | 3. 运行程序 37 | ```bash 38 | npm start 39 | ``` 40 | 41 | 42 | 4. 构建应用(如果想要自己封装成应用软件,如 macbook 的 dmg文件,windows 的 exe 文件) 43 | ```bash 44 | npm run dist:mac # 仅构建 macOS 45 | npm run dist:win # 仅构建 Windows 46 | ``` 47 | 48 | ## 软件安装说明(如果自己封装成了应用软件,或者获取了已经封装好的应用) 49 | 50 | ### Windows 用户 51 | 1. 下载最新的 `Copy2Translate-Setup.exe` 52 | 2. 双击运行安装程序 53 | 3. 按照提示完成安装 54 | 55 | ### macOS 用户 56 | 1. 下载最新的 `Copy2Translate.dmg` 57 | 2. 双击打开 DMG 文件 58 | 3. 将 `Copy2Translate.app` 拖到 `Applications` 文件夹 59 | 60 | > **注意**:由于应用未经 Apple 开发者签名认证,macOS 可能会阻止应用运行。 61 | ![Error](./images/error.jpg) 62 | 您可以将应用拖入 `Applications` 文件夹后,打开"终端"应用,执行以下命令: 63 | > ```bash 64 | > xattr -rd com.apple.quarantine /Applications/Copy2Translate.app 65 | > ``` 66 | > 这会移除应用的隔离属性,之后即可双击打开。 67 | > 68 | > 作为开发者,我对使用此应用可能产生的任何风险不承担责任。如果您遇到安装或运行问题,请通过提交 [GitHub Issue](https://github.com/LizabethLi/Copy2Translate/issues) 来报告。 69 | 70 | 71 | ## 使用方法 72 | 73 | 1. 首次运行时,需要设置 API Key 74 | 2. 默认快捷键为 `Command+Shift+T`(macOS)或 `Ctrl+Alt+T`(Windows/Linux),如果默认快捷键失效,您可以自己设置快捷键即可 75 | 3. 选中要翻译的文本并复制 76 | 4. 按下快捷键即可翻译 77 | 5. 翻译结果会自动复制到剪贴板,直接粘贴即可,不需要离开当前的编辑页面 78 | 79 | 80 | ## API 密钥申请 81 | 82 | 83 | ### OpenRouter API 申请步骤 84 | 1. 访问 [OpenRouter 官网](https://openrouter.ai/) 85 | 2. 注册并登录您的账户 86 | 3. 导航到 API 密钥页面 87 | 4. 创建新的 API 密钥 88 | 5. 复制 API 密钥并添加到应用的设置中 89 | 90 | ### DeepSeek API 申请步骤 91 | 1. 访问 [DeepSeek 官网](https://platform.deepseek.com/) 92 | 2. 注册并登录您的账户 93 | 3. 导航到开发者或 API 页面 94 | 4. 申请 API 访问权限并创建密钥 95 | 5. 复制 API 密钥并添加到应用的设置中 96 | 97 | ## 配置说明 98 | 99 | ### API Key 设置 100 | 1. 点击顶部的 API Key 按钮 101 | 2. 选择翻译服务提供商 102 | 3. 输入对应的 API Key,填写模型名称,参见两个网站的模型卡 103 | 4. 点击保存 104 | 105 | ### 快捷键设置 106 | 1. 在设置界面中点击"更改快捷键" 107 | 2. 按下新的快捷键组合 108 | 3. 点击确认保存 109 | 110 | ### 翻译提示词 111 | - 可以添加、编辑、删除翻译提示词 112 | - 支持多种预设提示词模板 113 | - 可以随时切换不同的翻译风格 114 | 115 | ## 商业使用与署名 116 | 117 | 本项目基于 MIT 许可证开源,您可以自由地使用、修改和分发。如果您在商业项目或公开场合(如技术分享、博客文章等)使用了本项目的代码或设计,请考虑在适当的位置(例如项目文档、关于页面、致谢部分)提及本项目名称 (Copy2Translate) 并附上 GitHub 仓库链接 [https://github.com/LizabethLi/Copy2Translate](https://github.com/LizabethLi/Copy2Translate)。这不仅是对我的工作的认可,也有助于更多人发现和使用这个工具。 118 | 119 | ## 问题反馈 120 | 121 | 如果你在使用过程中遇到任何问题或有功能建议,请通过以下方式反馈: 122 | 1. 提交 [GitHub Issue](https://github.com/LizabethLi/Copy2Translate/issues) 123 | 124 | 125 | --- 126 | 127 | ## English 128 | 129 | Copy2Translate is a simple and efficient translation tool designed to streamline the translation process by copying text and pressing a hotkey. Whether you are a student, professional, or someone who frequently needs translations, Copy2Translate simplifies the workflow, saving you time and effort. 130 | 131 | ## Features 132 | 133 | - **Hotkey Translation**: Quickly translate text with a simple hotkey combination, improving work efficiency. 134 | - **Multiple Translation Prompts**: Supports multiple preset translation prompts to meet different translation needs in various scenarios. 135 | - **OpenRouter API Support**: Utilizes the powerful translation capabilities provided by the OpenRouter API. 136 | - **DeepSeek API Support**: Integrates DeepSeek API for more accurate translation results. 137 | - **Automatic Copy to Clipboard**: Translation results are automatically copied to the clipboard for convenient subsequent use. 138 | 139 | ![Copy2Translate Interface Screenshot](./images/tool.png) 140 | 141 | ## Development Environment Setup 142 | 143 | If you want to develop or modify this project locally, make sure you have Node.js and npm installed. Then follow these steps: 144 | 145 | 1. Clone the repository 146 | ```bash 147 | git clone https://github.com/LizabethLi/Copy2Translate.git 148 | cd copy2translate 149 | ``` 150 | 151 | 2. Install dependencies 152 | ```bash 153 | npm install 154 | ``` 155 | 156 | 3. Run the environment 157 | ```bash 158 | npm start 159 | ``` 160 | 161 | 4. Build the application (if you want to package it yourself, such as dmg file for macbook or exe file for windows) 162 | ```bash 163 | npm run dist:mac # Build for macOS only 164 | npm run dist:win # Build for Windows only 165 | ``` 166 | 167 | ## Software Installation Instructions (if you have packaged it yourself or obtained a pre-packaged application) 168 | 169 | ### Windows Users 170 | 1. Download the latest `Copy2Translate-Setup.exe` 171 | 2. Double-click to run the installer 172 | 3. Follow the prompts to complete the installation 173 | 174 | ### macOS Users 175 | 1. Download the latest `Copy2Translate.dmg` 176 | 2. Double-click to open the DMG file 177 | 3. Drag the application to the Applications folder 178 | 179 | > **Note**: Since the application is not signed by an Apple developer, macOS may block the application from running. You may come accross error like this: 180 | 181 | ![Error](./images/error.jpg) 182 | > 183 | You can drag the application to the "Applications" folder, then open the "Terminal" application, and execute the following command: 184 | > ```bash 185 | > xattr -rd com.apple.quarantine /Applications/Copy2Translate.app 186 | > ``` 187 | > This will remove the application's quarantine attribute, after which you can double-click to open it. 188 | > 189 | > As a developer, I am not responsible for any risks associated with using this application. If you encounter installation or runtime issues, please report them by submitting a [GitHub Issue](https://github.com/LizabethLi/Copy2Translate/issues). 190 | 191 | ## Usage 192 | 193 | 1. When running for the first time, you need to set the API Key 194 | 2. The default hotkey is `Command+Shift+T` (macOS) or `Ctrl+Alt+T` (Windows/Linux). If the default shortcut keys do not work, you can set your own shortcut keys. 195 | 3. Select the text you want to translate and copy it 196 | 4. Press the hotkey to translate 197 | 5. The translation result will be automatically copied to the clipboard, simply paste it without leaving the current editing page 198 | 199 | ## API Key Application 200 | 201 | ### OpenRouter API Application Steps 202 | 1. Visit the [OpenRouter official website](https://openrouter.ai/) 203 | 2. Register and log in to your account 204 | 3. Navigate to the API Key page 205 | 4. Create a new API Key 206 | 5. Copy the API Key and add it to the application settings 207 | 208 | ### DeepSeek API Application Steps 209 | 1. Visit the [DeepSeek official website](https://platform.deepseek.com/) 210 | 2. Register and log in to your account 211 | 3. Navigate to the Developer or API page 212 | 4. Apply for API access and create a key 213 | 5. Copy the API key and add it to the application settings 214 | 215 | ## Configuration Instructions 216 | 217 | ### API Key Settings 218 | 1. Click the API Key button at the top 219 | 2. Select the translation service provider 220 | 3. Enter the corresponding API Key, fill in the model name, refer to the model cards of the two websites 221 | 4. Click Save 222 | 223 | ### Shortcut Key Settings 224 | 1. Click "Change Shortcut Key" in the settings interface 225 | 2. Press the new shortcut key combination 226 | 3. Click Confirm to save 227 | 228 | ### Translation Prompts 229 | - You can add, edit, and delete translation prompts 230 | - Supports various preset prompt templates 231 | - You can switch between different translation styles at any time 232 | 233 | ## Commercial Use and Attribution 234 | 235 | This project is open-sourced under the MIT license, and you are free to use, modify, and distribute it. If you use the code or design of this project in a commercial project or public setting (such as technical sharing, blog articles, etc.), please consider mentioning the project name (Copy2Translate) and including a link to the GitHub repository [https://github.com/LizabethLi/Copy2Translate](https://github.com/LizabethLi/Copy2Translate) in an appropriate location (such as project documentation, about page, acknowledgments section). This not only recognizes my work but also helps more people discover and use this tool. 236 | 237 | ## Feedback 238 | 239 | If you encounter any problems during use or have feature suggestions, please provide feedback through the following methods: 240 | 1. Submit a [GitHub Issue](https://github.com/LizabethLi/Copy2Translate/issues) 241 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /delete-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /edit-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /images/error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LizabethLi/Copy2Translate/8c56167bf8a1ae86272bccfbe2da3dc93357cae4/images/error.jpg -------------------------------------------------------------------------------- /images/tool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LizabethLi/Copy2Translate/8c56167bf8a1ae86272bccfbe2da3dc93357cae4/images/tool.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Copy2translate 6 | 7 | 8 | 9 | 10 |
11 |
Copy2translate
12 |
13 | 17 |
18 |
19 | 20 |
21 |
22 |
23 |

快捷键设置

24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 | 32 |
33 |
34 |
35 | 36 |
37 |

当前翻译提示词

38 |
39 |
40 | 43 |
44 | 45 |
46 |
47 |
48 | 49 |
50 |

提示词管理

51 |
52 |
53 |
54 |
默认英文翻译
55 |
Please translate this Chinese text to English, maintaining its professional tone:
56 |
当前使用中
57 |
58 | 59 | 60 |
61 |
62 |
63 |
64 | 65 | 66 | 67 |
68 |
69 |
70 | 71 |
72 |

翻译结果

73 |
74 |
75 | 76 | 79 |
80 | 81 | 82 |
83 |
84 |
85 | 86 | 89 | 90 | 91 | 182 | 183 | 184 | 201 | 202 | 203 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { app, BrowserWindow, globalShortcut, clipboard, ipcMain, Notification } = require("electron"); 3 | const axios = require("axios"); 4 | const path = require("path"); 5 | const settings = require("electron-settings"); 6 | const { HttpsProxyAgent } = require('https-proxy-agent'); 7 | const https = require('https'); 8 | 9 | // 使用系统代理设置 10 | app.commandLine.appendSwitch('proxy-server', process.env.HTTPS_PROXY); 11 | app.commandLine.appendSwitch('ignore-certificate-errors'); 12 | 13 | let mainWindow; 14 | let userShortcut = settings.getSync("shortcut") || (process.platform === 'darwin' ? "Shift+Command+T" : "Control+Shift+T"); 15 | let apiKey = settings.getSync("apiKey"); 16 | let userPrompt = settings.getSync("prompt") || "Translate the following Chinese text to English:"; 17 | 18 | // 修改通知函数,添加更多选项 19 | function showNotification(title, body) { 20 | if (Notification.isSupported()) { 21 | try { 22 | const notification = new Notification({ 23 | title, 24 | body, 25 | silent: false, 26 | subtitle: process.platform === 'darwin' ? 'Copy2translate' : undefined, // macOS特有属性 27 | icon: process.platform === 'darwin' ? undefined : path.join(__dirname, 'icon.png'), 28 | timeoutType: 'default' 29 | }); 30 | 31 | // 确保在macOS上应用已注册可发送通知 32 | if (process.platform === 'darwin') { 33 | // 设置为告警型通知,确保显示横幅 34 | notification.urgency = 'critical'; 35 | } 36 | 37 | notification.show(); 38 | 39 | // 为macOS添加回调以确认通知已显示 40 | if (process.platform === 'darwin') { 41 | notification.on('show', () => { 42 | console.log('通知已显示'); 43 | }); 44 | 45 | notification.on('click', () => { 46 | if (mainWindow) { 47 | if (mainWindow.isMinimized()) mainWindow.restore(); 48 | mainWindow.focus(); 49 | } 50 | }); 51 | } 52 | 53 | // 如果是翻译完成的通知,让主窗口闪烁提醒用户 54 | if (title === "翻译完成" && mainWindow) { 55 | if (process.platform === 'darwin') { 56 | // macOS上使用dock弹跳作为辅助提醒 57 | try { 58 | if (app.dock && typeof app.dock.bounce === 'function') { 59 | const bounceId = app.dock.bounce('informational'); 60 | // 3秒后停止弹跳 61 | setTimeout(() => { 62 | if (bounceId !== undefined) app.dock.cancelBounce(bounceId); 63 | }, 3000); 64 | } 65 | } catch (error) { 66 | console.error('Dock弹跳失败:', error); 67 | // 忽略失败,继续执行 68 | } 69 | } else if (process.platform === 'win32') { 70 | // Windows上闪烁任务栏图标 71 | mainWindow.flashFrame(true); 72 | setTimeout(() => { 73 | mainWindow.flashFrame(false); 74 | }, 2000); 75 | } 76 | } 77 | } catch (error) { 78 | console.error('显示通知失败:', error); 79 | // 如果通知失败,尝试通过窗口提醒用户 80 | if (mainWindow && mainWindow.webContents) { 81 | mainWindow.webContents.send('showTranslationComplete', { title, body }); 82 | } 83 | } 84 | } else { 85 | console.warn('系统不支持通知,将使用窗口内提醒'); 86 | // 通过窗口提醒用户 87 | if (mainWindow && mainWindow.webContents) { 88 | mainWindow.webContents.send('showTranslationComplete', { title, body }); 89 | } 90 | } 91 | } 92 | 93 | // 注册快捷键 94 | function registerShortcut(shortcut) { 95 | try { 96 | // 验证快捷键字符串是否只包含ASCII字符 97 | if (!/^[\x00-\x7F]+$/.test(shortcut)) { 98 | console.error(`快捷键字符串只能包含ASCII字符,无效的字符串: "${shortcut}"`); 99 | showNotification("快捷键错误", "快捷键包含无效字符,已重置为默认值"); 100 | shortcut = process.platform === 'darwin' ? "Shift+Command+T" : "Control+Shift+T"; // 重置为默认值 101 | settings.setSync("shortcut", shortcut); 102 | } 103 | 104 | // 注册快捷键 105 | globalShortcut.register(shortcut, async () => { 106 | const textToTranslate = clipboard.readText(); 107 | if (!textToTranslate) { 108 | showNotification("翻译提示", "剪切板为空,请先复制要翻译的文本"); 109 | return; 110 | } 111 | 112 | console.log("翻译中:", textToTranslate); 113 | const translatedText = await translateText(textToTranslate); 114 | clipboard.writeText(translatedText); 115 | console.log("翻译完成,已复制到剪切板:", translatedText); 116 | 117 | showNotification("翻译完成", "译文已复制到剪切板"); 118 | mainWindow.webContents.send("translatedText", translatedText); 119 | }); 120 | 121 | return true; 122 | } catch (error) { 123 | console.error("注册快捷键失败:", error); 124 | showNotification("快捷键错误", "注册快捷键失败,请尝试其他组合"); 125 | return false; 126 | } 127 | } 128 | 129 | // 翻译文本(调用 OpenRouter API) 130 | async function translateText(text) { 131 | try { 132 | if (!apiKey) { 133 | showNotification("错误", "请先设置 API Key"); 134 | mainWindow.webContents.send("showApiKeyPrompt"); 135 | return "请先设置 API Key"; 136 | } 137 | 138 | const instance = axios.create({ 139 | proxy: false, 140 | httpsAgent: new https.Agent({ 141 | rejectUnauthorized: false 142 | }), 143 | timeout: 30000 144 | }); 145 | 146 | console.log('Sending request to OpenRouter API...'); 147 | const response = await instance.post( 148 | "https://openrouter.ai/api/v1/chat/completions", 149 | { 150 | model: "anthropic/claude-3-opus:2024-05-23", // 可以根据需要更换模型 151 | messages: [ 152 | { role: "system", content: "You are a professional translator." }, 153 | { role: "user", content: `${userPrompt}\n${text}` } 154 | ], 155 | temperature: 0.7, 156 | max_tokens: 800, 157 | top_p: 0.8, 158 | frequency_penalty: 0, 159 | presence_penalty: 0 160 | }, 161 | { 162 | headers: { 163 | "Content-Type": "application/json", 164 | "Authorization": `Bearer ${apiKey}`, 165 | "HTTP-Referer": "https://copy2translate.app", // 替换为你的应用域名 166 | "X-Title": "Copy2Translate" 167 | } 168 | } 169 | ); 170 | 171 | console.log('Response received:', response.status); 172 | return response.data.choices[0].message.content; 173 | 174 | } catch (error) { 175 | console.error("翻译失败:", error); 176 | if (error.response) { 177 | console.error("错误详情:", error.response.data); 178 | console.error("状态码:", error.response.status); 179 | console.error("响应头:", error.response.headers); 180 | } else { 181 | console.error("错误类型:", error.name); 182 | console.error("错误消息:", error.message); 183 | if (error.code) { 184 | console.error("错误代码:", error.code); 185 | } 186 | } 187 | return "翻译失败,请检查 API Key 或网络连接"; 188 | } 189 | } 190 | 191 | app.whenReady().then(() => { 192 | // 请求通知权限(仅在 macOS 上需要) 193 | if (process.platform === 'darwin') { 194 | // macOS 不需要设置 AppUserModelId,这仅在 Windows 上才需要 195 | // 设置应用名称,这在 macOS 上对于通知很重要 196 | app.name = 'Copy2translate'; 197 | 198 | // 如果使用Electron v9+,可以检查通知权限 199 | if (Notification.isSupported()) { 200 | // 检查权限 201 | Notification.requestPermission().then(permission => { 202 | console.log('通知权限状态:', permission); 203 | }); 204 | } 205 | } else if (process.platform === 'win32') { 206 | // 在 Windows 上设置应用 ID 207 | app.setAppUserModelId('com.copy2translate.app'); 208 | } 209 | 210 | // 创建主窗口 211 | createWindow(); 212 | 213 | // 注册快捷键 214 | const registeredShortcut = registerShortcut(userShortcut); 215 | console.log("已注册快捷键:", registeredShortcut ? "成功" : "失败"); 216 | }); 217 | 218 | // 监听应用退出,释放快捷键 219 | app.on("will-quit", () => { 220 | globalShortcut.unregisterAll(); 221 | }); 222 | 223 | // 在应用准备就绪时创建窗口 224 | app.on('window-all-closed', () => { 225 | if (process.platform !== 'darwin') { 226 | app.quit(); 227 | } 228 | }); 229 | 230 | app.on('activate', () => { 231 | if (BrowserWindow.getAllWindows().length === 0) { 232 | createWindow(); 233 | } 234 | }); 235 | 236 | // 定义创建窗口的函数 237 | function createWindow() { 238 | // 创建窗口 239 | mainWindow = new BrowserWindow({ 240 | width: 400, 241 | height: 350, 242 | webPreferences: { 243 | nodeIntegration: false, 244 | contextIsolation: true, 245 | preload: path.join(__dirname, 'preload.js'), 246 | webSecurity: true 247 | }, 248 | }); 249 | 250 | // 加载HTML文件 251 | mainWindow.loadFile('index.html'); 252 | 253 | // 设置 CSP 头 254 | mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => { 255 | callback({ 256 | responseHeaders: { 257 | ...details.responseHeaders, 258 | 'Content-Security-Policy': ["default-src 'self'; style-src 'self' 'unsafe-inline';"] 259 | } 260 | }); 261 | }); 262 | 263 | // 检查是否有通知权限 264 | if (Notification.isSupported()) { 265 | console.log('通知功能支持'); 266 | } else { 267 | console.log('通知功能不支持'); 268 | } 269 | 270 | // 监听提示词更新 271 | setupIPCListeners(); 272 | 273 | // 在开发环境中打开开发者工具 274 | if (process.env.NODE_ENV === "development") { 275 | mainWindow.webContents.openDevTools(); 276 | } 277 | } 278 | 279 | // 设置IPC监听器 280 | function setupIPCListeners() { 281 | // 检查是否有 API key 282 | if (!apiKey) { 283 | mainWindow.webContents.send("showApiKeyPrompt"); 284 | } 285 | 286 | // 监听快捷键更新 287 | ipcMain.on("updateShortcut", (event, newShortcut) => { 288 | // 验证快捷键字符串是否只包含ASCII字符 289 | if (!/^[\x00-\x7F]+$/.test(newShortcut)) { 290 | console.error(`快捷键字符串只能包含ASCII字符,无效的字符串: "${newShortcut}"`); 291 | showNotification("快捷键错误", "快捷键包含无效字符,请使用有效的组合"); 292 | event.reply("shortcutUpdateFailed", "快捷键包含无效字符"); 293 | return; 294 | } 295 | 296 | // 保存新的快捷键 297 | settings.setSync("shortcut", newShortcut); 298 | userShortcut = newShortcut; 299 | 300 | // 注销所有快捷键并注册新的快捷键 301 | globalShortcut.unregisterAll(); 302 | const success = registerShortcut(newShortcut); 303 | 304 | if (success) { 305 | event.reply("shortcutUpdated", newShortcut); 306 | } else { 307 | event.reply("shortcutUpdateFailed", "注册快捷键失败"); 308 | } 309 | }); 310 | 311 | // 监听 API key 设置 312 | ipcMain.on("setApiKey", (event, newApiKey, provider) => { 313 | if (provider === 'openRouter') { 314 | apiKey = newApiKey; 315 | settings.setSync("apiKey", newApiKey); 316 | } else if (provider === 'deepSeek') { 317 | settings.setSync("deepSeekApiKey", newApiKey); 318 | } 319 | showNotification("设置成功", "API Key 已保存"); 320 | event.reply("apiKeySaved"); 321 | }); 322 | 323 | // 监听模型设置 324 | ipcMain.on("setModel", (event, model, provider) => { 325 | if (provider === 'openRouter') { 326 | settings.setSync("model", model); 327 | } else if (provider === 'deepSeek') { 328 | settings.setSync("deepSeekModel", model); 329 | } 330 | }); 331 | 332 | // 添加 IPC 监听器来获取快捷键 333 | ipcMain.handle('getShortcut', () => { 334 | return settings.getSync("shortcut") || (process.platform === 'darwin' ? "Shift+Command+T" : "Control+Shift+T"); 335 | }); 336 | 337 | // 添加 IPC 监听器来获取 prompt 338 | ipcMain.handle('getPrompt', () => { 339 | return settings.getSync("prompt") || "Translate the following Chinese text to English:"; 340 | }); 341 | 342 | // 添加 IPC 处理程序来获取 API Key 343 | ipcMain.handle('getApiKey', (event, provider) => { 344 | if (provider === 'openRouter') { 345 | return settings.getSync("apiKey") || ""; 346 | } else if (provider === 'deepSeek') { 347 | return settings.getSync("deepSeekApiKey") || ""; 348 | } 349 | return ""; 350 | }); 351 | 352 | // 添加 IPC 处理程序来获取模型 353 | ipcMain.handle('getModel', (event, provider) => { 354 | if (provider === 'openRouter') { 355 | return settings.getSync("model") || "openai/gpt-4o"; 356 | } else if (provider === 'deepSeek') { 357 | return settings.getSync("deepSeekModel") || "deepseek-chat"; 358 | } 359 | return ""; 360 | }); 361 | 362 | // 监听 prompt 更新 363 | ipcMain.on("updatePrompt", (event, newPrompt) => { 364 | userPrompt = newPrompt; 365 | settings.setSync("prompt", newPrompt); 366 | showNotification("设置成功", "翻译提示词已更新"); 367 | }); 368 | 369 | // 添加 IPC 处理程序来保存提示词列表 370 | ipcMain.on("savePrompts", (event, prompts) => { 371 | console.log("主进程保存提示词列表:", prompts); 372 | settings.setSync("prompts", prompts); 373 | }); 374 | 375 | // 添加 IPC 处理程序来获取提示词列表 376 | ipcMain.handle("getPrompts", () => { 377 | const savedPrompts = settings.getSync("prompts"); 378 | console.log("主进程获取提示词列表:", savedPrompts); 379 | 380 | // 如果没有保存的提示词,返回默认的三条基本提示词 381 | if (!savedPrompts || savedPrompts.length === 0) { 382 | const defaultPrompts = [ 383 | { 384 | id: "default", 385 | name: "默认英文翻译", 386 | text: "Please translate this Chinese text to English, only return the translation:", 387 | isActive: true 388 | }, 389 | { 390 | id: "formal", 391 | name: "正式商务", 392 | text: "Please translate this Chinese text to formal Business English. Use professional vocabulary, maintain a respectful tone, and ensure the language is appropriate for corporate communications or official documents,only return the translation:", 393 | isActive: false 394 | }, 395 | { 396 | id: "casual", 397 | name: "日常口语", 398 | text: "Please translate this Chinese text to casual, conversational English. Use everyday expressions, contractions, and a friendly tone that would be appropriate for informal conversations with friends,only return the translation:", 399 | isActive: false 400 | } 401 | ]; 402 | 403 | // 保存默认提示词到设置中 404 | settings.setSync("prompts", defaultPrompts); 405 | return defaultPrompts; 406 | } 407 | 408 | return savedPrompts; 409 | }); 410 | 411 | // 添加 IPC 处理程序来清除提示词列表 412 | ipcMain.handle("clearPrompts", () => { 413 | console.log("清除主进程中的提示词列表"); 414 | settings.unsetSync("prompts"); 415 | return true; 416 | }); 417 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "copy2translate", 3 | "version": "1.0.0", 4 | "main": "main.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "start": "cross-env NODE_ENV=production electron .", 8 | "dev": "cross-env NODE_ENV=development electron .", 9 | "pack": "electron-builder --dir", 10 | "dist": "electron-builder", 11 | "dist:mac": "electron-builder --mac", 12 | "dist:win": "electron-builder --win" 13 | }, 14 | "keywords": [], 15 | "author": "Liz Li", 16 | "license": "MIT", 17 | "description": "A translation app that helps you translate text with custom shortcut keys", 18 | "dependencies": { 19 | "axios": "^1.8.2", 20 | "clipboardy": "^4.0.0", 21 | "dotenv": "^16.4.5", 22 | "electron-settings": "^4.0.2", 23 | "https-proxy-agent": "^7.0.4", 24 | "settings": "^0.1.1" 25 | }, 26 | "devDependencies": { 27 | "cross-env": "^7.0.3", 28 | "electron": "^35.0.0", 29 | "electron-builder": "^25.1.8" 30 | }, 31 | "build": { 32 | "appId": "com.copy2translate.app", 33 | "productName": "Copy2Translate", 34 | "copyright": "Copyright © Liz Li", 35 | "asar": true, 36 | "compression": "maximum", 37 | "mac": { 38 | "category": "public.app-category.utilities", 39 | "target": [ 40 | "dmg", 41 | "zip" 42 | ], 43 | "icon": "build/icon.icns", 44 | "artifactName": "Copy2Translate.${ext}" 45 | }, 46 | "win": { 47 | "target": [ 48 | "nsis" 49 | ], 50 | "icon": "build/icon.ico", 51 | "artifactName": "Copy2Translate-Setup.${ext}" 52 | }, 53 | "linux": { 54 | "target": [ 55 | "AppImage" 56 | ], 57 | "icon": "build/icon.png" 58 | }, 59 | "directories": { 60 | "output": "release_new" 61 | }, 62 | "files": [ 63 | "main.js", 64 | "preload.js", 65 | "renderer.js", 66 | "index.html", 67 | "styles.css", 68 | "*.svg", 69 | "build/**/*", 70 | "node_modules/**/*" 71 | ], 72 | "asarUnpack": [], 73 | "npmRebuild": false 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /preload.js: -------------------------------------------------------------------------------- 1 | const { contextBridge, ipcRenderer } = require('electron'); 2 | 3 | // 暴露安全的 API 给渲染进程 4 | contextBridge.exposeInMainWorld('electron', { 5 | // 从主进程接收消息 6 | receive: (channel, func) => { 7 | const validChannels = [ 8 | 'translatedText', 9 | 'showApiKeyPrompt', 10 | 'shortcutUpdated', 11 | 'shortcutUpdateFailed', 12 | 'apiKeySaved', 13 | 'apiKeyError', 14 | 'showTranslationComplete' 15 | ]; 16 | if (validChannels.includes(channel)) { 17 | // 删除 IPC 事件监听器以避免内存泄漏 18 | ipcRenderer.removeAllListeners(channel); 19 | // 添加新的监听器 20 | ipcRenderer.on(channel, (event, ...args) => func(...args)); 21 | } 22 | }, 23 | 24 | // 一次性事件监听 25 | once: (channel, func) => { 26 | const validChannels = [ 27 | 'shortcutUpdated', 28 | 'shortcutUpdateFailed', 29 | 'apiKeySaved', 30 | 'apiKeyError' 31 | ]; 32 | if (validChannels.includes(channel)) { 33 | // 添加一次性监听器 34 | ipcRenderer.once(channel, (event, ...args) => func(...args)); 35 | } 36 | }, 37 | 38 | // 向主进程发送消息 39 | send: (channel, ...args) => { 40 | const validChannels = [ 41 | 'updateShortcut', 42 | 'updatePrompt', 43 | 'setApiKey', 44 | 'setModel', 45 | 'savePrompts' 46 | ]; 47 | if (validChannels.includes(channel)) { 48 | ipcRenderer.send(channel, ...args); 49 | } 50 | }, 51 | 52 | // 调用主进程方法并等待结果 53 | invoke: async (channel, ...args) => { 54 | const validChannels = [ 55 | 'getShortcut', 56 | 'getPrompt', 57 | 'getApiKey', 58 | 'getModel', 59 | 'getPrompts', 60 | 'clearPrompts' 61 | ]; 62 | if (validChannels.includes(channel)) { 63 | return await ipcRenderer.invoke(channel, ...args); 64 | } 65 | return Promise.reject(new Error(`Invalid channel: ${channel}`)); 66 | } 67 | }); -------------------------------------------------------------------------------- /renderer.js: -------------------------------------------------------------------------------- 1 | // 移除 require 语句,使用 window.electron API 2 | // const { ipcRenderer } = require("electron"); 3 | 4 | // 常量定义 5 | const AUDIO_SETTINGS = { 6 | HIGH_E: 1318.51, 7 | LOW_E: 659.25, 8 | DURATION: 0.5, 9 | INITIAL_GAIN: 0.1 10 | }; 11 | 12 | const PROVIDERS = { 13 | OPEN_ROUTER: 'openRouter', 14 | DEEP_SEEK: 'deepSeek' 15 | }; 16 | 17 | // 默认提示词配置 18 | const DEFAULT_PROMPTS = [ 19 | { 20 | id: "default", 21 | name: "默认英文翻译", 22 | text: "Please translate the following text to English, please make sure only return the translation:", 23 | isActive: true 24 | }, 25 | { 26 | id: "formal", 27 | name: "正式商务", 28 | text: "Please translate the following text to formal Business English. Use professional vocabulary, maintain a respectful tone, and ensure the language is appropriate for corporate communications or official documents,please make sure only return the translation:", 29 | isActive: false 30 | }, 31 | { 32 | id: "casual", 33 | name: "日常口语", 34 | text: "Please translate the following text to casual, conversational English. Use everyday expressions, contractions, and a friendly tone that would be appropriate for informal conversations with friends,please make sure only return the translation:", 35 | isActive: false 36 | } 37 | ]; 38 | 39 | // 初始化提示词列表 40 | let prompts = [...DEFAULT_PROMPTS]; 41 | 42 | // 标记当前是否处于编辑模式 43 | let isEditMode = false; 44 | let editingPromptId = null; 45 | 46 | // 当前选中的提供商 47 | let currentProvider = PROVIDERS.OPEN_ROUTER; 48 | 49 | document.addEventListener("DOMContentLoaded", () => { 50 | // 检查 localStorage 状态 51 | console.log("页面加载时的 localStorage 状态:"); 52 | console.log("prompts:", localStorage.getItem('prompts')); 53 | 54 | // 初始化提示词列表 55 | loadPrompts(); 56 | 57 | // 显示当前快捷键 58 | fetchShortcut(); 59 | 60 | // 显示当前 prompt 61 | fetchPrompt(); 62 | 63 | // 在页面关闭前保存提示词列表 64 | window.addEventListener('beforeunload', () => { 65 | console.log("页面关闭前保存提示词列表"); 66 | savePrompts(); 67 | }); 68 | 69 | // 添加输入框事件监听 70 | document.getElementById("newShortcut").addEventListener("keydown", (e) => { 71 | e.preventDefault(); 72 | const modifiers = []; 73 | if (e.ctrlKey) modifiers.push("Ctrl"); 74 | if (e.shiftKey) modifiers.push("Shift"); 75 | if (e.altKey) modifiers.push("Alt"); 76 | if (e.metaKey) modifiers.push("Command"); 77 | 78 | // 只接受ASCII字符 79 | const key = e.key.toUpperCase(); 80 | if (key !== "CONTROL" && key !== "SHIFT" && key !== "ALT" && key !== "META") { 81 | // 检查是否为ASCII字符 82 | if (/^[\x00-\x7F]$/.test(e.key)) { 83 | const shortcut = [...modifiers, key].join("+"); 84 | document.getElementById("newShortcut").value = shortcut; 85 | } else { 86 | console.warn(`忽略非ASCII字符: ${e.key}`); 87 | } 88 | } 89 | }); 90 | 91 | // 监听显示 API key 输入提示 92 | window.electron.receive("showApiKeyPrompt", () => { 93 | document.getElementById("apiKeyModal").classList.remove('hidden'); 94 | }); 95 | 96 | // 翻译结果接收 97 | window.electron.receive("translatedText", (text) => { 98 | const translatedTextArea = document.getElementById("translatedText"); 99 | translatedTextArea.value = text; 100 | 101 | // 添加动画效果以提醒用户翻译已完成 102 | translatedTextArea.classList.add("translation-complete"); 103 | 104 | // 聚焦到翻译结果区域并选中所有文本 105 | translatedTextArea.focus(); 106 | translatedTextArea.select(); 107 | 108 | // 移除动画类,为下次翻译做准备 109 | setTimeout(() => { 110 | translatedTextArea.classList.remove("translation-complete"); 111 | }, 2000); 112 | }); 113 | 114 | // 添加应用内通知处理 115 | window.electron.receive("showTranslationComplete", (data) => { 116 | // 创建一个应用内通知元素 117 | showAppNotification(data.title, data.body); 118 | }); 119 | 120 | // 添加按钮事件监听器 121 | document.getElementById("updateShortcutBtn").addEventListener("click", updateShortcut); 122 | document.getElementById("saveApiKeyBtn").addEventListener("click", saveApiKey); 123 | document.getElementById("addPromptBtn").addEventListener("click", showAddPromptModal); 124 | document.getElementById("resetPromptsBtn").addEventListener("click", resetPrompts); 125 | document.getElementById("savePromptBtn").addEventListener("click", handleSavePrompt); 126 | document.getElementById("cancelAddPromptBtn").addEventListener("click", hideAddPromptModal); 127 | document.getElementById("translateBtn").addEventListener("click", simulateTranslation); 128 | 129 | // 添加清除 localStorage 按钮事件监听(如果存在这个按钮) 130 | const clearLocalStorageBtn = document.getElementById("clearLocalStorageBtn"); 131 | if (clearLocalStorageBtn) { 132 | clearLocalStorageBtn.addEventListener("click", clearLocalStoragePrompts); 133 | } 134 | 135 | // 顶部标题栏按钮事件监听 136 | document.getElementById("apiKeyBtn").addEventListener("click", showApiKeyModal); 137 | 138 | // API Key弹窗事件监听 139 | document.getElementById("saveApiKeyBtn").addEventListener("click", saveApiKey); 140 | document.getElementById("cancelApiKeyBtn").addEventListener("click", cancelApiKeySetting); 141 | document.getElementById("openRouterBtn").addEventListener("click", () => switchProvider('openRouter')); 142 | document.getElementById("deepSeekBtn").addEventListener("click", () => switchProvider('deepSeek')); 143 | 144 | // 密码显示/隐藏按钮 145 | document.addEventListener('click', function(e) { 146 | if (e.target.closest('.toggle-password-btn')) { 147 | const container = e.target.closest('.password-input-container'); 148 | const input = container.querySelector('input'); 149 | togglePasswordVisibility(input); 150 | } 151 | }); 152 | 153 | // 提示词选择变更 154 | document.getElementById("promptSelect").addEventListener("change", handlePromptSelect); 155 | }); 156 | 157 | // 读取并显示当前快捷键 158 | async function fetchShortcut() { 159 | try { 160 | const shortcut = await window.electron.invoke('getShortcut'); 161 | console.log("Current shortcut:", shortcut); 162 | document.getElementById("currentShortcut").innerText = shortcut; 163 | } catch (error) { 164 | console.error('Error fetching shortcut:', error); 165 | } 166 | } 167 | 168 | // 读取并显示当前 prompt 169 | async function fetchPrompt() { 170 | try { 171 | const prompt = await window.electron.invoke('getPrompt'); 172 | document.getElementById("promptInput").value = prompt; 173 | } catch (error) { 174 | console.error('Error fetching prompt:', error); 175 | } 176 | } 177 | 178 | // 更新快捷键 179 | function updateShortcut() { 180 | const newShortcut = document.getElementById("newShortcut").value; 181 | if (newShortcut) { 182 | // 先设置监听器 183 | window.electron.receive("shortcutUpdated", (updatedShortcut) => { 184 | document.getElementById("currentShortcut").innerText = updatedShortcut; 185 | alert(`快捷键已更新为:${updatedShortcut}`); 186 | }); 187 | 188 | window.electron.receive("shortcutUpdateFailed", (reason) => { 189 | alert(`快捷键更新失败:${reason}`); 190 | // 重新获取当前快捷键 191 | fetchShortcut(); 192 | }); 193 | 194 | // 然后发送更新请求 195 | window.electron.send("updateShortcut", newShortcut); 196 | } 197 | } 198 | 199 | // 显示API Key设置弹窗 200 | function showApiKeyModal() { 201 | const modal = document.getElementById("apiKeyModal"); 202 | modal.classList.remove('hidden'); 203 | 204 | // 加载当前API Key和模型信息 205 | loadApiKeySettings(); 206 | } 207 | 208 | // 加载API Key和模型设置 209 | async function loadApiKeySettings() { 210 | try { 211 | let openRouterApiKey = ''; 212 | let openRouterModel = ''; 213 | let deepSeekApiKey = ''; 214 | let deepSeekModel = ''; 215 | 216 | // 尝试从主进程获取设置 217 | try { 218 | // 尝试从主进程获取OpenRouter设置 219 | openRouterApiKey = await window.electron.invoke('getApiKey', 'openRouter') || ''; 220 | openRouterModel = await window.electron.invoke('getModel', 'openRouter') || ''; 221 | 222 | // 尝试从主进程获取DeepSeek设置 223 | deepSeekApiKey = await window.electron.invoke('getApiKey', 'deepSeek') || ''; 224 | deepSeekModel = await window.electron.invoke('getModel', 'deepSeek') || ''; 225 | } catch (ipcError) { 226 | console.warn('无法从主进程获取API设置,将使用本地存储:', ipcError); 227 | 228 | // 从本地存储获取设置作为备选方案 229 | openRouterApiKey = localStorage.getItem('openRouterApiKey') || ''; 230 | openRouterModel = localStorage.getItem('openRouterModel') || ''; 231 | deepSeekApiKey = localStorage.getItem('deepSeekApiKey') || ''; 232 | deepSeekModel = localStorage.getItem('deepSeekModel') || ''; 233 | } 234 | 235 | // 更新OpenRouter显示 236 | updateProviderDisplay('openRouter', openRouterApiKey, openRouterModel); 237 | 238 | // 更新DeepSeek显示 239 | updateProviderDisplay('deepSeek', deepSeekApiKey, deepSeekModel); 240 | 241 | // 显示当前选中的提供商设置面板 242 | switchProvider(currentProvider); 243 | } catch (error) { 244 | console.error('Error loading API Key settings:', error); 245 | } 246 | } 247 | 248 | // 更新提供商显示 249 | function updateProviderDisplay(provider, apiKey, model) { 250 | if (provider === 'openRouter') { 251 | const currentApiKeyInput = document.getElementById("currentApiKey"); 252 | const currentModelInput = document.getElementById("currentModel"); 253 | 254 | if (apiKey) { 255 | currentApiKeyInput.value = apiKey; 256 | currentApiKeyInput.placeholder = ''; 257 | } else { 258 | currentApiKeyInput.value = ''; 259 | currentApiKeyInput.placeholder = '尚未设置 API Key'; 260 | } 261 | 262 | if (model) { 263 | currentModelInput.value = model; 264 | currentModelInput.placeholder = ''; 265 | } else { 266 | currentModelInput.value = ''; 267 | currentModelInput.placeholder = '尚未设置模型'; 268 | } 269 | 270 | // 清空新输入框,只保留 placeholder 271 | document.getElementById("newApiKey").value = ''; 272 | document.getElementById("newModel").value = ''; 273 | } else if (provider === 'deepSeek') { 274 | const currentApiKeyInput = document.getElementById("currentDeepSeekApiKey"); 275 | const currentModelInput = document.getElementById("currentDeepSeekModel"); 276 | 277 | if (apiKey) { 278 | currentApiKeyInput.value = apiKey; 279 | currentApiKeyInput.placeholder = ''; 280 | } else { 281 | currentApiKeyInput.value = ''; 282 | currentApiKeyInput.placeholder = '尚未设置 API Key'; 283 | } 284 | 285 | if (model) { 286 | currentModelInput.value = model; 287 | currentModelInput.placeholder = ''; 288 | } else { 289 | currentModelInput.value = ''; 290 | currentModelInput.placeholder = '尚未设置模型'; 291 | } 292 | 293 | // 清空新输入框,只保留 placeholder 294 | document.getElementById("newDeepSeekApiKey").value = ''; 295 | document.getElementById("newDeepSeekModel").value = ''; 296 | } 297 | 298 | // 更新提供商状态 299 | updateProviderStatus(provider, apiKey); 300 | } 301 | 302 | // 更新提供商状态 303 | function updateProviderStatus(provider, apiKey) { 304 | const openRouterBtn = document.getElementById("openRouterBtn"); 305 | const deepSeekBtn = document.getElementById("deepSeekBtn"); 306 | 307 | const statusSpan = provider === 'openRouter' 308 | ? openRouterBtn.querySelector('.provider-status') 309 | : deepSeekBtn.querySelector('.provider-status'); 310 | 311 | // 清除之前的状态类 312 | statusSpan.classList.remove('status-set', 'status-unset'); 313 | 314 | if (apiKey) { 315 | statusSpan.textContent = '(已设置)'; 316 | statusSpan.classList.add('status-set'); 317 | } else { 318 | statusSpan.textContent = '(未设置)'; 319 | statusSpan.classList.add('status-unset'); 320 | } 321 | } 322 | 323 | // 保存 API Key 和模型设置 324 | function saveApiKey() { 325 | let apiKey, model; 326 | 327 | if (currentProvider === 'openRouter') { 328 | apiKey = document.getElementById("newApiKey").value; 329 | model = document.getElementById("newModel").value; 330 | 331 | // 如果没有输入新的API Key,检查是否有现有的 332 | if (!apiKey) { 333 | apiKey = document.getElementById("currentApiKey").value; 334 | } 335 | } else if (currentProvider === 'deepSeek') { 336 | apiKey = document.getElementById("newDeepSeekApiKey").value; 337 | model = document.getElementById("newDeepSeekModel").value; 338 | 339 | // 如果没有输入新的API Key,检查是否有现有的 340 | if (!apiKey) { 341 | apiKey = document.getElementById("currentDeepSeekApiKey").value; 342 | } 343 | } 344 | 345 | if (apiKey) { 346 | // 发送到主进程保存 347 | window.electron.send("setApiKey", apiKey, currentProvider); 348 | if (model) { // 只有当用户实际输入了模型时才保存 349 | window.electron.send("setModel", model, currentProvider); 350 | } 351 | 352 | // 同时保存到本地存储作为备选方案 353 | if (currentProvider === 'openRouter') { 354 | localStorage.setItem('openRouterApiKey', apiKey); 355 | if (model) { 356 | localStorage.setItem('openRouterModel', model); 357 | } 358 | } else if (currentProvider === 'deepSeek') { 359 | localStorage.setItem('deepSeekApiKey', apiKey); 360 | if (model) { 361 | localStorage.setItem('deepSeekModel', model); 362 | } 363 | } 364 | 365 | // 更新当前显示 366 | if (currentProvider === 'openRouter') { 367 | document.getElementById("currentApiKey").value = apiKey; 368 | document.getElementById("currentApiKey").placeholder = ''; 369 | document.getElementById("currentModel").value = model; 370 | } else if (currentProvider === 'deepSeek') { 371 | document.getElementById("currentDeepSeekApiKey").value = apiKey; 372 | document.getElementById("currentDeepSeekApiKey").placeholder = ''; 373 | document.getElementById("currentDeepSeekModel").value = model; 374 | } 375 | 376 | // 更新提供商状态 377 | updateProviderStatus(currentProvider, apiKey); 378 | 379 | // 关闭弹窗 380 | const modal = document.getElementById("apiKeyModal"); 381 | modal.classList.add('hidden'); 382 | } else { 383 | alert("请输入有效的 API Key"); 384 | } 385 | } 386 | 387 | // 切换密码显示/隐藏 388 | function togglePasswordVisibility(input) { 389 | if (input.type === "password") { 390 | input.type = "text"; 391 | } else { 392 | input.type = "password"; 393 | } 394 | } 395 | 396 | // 切换提供商 397 | function switchProvider(provider) { 398 | const openRouterBtn = document.getElementById("openRouterBtn"); 399 | const deepSeekBtn = document.getElementById("deepSeekBtn"); 400 | const openRouterSettings = document.getElementById("openRouterSettings"); 401 | const deepSeekSettings = document.getElementById("deepSeekSettings"); 402 | 403 | // 保存当前选中的提供商 404 | currentProvider = provider; 405 | 406 | // 重置按钮状态 407 | openRouterBtn.classList.remove('active'); 408 | deepSeekBtn.classList.remove('active'); 409 | 410 | // 隐藏所有设置面板 411 | openRouterSettings.classList.add('hidden'); 412 | deepSeekSettings.classList.add('hidden'); 413 | 414 | // 激活选中的提供商 415 | if (provider === 'openRouter') { 416 | openRouterBtn.classList.add('active'); 417 | openRouterSettings.classList.remove('hidden'); 418 | } else if (provider === 'deepSeek') { 419 | deepSeekBtn.classList.add('active'); 420 | deepSeekSettings.classList.remove('hidden'); 421 | } 422 | } 423 | 424 | // 取消API Key设置 425 | function cancelApiKeySetting() { 426 | const modal = document.getElementById("apiKeyModal"); 427 | modal.classList.add('hidden'); 428 | } 429 | 430 | // 加载提示词列表 431 | function loadPrompts() { 432 | // 尝试从主进程获取提示词列表 433 | window.electron.invoke('getPrompts') 434 | .then(savedPrompts => { 435 | console.log("从主进程获取提示词:", savedPrompts); 436 | 437 | if (savedPrompts && savedPrompts.length > 0) { 438 | // 如果主进程中有提示词,使用它们 439 | prompts = savedPrompts; 440 | console.log("成功从主进程获取提示词:", prompts); 441 | } else { 442 | // 尝试从本地存储加载提示词列表 443 | const localPrompts = localStorage.getItem('prompts'); 444 | console.log("从本地存储加载提示词:", localPrompts); 445 | 446 | if (localPrompts) { 447 | try { 448 | // 如果本地存储中有提示词,使用它们 449 | prompts = JSON.parse(localPrompts); 450 | console.log("成功解析本地存储中的提示词:", prompts); 451 | 452 | // 同步到主进程 453 | window.electron.send('savePrompts', prompts); 454 | } catch (error) { 455 | console.error("解析本地存储中的提示词时出错:", error); 456 | // 如果解析出错,使用默认提示词 457 | localStorage.setItem('prompts', JSON.stringify(prompts)); 458 | // 同步到主进程 459 | window.electron.send('savePrompts', prompts); 460 | } 461 | } else { 462 | console.log("本地存储中没有提示词,使用默认提示词"); 463 | // 如果本地存储中没有提示词,使用默认提示词并保存到本地存储 464 | localStorage.setItem('prompts', JSON.stringify(prompts)); 465 | // 同步到主进程 466 | window.electron.send('savePrompts', prompts); 467 | } 468 | } 469 | 470 | // 确保至少有一个提示词是活跃的 471 | const hasActivePrompt = prompts.some(p => p.isActive); 472 | if (!hasActivePrompt && prompts.length > 0) { 473 | prompts[0].isActive = true; 474 | console.log("没有活跃的提示词,将第一个提示词设为活跃:", prompts[0]); 475 | } 476 | 477 | // 更新提示词下拉列表 478 | updatePromptSelect(); 479 | 480 | // 更新提示词列表显示 481 | renderPromptsList(); 482 | }) 483 | .catch(error => { 484 | console.error("从主进程获取提示词时出错:", error); 485 | 486 | // 尝试从本地存储加载提示词列表 487 | const localPrompts = localStorage.getItem('prompts'); 488 | console.log("从本地存储加载提示词:", localPrompts); 489 | 490 | if (localPrompts) { 491 | try { 492 | // 如果本地存储中有提示词,使用它们 493 | prompts = JSON.parse(localPrompts); 494 | console.log("成功解析本地存储中的提示词:", prompts); 495 | } catch (error) { 496 | console.error("解析本地存储中的提示词时出错:", error); 497 | // 如果解析出错,使用默认提示词 498 | localStorage.setItem('prompts', JSON.stringify(prompts)); 499 | } 500 | } else { 501 | console.log("本地存储中没有提示词,使用默认提示词"); 502 | // 如果本地存储中没有提示词,使用默认提示词并保存到本地存储 503 | localStorage.setItem('prompts', JSON.stringify(prompts)); 504 | } 505 | 506 | // 确保至少有一个提示词是活跃的 507 | const hasActivePrompt = prompts.some(p => p.isActive); 508 | if (!hasActivePrompt && prompts.length > 0) { 509 | prompts[0].isActive = true; 510 | console.log("没有活跃的提示词,将第一个提示词设为活跃:", prompts[0]); 511 | } 512 | 513 | // 更新提示词下拉列表 514 | updatePromptSelect(); 515 | 516 | // 更新提示词列表显示 517 | renderPromptsList(); 518 | }); 519 | } 520 | 521 | // 更新提示词下拉列表 522 | function updatePromptSelect() { 523 | const select = document.getElementById("promptSelect"); 524 | select.innerHTML = ''; 525 | 526 | prompts.forEach(prompt => { 527 | const option = document.createElement('option'); 528 | option.value = prompt.id; 529 | option.textContent = prompt.name; 530 | if (prompt.isActive) { 531 | option.selected = true; 532 | } 533 | select.appendChild(option); 534 | }); 535 | } 536 | 537 | // 渲染提示词列表 538 | function renderPromptsList() { 539 | const promptsList = document.getElementById("promptsList"); 540 | promptsList.innerHTML = ''; 541 | 542 | console.log("渲染提示词列表,共", prompts.length, "个提示词"); 543 | 544 | prompts.forEach(prompt => { 545 | console.log("渲染提示词:", prompt.id, prompt.name, prompt.isActive ? "(活跃)" : ""); 546 | 547 | const promptItem = document.createElement('div'); 548 | promptItem.className = 'prompt-item'; 549 | 550 | const nameDiv = document.createElement('div'); 551 | nameDiv.className = 'prompt-item-name'; 552 | nameDiv.textContent = prompt.name; 553 | 554 | const textDiv = document.createElement('div'); 555 | textDiv.className = 'prompt-item-text'; 556 | textDiv.textContent = prompt.text; 557 | 558 | const statusDiv = document.createElement('div'); 559 | statusDiv.className = 'prompt-item-status'; 560 | if (prompt.isActive) { 561 | statusDiv.textContent = '当前使用中'; 562 | } else { 563 | statusDiv.style.display = 'none'; 564 | } 565 | 566 | const actionsDiv = document.createElement('div'); 567 | actionsDiv.className = 'prompt-item-actions'; 568 | 569 | const editBtn = document.createElement('button'); 570 | editBtn.className = 'edit-btn'; 571 | editBtn.innerHTML = ''; 572 | editBtn.onclick = () => editPrompt(prompt.id); 573 | 574 | const deleteBtn = document.createElement('button'); 575 | deleteBtn.className = 'delete-btn'; 576 | deleteBtn.innerHTML = ''; 577 | deleteBtn.onclick = () => deletePrompt(prompt.id); 578 | 579 | actionsDiv.appendChild(editBtn); 580 | actionsDiv.appendChild(deleteBtn); 581 | 582 | promptItem.appendChild(nameDiv); 583 | promptItem.appendChild(textDiv); 584 | promptItem.appendChild(statusDiv); 585 | promptItem.appendChild(actionsDiv); 586 | 587 | promptsList.appendChild(promptItem); 588 | }); 589 | } 590 | 591 | // 显示添加提示词弹窗 592 | function showAddPromptModal() { 593 | isEditMode = false; 594 | editingPromptId = null; 595 | document.getElementById("addPromptModal").classList.remove('hidden'); 596 | document.getElementById("newPromptName").value = ""; 597 | document.getElementById("newPromptText").value = ""; 598 | } 599 | 600 | // 隐藏添加提示词弹窗 601 | function hideAddPromptModal() { 602 | document.getElementById("addPromptModal").classList.add('hidden'); 603 | isEditMode = false; 604 | editingPromptId = null; 605 | } 606 | 607 | // 处理保存提示词(新增或更新) 608 | function handleSavePrompt() { 609 | const name = document.getElementById("newPromptName").value; 610 | const text = document.getElementById("newPromptText").value; 611 | 612 | if (!name || !text) { 613 | alert("请填写提示词名称和内容"); 614 | return; 615 | } 616 | 617 | if (isEditMode && editingPromptId) { 618 | console.log("更新现有提示词:", editingPromptId); 619 | // 更新现有提示词 620 | const prompt = prompts.find(p => p.id === editingPromptId); 621 | if (prompt) { 622 | prompt.name = name; 623 | prompt.text = text; 624 | 625 | // 如果编辑的是当前活跃的提示词,同时更新当前翻译提示词的文本框 626 | if (prompt.isActive) { 627 | document.getElementById("promptInput").value = text; 628 | // 保存到主进程 629 | window.electron.send("updatePrompt", text); 630 | } 631 | } 632 | } else { 633 | console.log("添加新提示词:", name); 634 | // 添加新提示词 635 | const newPrompt = { 636 | id: Date.now().toString(), 637 | name, 638 | text, 639 | isActive: false 640 | }; 641 | prompts.push(newPrompt); 642 | } 643 | 644 | savePrompts(); 645 | hideAddPromptModal(); 646 | } 647 | 648 | // 编辑提示词 649 | function editPrompt(id) { 650 | const prompt = prompts.find(p => p.id === id); 651 | if (!prompt) return; 652 | 653 | isEditMode = true; 654 | editingPromptId = id; 655 | 656 | // 显示弹窗并填充数据 657 | document.getElementById("addPromptModal").classList.remove('hidden'); 658 | document.getElementById("newPromptName").value = prompt.name; 659 | document.getElementById("newPromptText").value = prompt.text; 660 | } 661 | 662 | // 删除提示词 663 | function deletePrompt(id) { 664 | if (prompts.length <= 1) { 665 | alert("至少保留一个提示词"); 666 | return; 667 | } 668 | 669 | const prompt = prompts.find(p => p.id === id); 670 | if (!prompt) return; 671 | 672 | if (prompt.isActive) { 673 | alert("无法删除当前使用中的提示词"); 674 | return; 675 | } 676 | 677 | if (confirm(`确定要删除提示词"${prompt.name}"吗?`)) { 678 | console.log("删除提示词:", id, prompt.name); 679 | prompts = prompts.filter(p => p.id !== id); 680 | console.log("删除后的提示词列表:", prompts); 681 | savePrompts(); 682 | } 683 | } 684 | 685 | // 保存提示词列表到本地存储和主进程 686 | function savePrompts() { 687 | console.log("保存提示词列表:", prompts); 688 | 689 | // 保存到本地存储 690 | localStorage.setItem('prompts', JSON.stringify(prompts)); 691 | console.log("保存到本地存储完成"); 692 | 693 | // 保存到主进程 694 | window.electron.send('savePrompts', prompts); 695 | console.log("保存到主进程完成"); 696 | 697 | // 更新界面 698 | updatePromptSelect(); 699 | renderPromptsList(); 700 | } 701 | 702 | // 更新提示词UI(在修改prompts后调用此函数立即更新界面) 703 | function updatePromptsUI() { 704 | console.log("更新提示词UI"); 705 | 706 | // 更新提示词下拉列表 707 | updatePromptSelect(); 708 | 709 | // 更新提示词列表显示 710 | renderPromptsList(); 711 | 712 | // 更新当前活跃提示词的输入框 713 | const activePrompt = prompts.find(p => p.isActive); 714 | if (activePrompt) { 715 | document.getElementById("promptInput").value = activePrompt.text; 716 | } 717 | 718 | // 保存更改 719 | savePrompts(); 720 | } 721 | 722 | // 处理提示词选择变更 723 | function handlePromptSelect() { 724 | const selectElement = document.getElementById("promptSelect"); 725 | const selectedId = selectElement.value; 726 | 727 | // 更新活跃状态 728 | prompts.forEach(prompt => { 729 | prompt.isActive = (prompt.id === selectedId); 730 | }); 731 | 732 | // 更新提示词输入框 733 | const activePrompt = prompts.find(p => p.isActive); 734 | if (activePrompt) { 735 | document.getElementById("promptInput").value = activePrompt.text; 736 | // 保存到主进程 737 | window.electron.send("updatePrompt", activePrompt.text); 738 | } 739 | 740 | // 更新列表显示 741 | renderPromptsList(); 742 | savePrompts(); 743 | } 744 | 745 | // 模拟翻译 746 | function simulateTranslation() { 747 | const promptText = document.getElementById("promptInput").value; 748 | const modelSelect = document.getElementById("modelSelect").value; 749 | 750 | // 这里只是模拟,实际应用中应该调用真实的翻译API 751 | const sampleText = "这是一个模拟的翻译结果。在实际应用中,这里会显示通过选定的模型(" + modelSelect + ")翻译的文本。"; 752 | 753 | // 直接调用与真实翻译相同的处理方式 754 | const translatedTextArea = document.getElementById("translatedText"); 755 | translatedTextArea.value = sampleText; 756 | 757 | // 添加动画效果以提醒用户翻译已完成 758 | translatedTextArea.classList.add("translation-complete"); 759 | 760 | // 聚焦到翻译结果区域并选中所有文本 761 | translatedTextArea.focus(); 762 | translatedTextArea.select(); 763 | 764 | // 移除动画类,为下次翻译做准备 765 | setTimeout(() => { 766 | translatedTextArea.classList.remove("translation-complete"); 767 | }, 2000); 768 | 769 | // 也可以通过IPC调用主进程进行实际翻译 770 | // ipcRenderer.send("translate", { text: "要翻译的文本", model: modelSelect }); 771 | } 772 | 773 | // 重置提示词列表为预设值 774 | function resetPrompts() { 775 | if (!confirm("确定要重置提示词列表吗?这将删除所有自定义提示词。")) { 776 | return; 777 | } 778 | 779 | prompts = [...DEFAULT_PROMPTS]; 780 | 781 | // 保存到本地存储和主进程 782 | localStorage.setItem('prompts', JSON.stringify(prompts)); 783 | window.electron.send('savePrompts', prompts); 784 | 785 | // 更新界面 786 | updatePromptSelect(); 787 | renderPromptsList(); 788 | 789 | alert("提示词列表已重置为预设值。"); 790 | } 791 | 792 | // 应用内通知函数 793 | function showAppNotification(title, message) { 794 | // 检查是否存在通知容器,如果不存在则创建 795 | let notificationContainer = document.getElementById('app-notification-container'); 796 | if (!notificationContainer) { 797 | notificationContainer = document.createElement('div'); 798 | notificationContainer.id = 'app-notification-container'; 799 | notificationContainer.style.position = 'fixed'; 800 | notificationContainer.style.top = '20px'; 801 | notificationContainer.style.right = '20px'; 802 | notificationContainer.style.zIndex = '9999'; 803 | document.body.appendChild(notificationContainer); 804 | } 805 | 806 | // 创建通知元素 807 | const notification = document.createElement('div'); 808 | notification.className = 'app-notification'; 809 | notification.style.backgroundColor = 'rgba(60, 60, 60, 0.9)'; 810 | notification.style.color = 'white'; 811 | notification.style.padding = '12px 16px'; 812 | notification.style.marginBottom = '10px'; 813 | notification.style.borderRadius = '8px'; 814 | notification.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)'; 815 | notification.style.width = '300px'; 816 | notification.style.maxWidth = '100%'; 817 | notification.style.transition = 'all 0.3s ease'; 818 | notification.style.opacity = '0'; 819 | notification.style.transform = 'translateX(20px)'; 820 | 821 | // 创建标题 822 | const titleElement = document.createElement('div'); 823 | titleElement.textContent = title; 824 | titleElement.style.fontWeight = 'bold'; 825 | titleElement.style.marginBottom = '5px'; 826 | notification.appendChild(titleElement); 827 | 828 | // 创建消息 829 | const messageElement = document.createElement('div'); 830 | messageElement.textContent = message; 831 | messageElement.style.fontSize = '14px'; 832 | notification.appendChild(messageElement); 833 | 834 | // 添加到容器 835 | notificationContainer.appendChild(notification); 836 | 837 | // 触发过渡动画 838 | setTimeout(() => { 839 | notification.style.opacity = '1'; 840 | notification.style.transform = 'translateX(0)'; 841 | }, 10); 842 | 843 | // 自动消失 844 | setTimeout(() => { 845 | notification.style.opacity = '0'; 846 | notification.style.transform = 'translateX(20px)'; 847 | 848 | // 移除元素 849 | setTimeout(() => { 850 | notificationContainer.removeChild(notification); 851 | }, 300); 852 | }, 3000); 853 | } -------------------------------------------------------------------------------- /scripts/notarize.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { notarize } = require('electron-notarize'); 3 | 4 | exports.default = async function notarizing(context) { 5 | const { electronPlatformName, appOutDir } = context; 6 | if (electronPlatformName !== 'darwin') { 7 | return; 8 | } 9 | 10 | const appName = context.packager.appInfo.productFilename; 11 | const appPath = `${appOutDir}/${appName}.app`; 12 | 13 | if (!process.env.APPLE_ID || !process.env.APPLE_ID_PASSWORD || !process.env.APPLE_TEAM_ID) { 14 | console.warn('Skipping notarization: Required environment variables are missing.'); 15 | return; 16 | } 17 | 18 | console.log(`Notarizing ${appName}...`); 19 | 20 | try { 21 | await notarize({ 22 | appBundleId: 'com.copy2translate.app', 23 | appPath, 24 | appleId: process.env.APPLE_ID, 25 | appleIdPassword: process.env.APPLE_ID_PASSWORD, 26 | teamId: process.env.APPLE_TEAM_ID, 27 | }); 28 | } catch (error) { 29 | console.error('Notarization failed:', error); 30 | throw error; 31 | } 32 | 33 | console.log(`Done notarizing ${appName}`); 34 | }; -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 3 | margin: 0; 4 | padding: 0; 5 | background-color: #f5f5f5; 6 | color: #333; 7 | line-height: 1.5; 8 | } 9 | 10 | /* 顶部标题栏 */ 11 | .header { 12 | display: flex; 13 | justify-content: space-between; 14 | align-items: center; 15 | padding: 16px 24px; 16 | background-color: #fff; 17 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 18 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); 19 | max-width: 1147px; 20 | margin: 0 auto; 21 | box-sizing: border-box; 22 | } 23 | 24 | .app-title { 25 | font-size: 24px; 26 | font-weight: 600; 27 | color: #1a73e8; 28 | letter-spacing: -0.5px; 29 | } 30 | 31 | .header-actions { 32 | display: flex; 33 | gap: 12px; 34 | } 35 | 36 | .header-btn { 37 | display: flex; 38 | align-items: center; 39 | gap: 6px; 40 | padding: 8px 16px; 41 | background-color: #f8f9fa; 42 | border: 1px solid #dadce0; 43 | border-radius: 6px; 44 | font-size: 14px; 45 | font-weight: 500; 46 | color: #5f6368; 47 | cursor: pointer; 48 | transition: all 0.2s ease; 49 | } 50 | 51 | .header-btn:hover { 52 | background-color: #f1f3f4; 53 | border-color: #c6c6c6; 54 | color: #202124; 55 | } 56 | 57 | .btn-icon { 58 | font-size: 16px; 59 | } 60 | 61 | .container { 62 | max-width: 1200px; 63 | margin: 24px auto; 64 | padding: 0 24px; 65 | box-sizing: border-box; 66 | } 67 | 68 | /* 顶部设置行 - 快捷键和当前提示词并排 */ 69 | .top-settings-row { 70 | display: flex; 71 | gap: 24px; 72 | margin-bottom: 24px; 73 | box-sizing: border-box; 74 | } 75 | 76 | .shortcut-section { 77 | flex: 1; 78 | } 79 | 80 | .prompt-section { 81 | flex: 2; 82 | } 83 | 84 | h2 { 85 | font-size: 18px; 86 | font-weight: 600; 87 | color: #202124; 88 | margin: 0 0 20px 0; 89 | letter-spacing: -0.3px; 90 | } 91 | 92 | /* 基础表单元素样式 */ 93 | input, select, textarea { 94 | font-family: inherit; 95 | border-radius: 8px; 96 | border: 1.5px solid #dadce0; 97 | padding: 10px 14px; 98 | font-size: 14px; 99 | transition: all 0.2s ease; 100 | background-color: #fff; 101 | color: #202124; 102 | box-sizing: border-box; 103 | } 104 | 105 | /* 添加 placeholder 样式 */ 106 | input::placeholder, 107 | textarea::placeholder { 108 | color: #9aa0a6; 109 | opacity: 1; 110 | } 111 | 112 | input:focus, select:focus, textarea:focus { 113 | outline: none; 114 | border-color: #1a73e8; 115 | box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.1); 116 | } 117 | 118 | button { 119 | cursor: pointer; 120 | font-weight: 500; 121 | transition: all 0.2s ease; 122 | } 123 | 124 | .update-btn, .translate-btn, .save-btn { 125 | background-color: #1a73e8; 126 | color: white; 127 | border: none; 128 | padding: 10px 20px; 129 | border-radius: 8px; 130 | font-size: 14px; 131 | font-weight: 500; 132 | transition: all 0.2s ease; 133 | } 134 | 135 | .update-btn:hover, .translate-btn:hover, .save-btn:hover { 136 | background-color: #1557b0; 137 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 138 | } 139 | 140 | .settings-section { 141 | background-color: #fff; 142 | border-radius: 12px; 143 | padding: 24px; 144 | margin-bottom: 24px; 145 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); 146 | transition: box-shadow 0.2s ease; 147 | } 148 | 149 | .settings-section:hover { 150 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.08); 151 | } 152 | 153 | /* 快捷键设置 */ 154 | .shortcut-container { 155 | display: flex; 156 | flex-direction: column; 157 | gap: 15px; 158 | } 159 | 160 | .current-shortcut { 161 | display: flex; 162 | align-items: center; 163 | gap: 10px; 164 | } 165 | 166 | .shortcut-input-group { 167 | display: flex; 168 | gap: 10px; 169 | } 170 | 171 | .shortcut-input-group input { 172 | flex: 1; 173 | } 174 | 175 | .update-btn:hover { 176 | background-color: #3367d6; 177 | } 178 | 179 | /* 翻译提示词 */ 180 | .prompt-container { 181 | display: flex; 182 | flex-direction: column; 183 | gap: 15px; 184 | width: 100%; 185 | } 186 | 187 | .prompt-select-container { 188 | width: 100%; 189 | } 190 | 191 | .prompt-select-container select { 192 | width: 100%; 193 | } 194 | 195 | .prompt-input { 196 | width: 100%; 197 | height: 100px; 198 | resize: vertical; 199 | box-sizing: border-box; 200 | } 201 | 202 | /* 提示词管理 */ 203 | .prompt-management { 204 | display: flex; 205 | flex-direction: column; 206 | gap: 15px; 207 | } 208 | 209 | .prompts-list { 210 | border: 1px solid #eee; 211 | border-radius: 8px; 212 | max-height: 500px; 213 | overflow-y: auto; 214 | } 215 | 216 | .prompt-item { 217 | padding: 16px; 218 | border-bottom: 1px solid #eee; 219 | display: grid; 220 | grid-template-columns: 1fr 2fr auto auto; 221 | align-items: center; 222 | gap: 20px; 223 | transition: background-color 0.2s ease; 224 | } 225 | 226 | .prompt-item:hover { 227 | background-color: #f8f9fa; 228 | } 229 | 230 | .prompt-item-name { 231 | font-weight: 500; 232 | color: #202124; 233 | } 234 | 235 | .prompt-item-text { 236 | color: #5f6368; 237 | font-size: 14px; 238 | white-space: nowrap; 239 | overflow: hidden; 240 | text-overflow: ellipsis; 241 | max-width: 100%; 242 | } 243 | 244 | .prompt-item-status { 245 | font-size: 12px; 246 | font-weight: 500; 247 | padding: 4px 8px; 248 | border-radius: 12px; 249 | background-color: #e8f0fe; 250 | color: #1a73e8; 251 | } 252 | 253 | .prompt-item-actions { 254 | display: flex; 255 | gap: 5px; 256 | } 257 | 258 | .edit-btn, .delete-btn { 259 | padding: 8px; 260 | border-radius: 6px; 261 | border: none; 262 | background: transparent; 263 | transition: all 0.2s ease; 264 | } 265 | 266 | .edit-btn:hover, .delete-btn:hover { 267 | background-color: #f1f3f4; 268 | } 269 | 270 | .edit-btn svg, .delete-btn svg { 271 | transition: all 0.2s ease; 272 | } 273 | 274 | .edit-btn:hover svg path, .delete-btn:hover svg path { 275 | stroke: #202124; 276 | } 277 | 278 | .add-prompt-btn { 279 | align-self: flex-end; 280 | background-color: white; 281 | border: 1px solid #ddd; 282 | padding: 8px 15px; 283 | } 284 | 285 | /* 翻译结果 */ 286 | .translation-result-container { 287 | display: flex; 288 | flex-direction: column; 289 | gap: 15px; 290 | width: 100%; 291 | } 292 | 293 | .model-selection { 294 | display: flex; 295 | align-items: center; 296 | gap: 10px; 297 | width: 100%; 298 | } 299 | 300 | .model-selection select { 301 | flex: 1; 302 | } 303 | 304 | .translation-result { 305 | width: 100%; 306 | height: 150px; 307 | resize: vertical; 308 | box-sizing: border-box; 309 | } 310 | 311 | .translate-btn { 312 | align-self: flex-end; 313 | background-color: #4285f4; 314 | color: white; 315 | border: none; 316 | padding: 8px 20px; 317 | } 318 | 319 | .translate-btn:hover { 320 | background-color: #3367d6; 321 | } 322 | 323 | /* 弹窗 */ 324 | .modal { 325 | position: fixed; 326 | top: 0; 327 | left: 0; 328 | width: 100%; 329 | height: 100%; 330 | background-color: rgba(0, 0, 0, 0.5); 331 | display: flex; 332 | justify-content: center; 333 | align-items: center; 334 | z-index: 1000; 335 | } 336 | 337 | .modal-content { 338 | background-color: white; 339 | padding: 20px; 340 | border-radius: 8px; 341 | max-width: 500px; 342 | width: 90%; 343 | } 344 | 345 | .modal-content h2 { 346 | margin-top: 0; 347 | } 348 | 349 | .modal-content input, 350 | .modal-content textarea { 351 | width: 100%; 352 | box-sizing: border-box; 353 | } 354 | 355 | .modal-content textarea { 356 | height: 100px; 357 | resize: vertical; 358 | } 359 | 360 | .modal-buttons { 361 | display: flex; 362 | justify-content: flex-end; 363 | gap: 10px; 364 | margin-top: 10px; 365 | } 366 | 367 | #savePromptBtn { 368 | background-color: #4285f4; 369 | color: white; 370 | border: none; 371 | } 372 | 373 | #savePromptBtn:hover { 374 | background-color: #3367d6; 375 | } 376 | 377 | /* 暗色主题 */ 378 | .dark-theme { 379 | background-color: #202124; 380 | color: #e8eaed; 381 | } 382 | 383 | .dark-theme .header { 384 | background-color: #292a2d; 385 | border-bottom-color: rgba(255, 255, 255, 0.1); 386 | } 387 | 388 | .dark-theme .app-title { 389 | color: #4285f4; 390 | } 391 | 392 | .dark-theme .header-btn { 393 | background-color: #333; 394 | border-color: #444; 395 | color: #e0e0e0; 396 | } 397 | 398 | .dark-theme .header-btn:hover { 399 | background-color: #444; 400 | } 401 | 402 | .dark-theme .settings-section { 403 | background-color: #292a2d; 404 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); 405 | } 406 | 407 | .dark-theme .settings-section:hover { 408 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); 409 | } 410 | 411 | .dark-theme input, 412 | .dark-theme select, 413 | .dark-theme textarea { 414 | background-color: #202124; 415 | border-color: #5f6368; 416 | color: #e8eaed; 417 | } 418 | 419 | .dark-theme input:focus, 420 | .dark-theme select:focus, 421 | .dark-theme textarea:focus { 422 | border-color: #8ab4f8; 423 | box-shadow: 0 0 0 2px rgba(138, 180, 248, 0.1); 424 | } 425 | 426 | .dark-theme button { 427 | background-color: #333; 428 | color: #e0e0e0; 429 | } 430 | 431 | .dark-theme button:hover { 432 | background-color: #444; 433 | } 434 | 435 | .dark-theme .update-btn, 436 | .dark-theme .translate-btn, 437 | .dark-theme #savePromptBtn { 438 | background-color: #4285f4; 439 | color: white; 440 | } 441 | 442 | .dark-theme .update-btn:hover, 443 | .dark-theme .translate-btn:hover, 444 | .dark-theme #savePromptBtn:hover { 445 | background-color: #3367d6; 446 | } 447 | 448 | .dark-theme .prompts-list { 449 | border-color: #444; 450 | } 451 | 452 | .dark-theme .prompt-item:hover { 453 | background-color: #303134; 454 | } 455 | 456 | .dark-theme .prompt-item-name { 457 | color: #e8eaed; 458 | } 459 | 460 | .dark-theme .prompt-item-text { 461 | color: #9aa0a6; 462 | } 463 | 464 | .dark-theme .prompt-item-status { 465 | background-color: rgba(138, 180, 248, 0.2); 466 | color: #8ab4f8; 467 | } 468 | 469 | .dark-theme .edit-btn:hover, 470 | .dark-theme .delete-btn:hover { 471 | background-color: #303134; 472 | } 473 | 474 | .dark-theme .edit-btn:hover svg path, 475 | .dark-theme .delete-btn:hover svg path { 476 | stroke: #e8eaed; 477 | } 478 | 479 | .dark-theme .add-prompt-btn { 480 | background-color: #252525; 481 | border-color: #444; 482 | } 483 | 484 | .dark-theme .modal-content { 485 | background-color: #2d2d2d; 486 | color: #ffffff; 487 | } 488 | 489 | /* API Key 弹窗样式 */ 490 | .api-key-modal { 491 | width: 600px; 492 | max-width: 95%; 493 | padding: 30px; 494 | } 495 | 496 | .api-key-modal h2 { 497 | font-size: 20px; 498 | margin-bottom: 20px; 499 | text-align: left; 500 | } 501 | 502 | .provider-selection p { 503 | font-size: 16px; 504 | margin-bottom: 15px; 505 | text-align: left; 506 | } 507 | 508 | .provider-buttons { 509 | display: flex; 510 | gap: 15px; 511 | margin-bottom: 25px; 512 | } 513 | 514 | .provider-btn { 515 | display: flex; 516 | align-items: center; 517 | gap: 8px; 518 | padding: 10px 15px; 519 | border: 1px solid #dadce0; 520 | border-radius: 6px; 521 | background-color: #f8f9fa; 522 | font-size: 15px; 523 | transition: all 0.2s; 524 | } 525 | 526 | .provider-btn.active { 527 | border-color: #4285f4; 528 | background-color: #e8f0fe; 529 | color: #1a73e8; 530 | } 531 | 532 | .provider-icon { 533 | font-size: 18px; 534 | } 535 | 536 | .provider-status { 537 | font-size: 13px; 538 | color: #5f6368; 539 | } 540 | 541 | .provider-status.status-set { 542 | color: #34a853; 543 | } 544 | 545 | .provider-status.status-unset { 546 | color: #ea4335; 547 | } 548 | 549 | .provider-btn.active .provider-status { 550 | color: #1a73e8; 551 | } 552 | 553 | .provider-btn.active .provider-status.status-set { 554 | color: #34a853; 555 | } 556 | 557 | .provider-btn.active .provider-status.status-unset { 558 | color: #ea4335; 559 | } 560 | 561 | .setting-item input[placeholder="尚未设置 API Key"] { 562 | color: #ea4335; 563 | font-style: italic; 564 | } 565 | 566 | /* 暗色主题下的状态样式 */ 567 | .dark-theme .provider-status.status-set { 568 | color: #81c995; 569 | } 570 | 571 | .dark-theme .provider-status.status-unset { 572 | color: #f28b82; 573 | } 574 | 575 | .dark-theme .provider-btn.active .provider-status.status-set { 576 | color: #81c995; 577 | } 578 | 579 | .dark-theme .provider-btn.active .provider-status.status-unset { 580 | color: #f28b82; 581 | } 582 | 583 | .dark-theme .setting-item input[placeholder="尚未设置 API Key"] { 584 | color: #f28b82; 585 | } 586 | 587 | .provider-settings { 588 | border: 1px solid #dadce0; 589 | border-radius: 8px; 590 | padding: 20px; 591 | margin-bottom: 25px; 592 | position: relative; 593 | } 594 | 595 | .provider-title { 596 | font-size: 18px; 597 | font-weight: 500; 598 | margin-bottom: 20px; 599 | } 600 | 601 | .setup-btn { 602 | position: absolute; 603 | top: 20px; 604 | right: 20px; 605 | padding: 6px 12px; 606 | background-color: #f8f9fa; 607 | border: 1px solid #dadce0; 608 | border-radius: 4px; 609 | font-size: 14px; 610 | } 611 | 612 | .setting-item { 613 | margin-bottom: 20px; 614 | } 615 | 616 | .setting-item label { 617 | display: block; 618 | font-size: 14px; 619 | margin-bottom: 8px; 620 | color: #5f6368; 621 | } 622 | 623 | .setting-item input { 624 | width: 100%; 625 | padding: 10px 12px; 626 | border: 1px solid #dadce0; 627 | border-radius: 4px; 628 | font-size: 15px; 629 | } 630 | 631 | .setting-item input[readonly] { 632 | background-color: #f8f9fa; 633 | } 634 | 635 | .password-input-container { 636 | display: flex; 637 | align-items: center; 638 | } 639 | 640 | .password-input-container input { 641 | flex: 1; 642 | border-top-right-radius: 0; 643 | border-bottom-right-radius: 0; 644 | } 645 | 646 | .toggle-password-btn { 647 | padding: 10px 12px; 648 | border: 1px solid #dadce0; 649 | border-left: none; 650 | border-radius: 0 4px 4px 0; 651 | background-color: #f8f9fa; 652 | } 653 | 654 | .eye-icon { 655 | font-size: 16px; 656 | } 657 | 658 | .modal-buttons { 659 | display: flex; 660 | justify-content: flex-end; 661 | gap: 15px; 662 | } 663 | 664 | .cancel-btn { 665 | padding: 10px 20px; 666 | background-color: #f8f9fa; 667 | border: 1px solid #dadce0; 668 | border-radius: 4px; 669 | font-size: 15px; 670 | } 671 | 672 | .save-btn { 673 | padding: 10px 20px; 674 | background-color: #1a73e8; 675 | color: white; 676 | border: none; 677 | border-radius: 4px; 678 | font-size: 15px; 679 | } 680 | 681 | .save-btn:hover { 682 | background-color: #1765cc; 683 | } 684 | 685 | /* 暗色主题下的API Key弹窗样式 */ 686 | .dark-theme .api-key-modal { 687 | background-color: #252525; 688 | } 689 | 690 | .dark-theme .provider-btn { 691 | background-color: #333; 692 | border-color: #444; 693 | color: #e0e0e0; 694 | } 695 | 696 | .dark-theme .provider-btn.active { 697 | border-color: #4285f4; 698 | background-color: rgba(66, 133, 244, 0.2); 699 | color: #4285f4; 700 | } 701 | 702 | .dark-theme .provider-status { 703 | color: #aaa; 704 | } 705 | 706 | .dark-theme .provider-btn.active .provider-status { 707 | color: #4285f4; 708 | } 709 | 710 | .dark-theme .provider-settings { 711 | border-color: #444; 712 | } 713 | 714 | .dark-theme .setting-item label { 715 | color: #aaa; 716 | } 717 | 718 | .dark-theme .setting-item input { 719 | background-color: #333; 720 | border-color: #444; 721 | color: #e0e0e0; 722 | } 723 | 724 | .dark-theme .setting-item input[readonly] { 725 | background-color: #2a2a2a; 726 | } 727 | 728 | .dark-theme .toggle-password-btn { 729 | background-color: #333; 730 | border-color: #444; 731 | color: #e0e0e0; 732 | } 733 | 734 | .dark-theme .cancel-btn { 735 | background-color: #333; 736 | border-color: #444; 737 | color: #e0e0e0; 738 | } 739 | 740 | .dark-theme .save-btn { 741 | background-color: #4285f4; 742 | } 743 | 744 | .dark-theme .save-btn:hover { 745 | background-color: #3367d6; 746 | } 747 | 748 | .hidden { 749 | display: none !important; 750 | } 751 | 752 | .prompt-buttons { 753 | display: flex; 754 | justify-content: flex-end; 755 | gap: 10px; 756 | margin-top: 10px; 757 | } 758 | 759 | .add-prompt-btn, .reset-prompts-btn { 760 | background-color: white; 761 | border: 1px solid #ddd; 762 | padding: 8px 15px; 763 | border-radius: 6px; 764 | font-size: 14px; 765 | transition: all 0.2s ease; 766 | } 767 | 768 | .add-prompt-btn:hover, .reset-prompts-btn:hover { 769 | background-color: #f1f3f4; 770 | border-color: #c6c6c6; 771 | } 772 | 773 | .reset-prompts-btn { 774 | color: #5f6368; 775 | } 776 | 777 | .dark-theme .add-prompt-btn, .dark-theme .reset-prompts-btn { 778 | background-color: #252525; 779 | border-color: #444; 780 | color: #e0e0e0; 781 | } 782 | 783 | .dark-theme .add-prompt-btn:hover, .dark-theme .reset-prompts-btn:hover { 784 | background-color: #303134; 785 | } 786 | 787 | /* 页脚样式 */ 788 | .footer { 789 | text-align: center; 790 | padding: 15px 0; 791 | font-size: 14px; 792 | color: #5f6368; 793 | background-color: #f5f5f5; 794 | border-top: 1px solid #e0e0e0; 795 | margin-top: 20px; 796 | } 797 | 798 | .footer a { 799 | color: #1a73e8; 800 | text-decoration: none; 801 | transition: color 0.2s; 802 | } 803 | 804 | .footer a:hover { 805 | color: #174ea6; 806 | text-decoration: underline; 807 | } 808 | 809 | /* 暗色主题下的页脚样式 */ 810 | .dark-theme .footer { 811 | background-color: #202124; 812 | color: #9aa0a6; 813 | border-top: 1px solid #3c4043; 814 | } 815 | 816 | .dark-theme .footer a { 817 | color: #8ab4f8; 818 | } 819 | 820 | .dark-theme .footer a:hover { 821 | color: #aecbfa; 822 | } 823 | 824 | /* 翻译完成动画 */ 825 | @keyframes highlight-pulse { 826 | 0% { box-shadow: 0 0 5px rgba(26, 115, 232, 0.5); background-color: rgba(232, 240, 254, 0.5); } 827 | 50% { box-shadow: 0 0 15px rgba(26, 115, 232, 0.8); background-color: rgba(232, 240, 254, 0.8); } 828 | 100% { box-shadow: 0 0 5px rgba(26, 115, 232, 0.5); background-color: rgba(232, 240, 254, 0.5); } 829 | } 830 | 831 | .translation-complete { 832 | animation: highlight-pulse 1s ease-in-out 2; 833 | border-color: #1a73e8 !important; 834 | } 835 | 836 | /* 暗色主题下的翻译完成动画 */ 837 | .dark-theme .translation-complete { 838 | animation: highlight-pulse-dark 1s ease-in-out 2; 839 | } 840 | 841 | @keyframes highlight-pulse-dark { 842 | 0% { box-shadow: 0 0 5px rgba(138, 180, 248, 0.5); background-color: rgba(25, 26, 28, 0.9); } 843 | 50% { box-shadow: 0 0 15px rgba(138, 180, 248, 0.8); background-color: rgba(48, 49, 52, 0.9); } 844 | 100% { box-shadow: 0 0 5px rgba(138, 180, 248, 0.5); background-color: rgba(25, 26, 28, 0.9); } 845 | } --------------------------------------------------------------------------------