├── src
├── assets
│ ├── icons
│ │ ├── app-icon.icns
│ │ ├── app-icon.ico
│ │ ├── tray-icon.png
│ │ └── app-icon.svg
│ └── locales
│ │ ├── zh-CN.json
│ │ └── en-US.json
├── shared
│ ├── defines.js
│ ├── appUtils.js
│ └── i18n.js
├── renderer
│ ├── css
│ │ ├── themes.css
│ │ └── styles.css
│ ├── index.html
│ ├── js
│ │ ├── ui-manager.js
│ │ ├── context-menu.js
│ │ ├── edit-item.js
│ │ ├── settings.js
│ │ └── renderer.js
│ ├── edit-item.html
│ └── settings.html
├── main
│ ├── tray-manager.js
│ ├── main.js
│ ├── item-handler.js
│ ├── data-store.js
│ ├── ipc-handler.js
│ └── window-manager.js
└── preload
│ └── preload.js
├── doc
└── imgs
│ └── launcher_app_main_window.png
├── .vscode
└── launch.json
├── LICENSE
├── README_zh.md
├── package.json
├── .gitignore
├── README.md
└── .github
└── copilot-instructions.md
/src/assets/icons/app-icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SolarianZ/launcher-app-electron/main/src/assets/icons/app-icon.icns
--------------------------------------------------------------------------------
/src/assets/icons/app-icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SolarianZ/launcher-app-electron/main/src/assets/icons/app-icon.ico
--------------------------------------------------------------------------------
/src/assets/icons/tray-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SolarianZ/launcher-app-electron/main/src/assets/icons/tray-icon.png
--------------------------------------------------------------------------------
/doc/imgs/launcher_app_main_window.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SolarianZ/launcher-app-electron/main/doc/imgs/launcher_app_main_window.png
--------------------------------------------------------------------------------
/src/shared/defines.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 共享常量和类定义
3 | * 此文件包含在主进程和渲染进程之间共享的常量、枚举和类定义
4 | * 用于确保代码一致性和类型安全
5 | */
6 |
7 | /**
8 | * 路径类型枚举
9 | * 用于标识不同类型的项目
10 | * @readonly
11 | * @enum {string}
12 | */
13 | const PathType = {
14 | FILE: "file", // 文件类型
15 | FOLDER: "folder", // 文件夹类型
16 | URL: "url", // 网址类型
17 | COMMAND: "command", // 命令类型
18 | };
19 |
20 | /**
21 | * 列表项类
22 | * 表示启动器中的一个项目
23 | */
24 | class ListItem {
25 | /**
26 | * 创建一个新的列表项
27 | * @param {string} path - 项目的路径、URL或命令
28 | * @param {string} type - 项目类型,使用PathType枚举值
29 | */
30 | constructor(path, type) {
31 | this.path = path;
32 | this.type = type;
33 | // 注意:name属性可以后续添加,默认为空
34 | }
35 | }
36 |
37 | // 导出模块定义
38 | module.exports = {
39 | PathType,
40 | ListItem,
41 | };
42 |
--------------------------------------------------------------------------------
/src/shared/appUtils.js:
--------------------------------------------------------------------------------
1 | const { app } = require('electron');
2 |
3 | /**
4 | * 更新自启动设置
5 | * 根据配置更新应用是否开机启动
6 | * @param {boolean} enabled 是否启用自启动
7 | */
8 | function updateAutoLaunchSettings(enabled) {
9 | // 开发环境禁止设置自启动(会导致node_modules中的electron自启)
10 | if (enabled && !app.isPackaged) {
11 | console.error('Error enabling auto launch: Auto launch is disabled in development mode.');
12 | return false;
13 | }
14 |
15 | try {
16 | app.setLoginItemSettings({
17 | openAtLogin: enabled,
18 | openAsHidden: true,
19 | args: ['--autostart']
20 | });
21 | console.log(`Auto launch ${enabled ? 'enabled' : 'disabled'}`);
22 | return true;
23 | } catch (error) {
24 | console.error('Error setting auto launch:', error);
25 | return false;
26 | }
27 | }
28 |
29 | module.exports = {
30 | updateAutoLaunchSettings
31 | };
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.0",
3 | "compounds": [
4 | {
5 | "name": "Main + Renderer",
6 | "configurations": ["Main", "Renderer"],
7 | "stopAll": true
8 | }
9 | ],
10 | "configurations": [
11 | {
12 | "name": "Renderer",
13 | "port": 9222,
14 | "request": "attach",
15 | "type": "chrome",
16 | "webRoot": "${workspaceFolder}"
17 | },
18 | {
19 | "name": "Main",
20 | "type": "node",
21 | "request": "launch",
22 | "cwd": "${workspaceFolder}",
23 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
24 | "windows": {
25 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
26 | },
27 | "args": [".", "--remote-debugging-port=9222"],
28 | "outputCapture": "std",
29 | "console": "integratedTerminal"
30 | }
31 | ]
32 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 ZQY
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/renderer/css/themes.css:
--------------------------------------------------------------------------------
1 | /* 深色主题 */
2 | .dark-theme {
3 | --primary-color: #5c9ce6;
4 | --secondary-color: #7ab85f;
5 | --text-color: #e0e0e0;
6 | --bg-color: #1e1e1e;
7 | --card-bg: #2d2d2d;
8 | --border-color: #444;
9 | --hover-bg: #3a3a3a;
10 | --active-bg: #444;
11 | --disabled-bg: #2a2a2a;
12 | --disabled-text: #777;
13 | --shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
14 | --title-bar-bg: #333;
15 | --title-bar-text: #e0e0e0;
16 | --search-bg: #3d3d3d;
17 | --modal-overlay: rgba(0, 0, 0, 0.7);
18 | --tooltip-bg: #e0e0e0;
19 | --tooltip-text: #333;
20 | --drag-indicator: #5c9ce6;
21 | --icon-color: #aaa;
22 | --menu-bg: #2d2d2d;
23 | --menu-hover: #3a3a3a;
24 | --menu-border: #444;
25 | --button-text: #fff;
26 | --button-bg: #5c9ce6;
27 | --button-hover: #4c8cd6;
28 | --button-active: #3c7cc6;
29 | --button-disabled: #3a5980;
30 | --cancel-button-bg: #3d3d3d;
31 | --cancel-button-text: #e0e0e0;
32 | --cancel-button-hover: #4a4a4a;
33 | --cancel-button-active: #555;
34 | --error-color: #e05c5c;
35 | }
--------------------------------------------------------------------------------
/src/assets/icons/app-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README_zh.md:
--------------------------------------------------------------------------------
1 | # Launcher App
2 |
3 | [English](./README.md)
4 |
5 | > 🚀 快速访问常用文件、文件夹、URL 和命令的桌面启动器应用
6 |
7 | 
8 | 
9 |
10 | 
11 |
12 | ## 📋 项目简介
13 |
14 | Launcher App 是一款基于 Electron 开发的桌面快速启动器工具,帮助用户快速访问常用的文件、文件夹、网站和命令。
15 |
16 | > ⚠️ **声明:**
17 | > 1. 本项目的代码主要由人工智能辅助生成。项目结构、功能实现及界面设计均使用了 AI 技术。
18 | > 2. 此文档内容亦主要由 AI 生成。
19 | > 3. 软件未经严格测试。
20 |
21 | ## ✨ 主要功能
22 |
23 | - 🗂️ 添加和管理多种类型的项目
24 | - 文件
25 | - 文件夹
26 | - 网址和 Deep Link
27 | - 命令行指令
28 | - 🔍 快速搜索项目
29 | - 🖱️ 支持拖放文件/文件夹直接添加
30 | - 📋 右键菜单提供丰富操作选项
31 | - 🌓 支持深色和浅色主题
32 | - 🌍 支持多语言
33 | - 内置中文、英文支持
34 | - 可以通过添加翻译文件来支持更多语言
35 | - ⌨️ 自定义全局快捷键呼出应用(默认是Alt+Shift+Q)
36 | - 🧩 系统托盘集成,显示最近使用的项目
37 | - 🔄 支持拖拽重新排序列表
38 | - ⚡ 通过双击或回车快速打开项目
39 | - 💬 跨平台支持 (Windows, macOS, Linux,未严格测试)
40 |
41 | ## 📥 构建
42 |
43 | 1. 确保已安装 [Node.js](https://nodejs.org/) (推荐 22 LTS 或更高版本)
44 |
45 | 2. 克隆仓库
46 |
47 | ```bash
48 | git clone https://github.com/SolarianZ/launcher-app-electron.git
49 | cd launcher-app-electron
50 | ```
51 |
52 | 3. 安装依赖
53 |
54 | ```bash
55 | npm install
56 | ```
57 |
58 | 4. 启动应用
59 |
60 | ```bash
61 | npm start
62 | ```
63 |
64 | ### 📦 打包应用
65 |
66 | 使用 electron-builder 打包为可分发的应用程序:
67 |
68 | ```bash
69 | npm run build
70 | ```
71 |
72 | 生成的安装包将保存在 `dist` 目录中。
73 |
74 | ## 🧩 技术实现
75 |
76 | - **Electron**:跨平台桌面应用框架
77 | - **模块化架构**:主进程和渲染进程分离
78 | - **IPC通信**:进程间安全通信
79 | - **本地存储**:JSON文件持久化数据
80 | - **国际化**:多语言支持系统
81 | - **响应式UI**:适配不同尺寸和主题
82 |
83 | 更多项目细节参考 [copilot-instructions](./.github/copilot-instructions.md) 。
84 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "launcher-app",
3 | "version": "1.0.0",
4 | "description": "快速访问常用文件、文件夹、URL和指令的工具",
5 | "main": "src/main/main.js",
6 | "scripts": {
7 | "start": "electron .",
8 | "build": "electron-builder"
9 | },
10 | "author": "ZQY",
11 | "homepage": "https://github.com/SolarianZ/launcher-app-electron",
12 | "license": "MIT",
13 | "devDependencies": {
14 | "electron": "^35.1.4",
15 | "electron-builder": "^26.0.12"
16 | },
17 | "build": {
18 | "appId": "com.zqy.launcher-app",
19 | "productName": "Launcher",
20 | "executableName": "Launcher",
21 | "files": [
22 | "src/**/*",
23 | "package.json",
24 | "!src/assets/locales/**/*"
25 | ],
26 | "extraResources": [
27 | {
28 | "from": "src/assets/locales",
29 | "to": "app/src/assets/locales",
30 | "filter": [
31 | "*.json"
32 | ]
33 | }
34 | ],
35 | "directories": {
36 | "output": "dist"
37 | },
38 | "win": {
39 | "target": "nsis",
40 | "icon": "src/assets/icons/app-icon.ico"
41 | },
42 | "nsis": {
43 | "oneClick": false,
44 | "allowToChangeInstallationDirectory": true,
45 | "createDesktopShortcut": true,
46 | "createStartMenuShortcut": true,
47 | "shortcutName": "Launcher",
48 | "perMachine": false,
49 | "allowElevation": true
50 | },
51 | "mac": {
52 | "target": "dmg",
53 | "icon": "src/assets/icons/app-icon.icns"
54 | },
55 | "linux": {
56 | "target": "AppImage",
57 | "icon": "src/assets/icons/app-icon.ico",
58 | "category": "Utility"
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/src/renderer/index.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 | Launcher
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
Launcher
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/assets/locales/zh-CN.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings": "设置",
3 | "appearance": "外观",
4 | "theme": "主题",
5 | "system-theme": "跟随系统",
6 | "light-theme": "浅色",
7 | "dark-theme": "深色",
8 | "language": "语言",
9 | "system-language": "跟随系统",
10 | "zh-CN": "中文",
11 | "en-US": "English",
12 | "add-language": "添加语言",
13 | "shortcuts": "快捷键",
14 | "enable-global-shortcut": "启用全局快捷键",
15 | "enable-shortcut-desc": "使用快捷键快速打开应用",
16 | "launcher-shortcut": "打开 Launcher 快捷键",
17 | "record-shortcut": "记录",
18 | "reset": "重置",
19 | "press-new-shortcut": "按下新的快捷键",
20 | "shortcut-input-desc": "点击\"记录\"按钮后按下组合键",
21 | "shortcut-need-modifier": "快捷键需要至少包含 Ctrl 或 Alt 之一",
22 | "shortcut-need-modifier-macos": "快捷键需要至少包含 Control 、 Option 或 Command 之一",
23 | "shortcut-taken": "该快捷键已被其他应用占用",
24 | "shortcut-invalid": "无效的快捷键",
25 | "shortcut-saved": "快捷键已保存",
26 | "shortcut-reset": "快捷键已重置为默认值",
27 | "enable-auto-launch": "开机自启动",
28 | "enable-auto-launch-desc": "系统启动时自动运行 Launcher",
29 | "auto-launch-enabled": "开机自启动已启用",
30 | "auto-launch-disabled": "开机自启动已关闭",
31 | "auto-launch-update-failed": "更新开机自启动设置失败",
32 | "data": "数据",
33 | "clear-data": "清空所有记录",
34 | "clear-data-desc": "移除所有已记录的项目",
35 | "data-cleared": "所有记录已清空",
36 | "open-storage": "打开应用数据文件夹",
37 | "open-storage-desc": "在文件夹中查看应用数据文件",
38 | "about": "关于",
39 | "version": "版本",
40 | "app-desc": "快速访问常用文件、文件夹、网址和命令的工具",
41 | "report-issue": "报告问题",
42 | "dev-tools-tip": "按F12打开Chrome开发者工具",
43 | "confirm-clear-data": "确定要移除所有记录吗?此操作不可撤销。",
44 | "app-name": "Launcher",
45 | "close": "关闭",
46 | "cancel": "取消",
47 | "save": "保存",
48 | "edit": "编辑",
49 | "remove": "移除",
50 | "open": "打开",
51 | "show-in-folder": "在文件夹中显示",
52 | "copy-path": "复制路径",
53 | "entry-exists": "项目已存在",
54 | "save-failed": "保存失败",
55 | "item-not-exist": "项目不存在",
56 | "clear-failed": "清除失败",
57 | "search": "搜索...",
58 | "add-new-item": "添加新项目",
59 | "item-content": "项目内容",
60 | "enter-path": "输入路径、网址或命令...",
61 | "enter-item-name": "输入项目名称(可选)",
62 | "file": "文件",
63 | "folder": "文件夹",
64 | "url": "URL",
65 | "command": "指令",
66 | "command-tip": "要执行多行命令,请将它们写入脚本文件,然后执行脚本文件",
67 | "select-file": "选择文件",
68 | "select-folder": "选择文件夹",
69 | "enter-path-required": "请输入路径或命令",
70 | "select-type-required": "请选择项目类型",
71 | "select-file-failed": "选择文件失败,请重试",
72 | "select-folder-failed": "选择文件夹失败,请重试",
73 | "update-failed": "更新失败,请重试",
74 | "add-failed": "添加失败,请重试",
75 | "context-open": "打开",
76 | "context-edit": "编辑",
77 | "context-remove": "移除",
78 | "context-show-in-folder": "在文件夹中显示",
79 | "context-copy-path": "复制路径",
80 | "context-copy-name": "复制名称",
81 | "context-copy": "复制",
82 | "context-execute": "执行",
83 | "tray-exit": "退出",
84 | "empty-list": "点击 + 按钮添加新项目",
85 | "cannot-get-file-path": "无法获取文件路径",
86 | "item-already-exists": "项目已存在"
87 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # vitepress build output
108 | **/.vitepress/dist
109 |
110 | # vitepress cache directory
111 | **/.vitepress/cache
112 |
113 | # Docusaurus cache and generated files
114 | .docusaurus
115 |
116 | # Serverless directories
117 | .serverless/
118 |
119 | # FuseBox cache
120 | .fusebox/
121 |
122 | # DynamoDB Local files
123 | .dynamodb/
124 |
125 | # TernJS port file
126 | .tern-port
127 |
128 | # Stores VSCode versions used for testing VSCode extensions
129 | .vscode-test
130 |
131 | # yarn v2
132 | .yarn/cache
133 | .yarn/unplugged
134 | .yarn/build-state.yml
135 | .yarn/install-state.gz
136 | .pnp.*
137 | .DS_Store
138 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Launcher App
2 |
3 | [中文](./README_zh.md)
4 |
5 | > 🚀 A desktop launcher application for quick access to frequently used files, folders, URLs, and commands
6 |
7 | 
8 | 
9 |
10 | 
11 |
12 | ## 📋 Project Introduction
13 |
14 | Launcher App is a desktop quick launcher tool developed with Electron, helping users quickly access commonly used files, folders, websites, and commands.
15 |
16 | > ⚠️ **Disclaimer:**
17 | > 1. The code of this project is mainly generated with AI assistance. The project structure, functionality implementation, and interface design all used AI technology.
18 | > 2. This document content is also mainly AI-generated.
19 | > 3. The software has not been rigorously tested.
20 |
21 | ## ✨ Main Features
22 |
23 | - 🗂️ Add and manage multiple types of items
24 | - Files
25 | - Folders
26 | - URLs and Deep Links
27 | - Command line instructions
28 | - 🔍 Quick item search
29 | - 🖱️ Support drag and drop to add files/folders
30 | - 📋 Rich operations through right-click menu
31 | - 🌓 Support dark and light themes
32 | - 🌍 Support for multiple languages
33 | - Built-in Chinese and English support
34 | - Support more languages by adding translation files
35 | - ⌨️ Custom global shortcut key to bring up the app (default is Alt+Shift+Q)
36 | - 🧩 System tray integration, showing recently used items
37 | - 🔄 Support drag and drop to reorder the list
38 | - ⚡ Quick open items with double-click or Enter key
39 | - 💬 Cross-platform support (Windows, macOS, Linux, not rigorously tested)
40 |
41 | ## 📥 Build
42 |
43 | 1. Make sure you have [Node.js](https://nodejs.org/) installed (recommended 22 LTS or higher)
44 |
45 | 2. Clone the repository
46 |
47 | ```bash
48 | git clone https://github.com/SolarianZ/launcher-app-electron.git
49 | cd launcher-app-electron
50 | ```
51 |
52 | 3. Install dependencies
53 |
54 | ```bash
55 | npm install
56 | ```
57 |
58 | 4. Start the application
59 |
60 | ```bash
61 | npm start
62 | ```
63 |
64 | ### 📦 Package the Application
65 |
66 | Use electron-builder to package for distribution:
67 |
68 | ```bash
69 | npm run build
70 | ```
71 |
72 | The generated installation packages will be saved in the `dist` directory.
73 |
74 | ## 🧩 Technical Implementation
75 |
76 | - **Electron**: Cross-platform desktop application framework
77 | - **Modular Architecture**: Separation of main process and renderer process
78 | - **IPC Communication**: Secure inter-process communication
79 | - **Local Storage**: JSON file for persistent data
80 | - **Internationalization**: Multi-language support system
81 | - **Responsive UI**: Adapts to different sizes and themes
82 |
83 | For more project details, refer to [copilot-instructions](./.github/copilot-instructions.md).
84 |
--------------------------------------------------------------------------------
/src/main/tray-manager.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 托盘管理模块
3 | * 负责创建和管理系统托盘图标、托盘菜单
4 | * 处理不同平台(Windows, macOS, Linux)下的托盘特性差异
5 | */
6 | const { Tray, Menu, nativeImage, app } = require('electron');
7 | const path = require('path');
8 | const i18n = require('../shared/i18n');
9 |
10 | // 全局托盘引用 - 防止被垃圾回收
11 | let tray = null;
12 |
13 | /**
14 | * 创建系统托盘
15 | * @param {Function} onClickTray 托盘点击行为
16 | * @returns {Tray} 创建的托盘对象
17 | */
18 | function createTray(onClickTray) {
19 | if (tray) {
20 | return tray;
21 | }
22 |
23 | /**
24 | * 托盘图标路径
25 | * 根据平台选择不同的图标处理方式
26 | * 托盘图标建议尺寸:
27 | * - Windows: 16x16 或 32x32 像素(建议32x32以适应高DPI屏幕)
28 | * - macOS: 16x16 或 18x18 像素
29 | * - Linux: 根据桌面环境有所不同,通常22x22像素
30 | */
31 | const iconPath = path.join(
32 | __dirname,
33 | '..',
34 | 'assets',
35 | 'icons',
36 | 'tray-icon.png'
37 | );
38 |
39 | let icon = nativeImage.createFromPath(iconPath);
40 |
41 | if (process.platform === 'darwin') {
42 | /**
43 | * macOS平台特定处理
44 | * 1. 调整图标大小至18x18像素以适合macOS菜单栏
45 | * 2. 设置为模板图像(templateImage)以适应深色和浅色模式
46 | * 模板图像应为单色透明PNG,macOS会自动处理颜色
47 | */
48 | const macSize = { width: 18, height: 18 };
49 | icon = icon.resize(macSize);
50 | // 设置为模板图像,让macOS自动处理明暗主题
51 | icon.setTemplateImage(true);
52 | }
53 |
54 | // 创建托盘实例
55 | tray = new Tray(icon);
56 | tray.setToolTip(i18n.t('app-name'));
57 |
58 | /**
59 | * 设置托盘点击行为
60 | * 注意: 在Linux平台上通常只响应右击显示菜单,此处点击事件在某些发行版可能无效
61 | */
62 | tray.on('click', () => {
63 | onClickTray();
64 | });
65 |
66 | // 监听语言变更事件
67 | i18n.addLanguageChangeListener((language) => {
68 | console.log(`Tray language changed to: ${language}`);
69 | tray.setToolTip(i18n.t('app-name'));
70 |
71 | // 如果有最新的项目列表,刷新托盘菜单
72 | if (lastItemsRef && lastHandlerRef) {
73 | updateTrayMenu(lastItemsRef, lastHandlerRef);
74 | }
75 | });
76 |
77 | return tray;
78 | }
79 |
80 | // 存储最近的items和handler引用,用于语言变更时刷新托盘菜单
81 | let lastItemsRef = null;
82 | let lastHandlerRef = null;
83 |
84 | /**
85 | * 更新托盘菜单
86 | * @param {Array} items 项目列表
87 | * @param {Function} handleItemAction 处理项目操作的函数
88 | */
89 | function updateTrayMenu(items, handleItemAction) {
90 | if (!tray) return;
91 |
92 | // 保存最新的引用,用于语言更新时重建菜单
93 | lastItemsRef = items;
94 | lastHandlerRef = handleItemAction;
95 |
96 | // 只显示最近的8个项目,防止菜单过长
97 | const recentItems = items.slice(0, 8).map((item) => {
98 | return {
99 | label: item.name || item.path,
100 | click: () => handleItemAction(item),
101 | };
102 | });
103 |
104 | /**
105 | * 创建托盘上下文菜单
106 | * 菜单项中的快捷键在平台间可能有差异:
107 | * - macOS: Command键使用 cmd
108 | * - Windows/Linux: Control键使用 ctrl
109 | */
110 | const contextMenu = Menu.buildFromTemplate([
111 | ...recentItems,
112 | { type: 'separator' },
113 | {
114 | label: i18n.t('tray-exit'),
115 | click: () => {
116 | // 设置标志,表示用户明确要求退出应用
117 | app.isQuitting = true;
118 | app.quit();
119 | },
120 | },
121 | ]);
122 |
123 | // 设置托盘的上下文菜单
124 | tray.setContextMenu(contextMenu);
125 | }
126 |
127 | /**
128 | * 销毁托盘图标
129 | * 在应用退出前调用,释放系统资源
130 | */
131 | function destroyTray() {
132 | if (tray) {
133 | i18n.removeLanguageChangeListener();
134 | tray.destroy();
135 | tray = null;
136 | lastItemsRef = null;
137 | lastHandlerRef = null;
138 | }
139 | }
140 |
141 | // 导出模块函数
142 | module.exports = {
143 | createTray,
144 | updateTrayMenu,
145 | destroyTray
146 | };
--------------------------------------------------------------------------------
/src/assets/locales/en-US.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings": "Settings",
3 | "appearance": "Appearance",
4 | "theme": "Theme",
5 | "system-theme": "System",
6 | "light-theme": "Light",
7 | "dark-theme": "Dark",
8 | "language": "Language",
9 | "system-language": "System",
10 | "zh-CN": "中文",
11 | "en-US": "English",
12 | "add-language": "Add Language",
13 | "shortcuts": "Shortcuts",
14 | "enable-global-shortcut": "Enable Global Shortcut",
15 | "enable-shortcut-desc": "Use keyboard shortcut to quickly open the app",
16 | "launcher-shortcut": "Launcher Shortcut",
17 | "record-shortcut": "Record",
18 | "reset": "Reset",
19 | "press-new-shortcut": "Press new shortcut key",
20 | "shortcut-input-desc": "Click 'Record' button then press key combination",
21 | "shortcut-need-modifier": "Shortcut needs to include at least one of Ctrl or Alt",
22 | "shortcut-need-modifier-macos": "Shortcut needs to include at least one of Control, Option or Command",
23 | "shortcut-taken": "This shortcut is already taken by another application",
24 | "shortcut-invalid": "Invalid shortcut",
25 | "shortcut-saved": "Shortcut saved",
26 | "shortcut-reset": "Shortcut reset to default",
27 | "enable-auto-launch": "Auto start on system boot",
28 | "enable-auto-launch-desc": "Run Launcher automatically when system starts",
29 | "auto-launch-enabled": "Auto start enabled",
30 | "auto-launch-disabled": "Auto start disabled",
31 | "auto-launch-update-failed": "Failed to update auto start settings",
32 | "data": "Data",
33 | "clear-data": "Clear All Records",
34 | "clear-data-desc": "Remove all recorded entries",
35 | "data-cleared": "All records cleared",
36 | "open-storage": "Open App Data Folder",
37 | "open-storage-desc": "View application data files in folder",
38 | "about": "About",
39 | "version": "Version",
40 | "app-desc": "A tool to quickly access frequently used files, folders, URLs, and commands",
41 | "report-issue": "Report Issue",
42 | "dev-tools-tip": "Press F12 to open Chrome Developer Tools",
43 | "confirm-clear-data": "Are you sure you want to remove all records? This action cannot be undone.",
44 | "app-name": "Launcher",
45 | "close": "Close",
46 | "cancel": "Cancel",
47 | "save": "Save",
48 | "edit": "Edit",
49 | "remove": "Remove",
50 | "open": "Open",
51 | "show-in-folder": "Show in folder",
52 | "copy-path": "Copy path",
53 | "entry-exists": "Entry already exists",
54 | "save-failed": "Save failed",
55 | "item-not-exist": "Item does not exist",
56 | "clear-failed": "Clear failed",
57 | "search": "Search...",
58 | "add-new-item": "Add new item",
59 | "item-content": "Item Content",
60 | "enter-path": "Enter path, URL or command...",
61 | "enter-item-name": "Enter item name (optional)",
62 | "file": "File",
63 | "folder": "Folder",
64 | "url": "URL",
65 | "command": "Command",
66 | "command-tip": "To execute multi-line commands, write them to a script file and execute it",
67 | "select-file": "Select file",
68 | "select-folder": "Select folder",
69 | "enter-path-required": "Please enter a path or command",
70 | "select-type-required": "Please select an item type",
71 | "select-file-failed": "Failed to select file, please try again",
72 | "select-folder-failed": "Failed to select folder, please try again",
73 | "update-failed": "Update failed, please try again",
74 | "add-failed": "Add failed, please try again",
75 | "context-open": "Open",
76 | "context-edit": "Edit",
77 | "context-remove": "Remove",
78 | "context-show-in-folder": "Show in folder",
79 | "context-copy-path": "Copy path",
80 | "context-copy-name": "Copy name",
81 | "context-copy": "Copy",
82 | "context-execute": "Execute",
83 | "tray-exit": "Exit",
84 | "empty-list": "Click + button to add new items",
85 | "cannot-get-file-path": "Cannot get file path",
86 | "item-already-exists": "Item already exists"
87 | }
--------------------------------------------------------------------------------
/src/renderer/js/ui-manager.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 共享UI管理模块
3 | * 负责处理UI相关的共享功能,包括主题和语言的管理
4 | * 避免在每个窗口脚本中重复相似代码
5 | */
6 |
7 | // 导入工具函数
8 | const { applyTheme, updatePageTexts, setupSystemThemeListener } = window.uiUtils;
9 | const i18n = window.electronAPI.i18n;
10 |
11 | /**
12 | * 显示提示消息
13 | * 统一的Toast提示实现,避免各窗口重复实现类似功能
14 | * @param {string} message 提示内容
15 | * @param {boolean} isError 是否是错误提示,默认为false
16 | * @param {number} duration 显示时长(毫秒),默认为2000毫秒
17 | */
18 | function showToast(message, isError = false, duration = 2000) {
19 | // 查找已有的toast元素,如果没有则创建一个
20 | let toast = document.getElementById("toast");
21 |
22 | // 如果toast不存在,创建一个新的toast元素
23 | if (!toast) {
24 | toast = document.createElement("div");
25 | toast.id = "toast";
26 | document.body.appendChild(toast);
27 | }
28 |
29 | // 设置toast内容和样式
30 | toast.textContent = message;
31 | toast.className = "toast";
32 |
33 | if (isError) {
34 | toast.classList.add("error-toast");
35 | }
36 |
37 | // 显示toast
38 | toast.style.display = "block";
39 | toast.style.opacity = "1";
40 |
41 | // 定时隐藏toast
42 | setTimeout(() => {
43 | toast.style.opacity = "0";
44 | // 动画完成后隐藏元素
45 | setTimeout(() => {
46 | toast.style.display = "none";
47 | }, 300); // 假设过渡动画为300ms
48 | }, duration);
49 |
50 | return toast;
51 | }
52 |
53 | /**
54 | * 初始化UI管理
55 | * @param {Object} options 配置选项
56 | * @param {string} options.containerSelector 主容器选择器,如 ".app-container" 或 ".modal"
57 | * @param {Function} options.onThemeChanged 主题变更时的回调函数 (可选)
58 | * @param {Function} options.onLanguageChanged 语言变更时的回调函数 (可选)
59 | * @param {Function} options.onUIReady UI完全准备好时的回调函数 (可选)
60 | * @param {string} options.windowType 窗口类型,用于通知主进程 (可选,默认为'main')
61 | * @returns {Object} 解绑函数对象,用于在需要时移除事件监听
62 | */
63 | async function initUI(options) {
64 | if (!options || !options.containerSelector) {
65 | console.error("Error initialiing ui manager: missing required parameters");
66 | return;
67 | }
68 |
69 | const container = document.querySelector(options.containerSelector);
70 | if (!container) {
71 | console.error(`Container element not found: ${options.containerSelector}`);
72 | return;
73 | }
74 |
75 | // 确定窗口类型
76 | const windowType = options.windowType || 'main';
77 |
78 | // 1. 应用主题设置
79 | const savedTheme = await window.electronAPI.getThemeConfig();
80 | applyTheme(savedTheme, container);
81 |
82 | // 2. 应用语言设置
83 | await updatePageTexts(i18n);
84 |
85 | // 3. 监听系统主题变化
86 | setupSystemThemeListener(container);
87 |
88 | // 4. 绑定主题变更事件
89 | const onThemeChangedHandler = (theme) => {
90 | console.log("Theme changed to:", theme);
91 | applyTheme(theme, container);
92 |
93 | // 如果提供了回调函数,则调用
94 | if (typeof options.onThemeChanged === 'function') {
95 | options.onThemeChanged(theme);
96 | }
97 | };
98 |
99 | // 5. 绑定语言变更事件
100 | const onLanguageChangedHandler = (language) => {
101 | console.log("Language changed to:", language);
102 | updatePageTexts(i18n);
103 |
104 | // 如果提供了回调函数,则调用
105 | if (typeof options.onLanguageChanged === 'function') {
106 | options.onLanguageChanged(language);
107 | }
108 | };
109 |
110 | // 6. 添加事件监听
111 | const themeCleanup = window.electronAPI.onThemeChanged(onThemeChangedHandler);
112 | const languageCleanup = window.electronAPI.onLanguageChanged(onLanguageChangedHandler);
113 |
114 | // 7. 调用UI准备完成回调(如果提供了)
115 | if (typeof options.onUIReady === 'function') {
116 | console.log("UI ready, calling onUIReady callback");
117 | // 确保UI元素都已经渲染完成
118 | setTimeout(async () => {
119 | await options.onUIReady();
120 |
121 | // 8. 通知主进程UI已准备完成,可以显示窗口了
122 | console.log(`UI initialized and ready: ${windowType}`);
123 | window.electronAPI.notifyUIReady(windowType);
124 | }, 10);
125 | } else {
126 | // 如果没有提供onUIReady回调,直接通知主进程
127 | console.log(`UI initialized: ${windowType}`);
128 | setTimeout(() => {
129 | window.electronAPI.notifyUIReady(windowType);
130 | }, 10); // 使用短暂延迟确保DOM完全渲染
131 | }
132 |
133 | // 返回解绑函数对象,用于在需要时移除事件监听
134 | return {
135 | unbindAll: () => {
136 | // 调用从事件注册时返回的清理函数
137 | if (themeCleanup && typeof themeCleanup === 'function') {
138 | themeCleanup();
139 | }
140 |
141 | if (languageCleanup && typeof languageCleanup === 'function') {
142 | languageCleanup();
143 | }
144 |
145 | console.log('UI manager event listeners cleaned up');
146 | }
147 | };
148 | }
149 |
150 | // 导出模块API
151 | window.uiManager = {
152 | initUI,
153 | showToast: showToast
154 | };
--------------------------------------------------------------------------------
/src/renderer/edit-item.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 | 项目内容
13 |
14 |
15 |
16 |
17 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
86 |
87 |
88 |
89 |
90 |
92 |
93 |
94 |
95 |
97 |
98 |
99 |
111 |
112 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
--------------------------------------------------------------------------------
/src/main/main.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 主进程入口文件
3 | * 负责应用程序的初始化、生命周期管理和核心功能协调
4 | */
5 | const { app, globalShortcut } = require('electron');
6 |
7 | // 导入拆分后的模块
8 | const windowManager = require('./window-manager');
9 | const dataStore = require('./data-store');
10 | const trayManager = require('./tray-manager');
11 | const itemHandler = require('./item-handler');
12 | const ipcHandler = require('./ipc-handler');
13 | const i18n = require('../shared/i18n');
14 | const appUtils = require('../shared/appUtils');
15 |
16 | // 只允许一个实例运行
17 | const singleInstanceLock = app.requestSingleInstanceLock();
18 | if (!singleInstanceLock) {
19 | app.quit();
20 | } else {
21 | app.on('second-instance', (event, commandLine, workingDirectory) => {
22 | // 当第二个实例试图启动时,聚焦到主窗口
23 | windowManager.showMainWindow();
24 | });
25 | }
26 |
27 | // 添加一个标志,用于区分应用是要退出还是只是关闭主窗口
28 | app.isQuitting = false;
29 |
30 | // 存储当前注册的全局快捷键
31 | let currentRegisteredShortcut = null;
32 |
33 | /**
34 | * 注册全局快捷键
35 | * 根据用户配置设置全局键盘快捷键
36 | */
37 | function registerGlobalShortcuts() {
38 | // 注销之前可能注册的快捷键
39 | unregisterGlobalShortcuts();
40 |
41 | // 加载快捷键配置
42 | const shortcutConfig = dataStore.getAppConfig().shortcut;
43 |
44 | // 如果启用了全局快捷键,则注册
45 | if (shortcutConfig.enabled && shortcutConfig.shortcut) {
46 | try {
47 | globalShortcut.register(shortcutConfig.shortcut, () => {
48 | // 修改为只能打开窗口,不再支持关闭窗口
49 | windowManager.showMainWindow();
50 | });
51 | currentRegisteredShortcut = shortcutConfig.shortcut;
52 | console.log(`Global shortcut ${shortcutConfig.shortcut} registered successfully`);
53 | } catch (error) {
54 | console.error(`Error registering global shortcut: ${error.message}`);
55 | }
56 | }
57 | }
58 |
59 | /**
60 | * 注销所有已注册的全局快捷键
61 | */
62 | function unregisterGlobalShortcuts() {
63 | if (currentRegisteredShortcut) {
64 | try {
65 | globalShortcut.unregister(currentRegisteredShortcut);
66 | currentRegisteredShortcut = null;
67 | console.log('Global shortcut unregistered');
68 | } catch (error) {
69 | console.error(`Error unregistering global shortcut: ${error.message}`);
70 | }
71 | }
72 | }
73 |
74 | /**
75 | * 更新托盘菜单
76 | * 在数据变化时调用此函数更新托盘菜单
77 | * 显示最近的项目并允许用户快速访问
78 | */
79 | function updateTrayMenuWithItems() {
80 | trayManager.updateTrayMenu(dataStore.getItems(), itemHandler.handleItemAction);
81 | }
82 |
83 | /**
84 | * 初始化应用语言设置
85 | * 获取配置文件中存储的语言设置或系统语言,并应用
86 | */
87 | function initializeLanguage() {
88 | // 从配置文件中获取语言设置
89 | const appConfig = dataStore.getAppConfig();
90 | let selectedLanguage;
91 |
92 | // 根据配置决定使用哪种语言
93 | if (appConfig.language && appConfig.language !== "system") {
94 | // 使用用户配置的语言
95 | selectedLanguage = appConfig.language;
96 | console.log(`Using language from config: ${selectedLanguage}`);
97 | } else {
98 | // 配置为"system"或未设置,则使用系统语言
99 | selectedLanguage = i18n.getSystemLanguage();
100 | console.log(`Using system language: ${selectedLanguage}`);
101 | }
102 |
103 | // 设置为全局语言变量,以便在创建新窗口时使用
104 | global.appLanguage = selectedLanguage;
105 |
106 | // 初始化i18n模块
107 | i18n.setLanguage(selectedLanguage);
108 |
109 | console.log(`Application language initialized to: ${selectedLanguage}`);
110 | }
111 |
112 | /**
113 | * 初始化应用主题设置
114 | * 获取配置文件中存储的主题设置,并应用到全局变量
115 | */
116 | function initializeTheme() {
117 | // 从配置文件中获取主题设置
118 | const appConfig = dataStore.getAppConfig();
119 | const theme = appConfig.theme || "system";
120 |
121 | // 设置为全局主题变量,以便在创建新窗口时使用
122 | global.appTheme = theme;
123 |
124 | console.log(`Application theme initialized to: ${theme}`);
125 | }
126 |
127 | /**
128 | * 初始化自启动设置
129 | */
130 | function initializeAutoLaunch() {
131 | const appConfig = dataStore.getAppConfig();
132 | const autoLaunchEnabled = appConfig.autoLaunch?.enabled || false;
133 | appUtils.updateAutoLaunchSettings(autoLaunchEnabled);
134 | }
135 |
136 | // 应用初始化 - 当Electron完成初始化并准备创建浏览器窗口时触发
137 | app.whenReady().then(() => {
138 | // 首先加载应用配置
139 | dataStore.loadItems();
140 | dataStore.loadAppConfig();
141 |
142 | // 初始化应用语言和主题
143 | initializeLanguage();
144 | initializeTheme();
145 |
146 | // 初始化自启动设置
147 | initializeAutoLaunch();
148 |
149 | // 创建主窗口
150 | const isAutoLaunch = process.argv.includes('--autostart');
151 | windowManager.createMainWindow(!isAutoLaunch); // 开机自启时,静默启动到托盘
152 |
153 | // 创建系统托盘图标
154 | trayManager.createTray(windowManager.showMainWindow);
155 | updateTrayMenuWithItems();
156 |
157 | // 注册全局快捷键
158 | registerGlobalShortcuts();
159 |
160 | // 设置IPC通信处理器 - 用于主进程和渲染进程间通信
161 | ipcHandler.setupIpcHandlers();
162 |
163 | // 在数据存储中添加更新回调,确保数据变化时托盘菜单同步更新
164 | dataStore.addChangeListener(updateTrayMenuWithItems);
165 |
166 | // 添加快捷键配置变化监听器
167 | dataStore.addShortcutChangeListener(config => {
168 | registerGlobalShortcuts();
169 | });
170 | });
171 |
172 | // 当所有窗口关闭时
173 | app.on('window-all-closed', () => {
174 | // 在macOS上,关闭所有窗口通常不会退出应用程序
175 | // 除非用户使用Cmd+Q显式退出
176 | if (process.platform !== 'darwin') {
177 | app.quit();
178 | }
179 | });
180 |
181 | // macOS平台特定 - 点击dock图标时
182 | app.on('activate', () => {
183 | // 在macOS上,当点击dock图标且没有其他窗口打开时,
184 | // 通常在应用程序中重新创建一个窗口
185 | if (!windowManager.getMainWindow()) {
186 | windowManager.createMainWindow();
187 | }
188 | });
189 |
190 | // 应用退出前清理资源
191 | app.on('will-quit', () => {
192 | // 注销所有快捷键
193 | unregisterGlobalShortcuts();
194 | globalShortcut.unregisterAll();
195 | // 销毁托盘图标
196 | trayManager.destroyTray();
197 | });
198 |
--------------------------------------------------------------------------------
/src/renderer/settings.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 | 设置
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
25 |
26 |
27 |
28 |
29 |
外观
30 |
31 |
32 |
33 |
38 |
39 |
40 |
41 |
42 |
46 |
47 |
48 |
49 |
50 |
51 |
快捷键
52 |
53 |
54 |
55 |
56 |
57 |
使用快捷键打开 Launcher
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
点击"记录"按钮然后按下组合键
67 |
68 |
69 |
70 |
71 |
72 |
73 |
系统启动时自动运行 Launcher
74 |
75 |
76 |
77 |
78 |
79 |
数据
80 |
81 |
82 |
83 |
移除所有已记录的项目
84 |
85 |
86 |
87 |
88 |
在文件夹中查看应用数据文件
89 |
90 |
91 |
92 |
93 |
94 |
关于
95 |
96 |
97 |
🚀
98 |
Launcher
99 |
版本 1.0.0
100 |
记录和快速访问常用文件、文件夹、URL和指令的工具。
101 |
102 |
106 |
按F12打开Chrome开发者工具
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
--------------------------------------------------------------------------------
/.github/copilot-instructions.md:
--------------------------------------------------------------------------------
1 | # GitHub Copilot 指导文件
2 |
3 | ## 项目概述
4 |
5 | Launcher App 是一个基于 Electron 的启动器应用程序,用于管理用户常用的文件、文件夹、URL 和指令。
6 | 该应用程序包含项目列表界面(主界面)、项目编辑界面、项目右键菜单、设置界面、托盘图标、托盘右键菜单等。
7 | 支持多平台(Windows, macOS, Linux)、多语言(中文、英文)和主题切换(浅色、深色)。
8 |
9 | ## 主要功能
10 |
11 | 1. **项目管理**
12 | - 添加、编辑和移除项目(文件、文件夹、URL、命令)
13 | - 拖放添加文件/文件夹
14 | - 项目排序与搜索
15 | - 右键菜单操作(打开、复制、在文件夹中显示等)
16 |
17 | 2. **用户界面**
18 | - 主窗口(项目列表)
19 | - 项目编辑窗口
20 | - 设置窗口
21 | - 系统托盘集成
22 | - 键盘导航与快捷键
23 | - 统一的Toast提示组件
24 |
25 | 3. **系统集成**
26 | - 全局快捷键(Alt+Shift+Q 呼出应用)
27 | - 系统托盘菜单(显示最近8个项目)
28 | - 跨平台终端命令执行
29 | - 系统文件操作(在文件夹中显示、复制文件)
30 |
31 | 4. **设置与配置**
32 | - 主题设置(浅色/深色/跟随系统)
33 | - 语言设置(中文/英文/跟随系统)
34 | - 数据管理(清空数据、查看数据存储位置)
35 |
36 | ## 项目结构
37 |
38 | ```
39 | launcher-app-electron/
40 | ├── main.js # 应用入口
41 | ├── package.json # 项目配置
42 | ├── src/
43 | │ ├── assets/ # 静态资源
44 | │ │ ├── icons/ # 应用图标
45 | │ │ └── locales/ # 语言文件
46 | │ ├── main/ # 主进程代码
47 | │ │ ├── data-store.js # 数据存储
48 | │ │ ├── ipc-handler.js # IPC通信
49 | │ │ ├── item-handler.js # 项目处理
50 | │ │ ├── main.js # 主进程入口
51 | │ │ ├── tray-manager.js # 托盘管理
52 | │ │ └── window-manager.js # 窗口管理
53 | │ ├── preload/ # 预加载脚本
54 | │ │ └── preload.js # 预加载入口
55 | │ ├── renderer/ # 渲染进程代码
56 | │ │ ├── css/ # 样式文件
57 | │ │ ├── js/ # 渲染进程脚本
58 | │ │ ├── edit-item.html # 编辑项目窗口
59 | │ │ ├── index.html # 主窗口
60 | │ │ └── settings.html # 设置窗口
61 | │ └── shared/ # 共享代码
62 | │ ├── defines.js # 共享定义
63 | │ └── i18n.js # 国际化
64 | └── ...
65 | ```
66 |
67 | ## 关键模块说明
68 |
69 | ### 主进程模块
70 |
71 | 1. **window-manager.js**
72 | - 负责创建和管理所有窗口(主窗口、项目编辑窗口、设置窗口)
73 | - 处理窗口生命周期、位置和大小记忆、平台特定窗口样式
74 |
75 | 2. **tray-manager.js**
76 | - 创建和管理系统托盘图标
77 | - 根据平台差异处理托盘图标和菜单
78 | - 显示最近使用的项目列表
79 |
80 | 3. **data-store.js**
81 | - 负责数据持久化存储
82 | - 提供项目CRUD操作
83 | - 存储窗口配置
84 |
85 | 4. **item-handler.js**
86 | - 处理项目类型判断
87 | - 执行项目操作(打开文件/文件夹/URL、执行命令)
88 | - 处理平台特定的终端命令执行
89 |
90 | 5. **ipc-handler.js**
91 | - 配置主进程与渲染进程间的通信
92 | - 处理窗口控制、数据操作、项目操作等IPC消息
93 |
94 | ### 渲染进程模块
95 |
96 | 1. **renderer.js**
97 | - 主窗口逻辑
98 | - 列表渲染和交互
99 | - 拖放处理
100 | - 键盘导航
101 | - 拖拽排序功能
102 |
103 | 2. **edit-item.js**
104 | - 项目编辑表单处理
105 | - 自动类型判断
106 | - 文件/文件夹选择对话框
107 |
108 | 3. **settings.js**
109 | - 主题和语言设置
110 | - 数据管理
111 | - 应用信息显示
112 |
113 | 4. **context-menu.js**
114 | - 右键菜单生成
115 | - 针对不同项目类型生成不同菜单项
116 |
117 | 5. **ui-manager.js**
118 | - 共享 UI 管理函数
119 | - 主题应用与切换
120 | - 语言更新处理
121 | - 事件监听管理
122 | - 提供统一的Toast提示组件
123 |
124 | ### 共享模块
125 |
126 | 1. **defines.js**
127 | - 定义项目类型常量(文件、文件夹、URL、命令)
128 | - 提供 ListItem 类定义
129 |
130 | 2. **i18n.js**
131 | - 多语言支持
132 | - 语言切换
133 | - 文本翻译
134 |
135 | ## 编码规范
136 |
137 | 此项目是 Electron 和 Web 技术的示例项目,需要遵守以下规范,以便入门者阅读学习。
138 |
139 | 1. 注重代码质量和可读性,使用有意义的变量和函数名称。
140 | 2. 添加详细的注释,尤其是处理平台特定代码时。
141 | 3. 分离关注点,将不同功能模块化处理。
142 | 4. 使用异步编程处理IO操作,避免阻塞主线程。
143 | 5. 使用预加载脚本来安全地暴露API,而不是直接启用nodeIntegration。
144 | 6. 避免代码重复,将常用功能提取为模块或共享函数。
145 | 7. 保持代码的一致性,不要在项目中维护多个具有相同功能的文件。
146 | 8. 使用配置文件存储数据,不要使用localStorage。
147 | 9. 打印日志时使用英文,不需要多语言支持,避免乱码。
148 |
149 | ### 代码优化建议
150 |
151 | 1. **UI/UX 一致性**
152 | - 确保所有窗口在主题切换、语言切换时有一致的行为
153 |
154 | 2. **模块化改进**
155 | - 将重复的主题和语言处理函数抽象为共享模块
156 | - 通过预加载脚本提供共享的类型和常量定义,避免重复定义
157 |
158 | 3. **错误处理**
159 | - 添加更全面的错误捕获和用户友好的错误显示
160 | - 实现操作日志记录功能
161 |
162 | ## Electron API备注
163 |
164 | **preload**
165 |
166 | - Electron默认启用沙盒化,在 `preload.js` 只允许使用 `require` 导入Electron和Node的特定子集,不能使用 `require` 导入自定义的Javascript代码。如果需要访问自定义的JavaScript API,需要借助IPC通信。
167 |
168 | **webUtils**
169 |
170 | - 最新版Electron中,需要使用 `webUtils.getPathForFile(file)` 替代 `event.dataTransfer.files[0].path` 来获取文件路径。
171 |
172 | ## API 参考
173 |
174 | ### 主进程暴露给渲染进程的API
175 |
176 | 通过预加载脚本(`preload.js`),主进程向渲染进程暴露了以下API:
177 |
178 | 1. **窗口控制API**
179 | - `window.electronAPI.closeMainWindow()` - 关闭/隐藏窗口
180 | - `window.electronAPI.showSettingsWindow()` - 显示设置窗口
181 |
182 | 2. **项目管理API**
183 | - `window.electronAPI.getItems()` - 获取所有项目
184 | - `window.electronAPI.addItem(item)` - 添加项目
185 | - `window.electronAPI.removeItem(index)` - 移除项目
186 | - `window.electronAPI.updateItem(index, updatedItem)` - 更新项目
187 | - `window.electronAPI.updateItemsOrder(items)` - 更新项目排序
188 |
189 | 3. **项目操作API**
190 | - `window.electronAPI.openItem(item)` - 打开项目
191 | - `window.electronAPI.showItemInFolder(path)` - 在文件夹中显示
192 | - `window.electronAPI.copyText(text)` - 复制文本
193 | - `window.electronAPI.getItemType(path)` - 获取项目类型
194 |
195 | 4. **文件选择API**
196 | - `window.electronAPI.selectFile()` - 打开文件选择对话框
197 | - `window.electronAPI.selectFolder()` - 打开文件夹选择对话框
198 | - `window.electronAPI.getFileOrFolderPath(item)` - 获取拖放文件的路径
199 |
200 | 5. **设置API**
201 | - `window.electronAPI.themeChanged(theme)` - 通知主题变更
202 | - `window.electronAPI.languageChanged(language)` - 通知语言变更
203 | - `window.electronAPI.openStorageLocation()` - 打开存储位置
204 | - `window.electronAPI.clearAllItems()` - 清空所有项目
205 |
206 | 6. **事件监听API**
207 | - `window.electronAPI.onItemsUpdated(callback)` - 监听项目更新
208 | - `window.electronAPI.onThemeChanged(callback)` - 监听主题变更
209 | - `window.electronAPI.onLanguageChanged(callback)` - 监听语言变更
210 |
211 | 7. **国际化API**
212 | - `window.electronAPI.i18n.t(key, params)` - 翻译文本
213 | - `window.electronAPI.i18n.setLanguage(language)` - 设置语言
214 | - `window.electronAPI.i18n.getCurrentLanguage()` - 获取当前语言
215 | - `window.electronAPI.i18n.getSystemLanguage()` - 获取系统语言
216 | - `window.electronAPI.i18n.getAvailableLanguages()` - 获取可用语言列表
217 | - `window.electronAPI.i18n.getLanguageName()` - 获取语言名称
218 |
219 | ### 渲染进程暴露的API
220 |
221 | 除了通过预加载脚本暴露的API外,渲染进程还提供了以下内部API:
222 |
223 | 1. **UI管理器API (ui-manager.js)**
224 | - `window.uiManager.initUI(options)` - 初始化UI
225 | - `options.containerSelector` - 主容器选择器
226 | - `options.onThemeChanged` - 主题变更回调 (可选)
227 | - `options.onLanguageChanged` - 语言变更回调 (可选)
228 | - `window.uiManager.showToast(message, isError, duration)` - 显示提示消息
229 | - `message` - 提示内容
230 | - `isError` - 是否为错误提示,默认false
231 | - `duration` - 显示时长(毫秒),默认2000毫秒
232 |
233 | ## 通用操作流程
234 |
235 | 1. **添加新项目流程**
236 | - 用户点击添加按钮或拖放文件
237 | - 显示编辑窗口
238 | - 填写/确认项目信息
239 | - 保存项目到数据存储
240 | - 刷新主窗口列表
241 | - 更新托盘菜单
242 |
243 | 2. **打开项目流程**
244 | - 用户双击项目或右键菜单选择打开
245 | - 根据项目类型执行对应操作:
246 | - 文件/文件夹: 使用shell.openPath
247 | - URL: 使用shell.openExternal
248 | - 命令: 使用平台特定方法在终端执行
249 |
250 | 3. **设置主题流程**
251 | - 用户在设置窗口选择主题
252 | - 保存主题设置到本地存储
253 | - 通知所有窗口应用新主题
254 | - 根据主题应用相应CSS类
255 |
256 | 4. **设置语言流程**
257 | - 用户在设置窗口选择语言
258 | - 保存语言设置
259 | - 使用i18n模块加载对应语言资源
260 | - 更新所有窗口的文本
261 |
--------------------------------------------------------------------------------
/src/main/item-handler.js:
--------------------------------------------------------------------------------
1 | const { shell, clipboard } = require("electron");
2 | const fs = require("fs");
3 | const path = require("path");
4 | const { exec } = require("child_process");
5 | const os = require("os");
6 | const { PathType } = require("../shared/defines");
7 |
8 | /**
9 | * 处理项目动作(打开文件、文件夹、URL或执行命令)
10 | * @param {Object} item 要处理的项目对象
11 | */
12 | function handleItemAction(item) {
13 | switch (item.type) {
14 | case PathType.FILE:
15 | case PathType.FOLDER:
16 | shell.openPath(item.path);
17 | break;
18 | case PathType.URL:
19 | // URL处理逻辑
20 | let urlToOpen = item.path;
21 |
22 | // 匹配任何协议前缀 (http://, https://, mailto:, tel:, app: 等)
23 | const protocolRegex = /^[a-z][a-z0-9+.-]*:(?:\/\/)?/i;
24 | // 匹配标准域名格式 (example.com, www.example.com 等)
25 | const domainRegex = /^([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,})(:[0-9]{1,5})?(\/.*)?$/i;
26 |
27 | // 只有当URL没有协议前缀,但符合标准域名格式时才添加https://
28 | if (!protocolRegex.test(urlToOpen) && domainRegex.test(urlToOpen)) {
29 | urlToOpen = `https://${urlToOpen}`;
30 | }
31 |
32 | shell.openExternal(urlToOpen);
33 | break;
34 | case PathType.COMMAND:
35 | executeCommand(item.path);
36 | break;
37 | }
38 | }
39 |
40 | /**
41 | * 安全执行命令
42 | * @param {string} command 要执行的命令
43 | */
44 | function executeCommand(command) {
45 | // 根据平台打开终端窗口执行命令,使用引号包裹命令,防止注入
46 | const platform = process.platform;
47 |
48 | // 确保工作目录存在
49 | const tmpDir = os.tmpdir();
50 | const workDir = path.join(tmpDir, "launcher-app-temp");
51 |
52 | // 创建工作目录(如果不存在)
53 | if (!fs.existsSync(workDir)) {
54 | try {
55 | fs.mkdirSync(workDir, { recursive: true });
56 | } catch (error) {
57 | console.error("Error creating working directory:", error);
58 | }
59 | }
60 |
61 | try {
62 | if (platform === "win32") {
63 | executeCommandForWindows(command, workDir);
64 | } else if (platform === "darwin") {
65 | executeCommandForMac(command, workDir);
66 | } else {
67 | executeCommandForLinux(command, workDir);
68 | }
69 | } catch (error) {
70 | console.error("Error executing command:", error);
71 | }
72 | }
73 |
74 | /* Windows平台特定代码
75 | * 使用CMD执行命令:
76 | * 1. /K - 执行命令后保持窗口打开
77 | * 2. /C - 执行命令后关闭窗口
78 | * 3. 使用detached和unref,避免CMD窗口关闭时Launcher被关闭
79 | */
80 | function executeCommandForWindows(command, workDir) {
81 | const execOptions = {
82 | windowsHide: false,
83 | detached: true,
84 | shell: true,
85 | };
86 |
87 | if (/^\/[KC]/i.test(command)) {
88 | // 命令自带 /K 或 /C 参数的情况
89 |
90 | // 提取开头的 /K 或 /C 及可能跟随的空格
91 | const cmdPrefix = command.match(/^\/[KC]\s*/i)[0];
92 | // 获取实际的命令部分
93 | const actualCommand = command.substring(cmdPrefix.length);
94 |
95 | // 构建新命令: start cmd /K(或/C) "cd /d 工作目录 && 实际命令"
96 | const newCommand = `start cmd ${cmdPrefix}"cd /d "${workDir}" && ${actualCommand}"`;
97 |
98 | const child = exec(newCommand, execOptions);
99 | child.unref();
100 | } else {
101 | // 默认使用 /K 模式保持窗口打开
102 | const child = exec(`start cmd /K "cd /d "${workDir}" && ${command}"`, execOptions);
103 | child.unref();
104 | }
105 | }
106 |
107 | /**
108 | * macOS平台执行命令
109 | * 使用AppleScript在Terminal中执行命令
110 | * 转义引号以防止命令注入攻击
111 | */
112 | function executeCommandForMac(command, workDir) {
113 | // 更安全的转义处理
114 | // 首先转义工作目录路径中的特殊字符
115 | const escapedWorkDir = workDir
116 | .replace(/\\/g, '\\\\') // 转义反斜杠
117 | .replace(/"/g, '\\"') // 转义双引号
118 | .replace(/'/g, "'\\''"); // 转义单引号
119 |
120 | // 然后转义命令中的特殊字符
121 | const escapedCommand = command
122 | .replace(/\\/g, '\\\\') // 转义反斜杠
123 | .replace(/"/g, '\\"') // 转义双引号
124 | .replace(/'/g, "'\\''"); // 转义单引号
125 |
126 | // 构建完整的AppleScript命令,确保命令能在Terminal中正确执行
127 | exec(
128 | `osascript -e 'tell app "Terminal" to do script "cd \\"${escapedWorkDir}\\" && ${escapedCommand}"'`
129 | );
130 | }
131 |
132 | /**
133 | * Linux平台执行命令
134 | * Linux有多种不同的终端模拟器,需要尝试多种终端
135 | */
136 | function executeCommandForLinux(command, workDir) {
137 | const terminals = [
138 | 'gnome-terminal -- bash -c "cd \\"{WORKDIR}\\" && {CMD}; exec bash"', // GNOME桌面环境
139 | 'konsole --noclose -e bash -c "cd \\"{WORKDIR}\\" && {CMD}"', // KDE桌面环境
140 | 'xterm -hold -e bash -c "cd \\"{WORKDIR}\\" && {CMD}"', // 通用X终端
141 | 'x-terminal-emulator -e bash -c "cd \\"{WORKDIR}\\" && {CMD}; exec bash"', // Debian/Ubuntu默认终端
142 | ];
143 |
144 | // 安全处理命令,转义引号以防止命令注入
145 | const escapedCommand = command
146 | .replace(/\\/g, '\\\\') // 转义反斜杠
147 | .replace(/"/g, '\\"') // 转义双引号
148 | .replace(/'/g, "'\\''"); // 转义单引号
149 |
150 | const escapedWorkDir = workDir
151 | .replace(/\\/g, '\\\\') // 转义反斜杠
152 | .replace(/"/g, '\\"') // 转义双引号
153 | .replace(/'/g, "'\\''"); // 转义单引号
154 |
155 | // 尝试所有可能的终端,直到一个成功
156 | tryNextLinuxTerminal(terminals, 0, escapedCommand, escapedWorkDir);
157 | }
158 |
159 | /**
160 | * 递归尝试不同的Linux终端
161 | * @param {Array} terminals 终端命令列表
162 | * @param {number} index 当前尝试的索引
163 | * @param {string} command 要执行的命令
164 | * @param {string} workDir 工作目录路径
165 | */
166 | function tryNextLinuxTerminal(terminals, index, command, workDir = "") {
167 | if (index >= terminals.length) {
168 | console.error("No available terminal found");
169 | return;
170 | }
171 |
172 | // 替换命令模板中的{CMD}和{WORKDIR}为实际值
173 | let terminalCmd = terminals[index].replace("{CMD}", command);
174 | if (workDir) {
175 | terminalCmd = terminalCmd.replace("{WORKDIR}", workDir);
176 | }
177 |
178 | exec(terminalCmd, (error) => {
179 | if (error) {
180 | console.warn(`Terminal ${index + 1}/${terminals.length} failed, trying next one...`);
181 | // 递归尝试下一个终端
182 | tryNextLinuxTerminal(terminals, index + 1, command, workDir);
183 | }
184 | });
185 | }
186 |
187 | /**
188 | * 在文件夹中显示项目
189 | * @param {string} path 文件路径
190 | */
191 | function showItemInFolder(path) {
192 | shell.showItemInFolder(path);
193 | }
194 |
195 | /**
196 | * 复制文本到剪贴板
197 | * @param {string} text 要复制的文本
198 | */
199 | function copyText(text) {
200 | clipboard.writeText(text);
201 | }
202 |
203 | /**
204 | * 判断项目类型(文件、文件夹、URL或命令)
205 | * @param {string} path 路径或URL
206 | * @returns {string} 项目类型
207 | */
208 | function getItemType(path) {
209 | if (!path || typeof path !== "string") {
210 | return undefined;
211 | }
212 |
213 | // 判断是否是文件或文件夹
214 | const fileExists = fs.existsSync(path);
215 | if (fileExists) {
216 | const stats = fs.statSync(path);
217 | if (stats.isDirectory()) {
218 | return PathType.FOLDER;
219 | }
220 |
221 | if (stats.isFile()) {
222 | return PathType.FILE;
223 | }
224 |
225 | // 其他类型的文件(如socket等)暂不处理
226 | return undefined;
227 | }
228 |
229 | // 判断是否是标准协议URL和自定义协议deep link
230 | // 匹配如http://, https://, ftp://, app://, myapp://等协议格式
231 | const protocolRegex = /^[a-z][a-z0-9+.-]*:\/\//i;
232 | // 匹配标准域名格式 (包括www开头和不带www的域名)
233 | const domainRegex =
234 | /^([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,})(:[0-9]{1,5})?(\/.*)?$/i;
235 | const isUrl = protocolRegex.test(path) || domainRegex.test(path);
236 | if (isUrl) {
237 | return PathType.URL;
238 | }
239 |
240 | // 不是URL也不是文件/文件夹,则认为是命令
241 | return PathType.COMMAND;
242 | }
243 |
244 | // 导出模块函数
245 | module.exports = {
246 | handleItemAction,
247 | showItemInFolder,
248 | copyText,
249 | getItemType,
250 | };
251 |
--------------------------------------------------------------------------------
/src/renderer/js/context-menu.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 上下文菜单脚本
3 | * 负责在主窗口中创建和管理右键菜单
4 | */
5 | document.addEventListener("DOMContentLoaded", () => {
6 | // 引用i18n模块,用于本地化菜单项目
7 | const i18n = window.electronAPI.i18n;
8 |
9 | // 上下文菜单DOM元素
10 | let contextMenu = null;
11 |
12 | /**
13 | * 创建上下文菜单
14 | * 根据不同项目类型创建对应的菜单
15 | * @param {MouseEvent} e 鼠标事件
16 | * @param {HTMLElement} target 触发菜单的目标元素
17 | */
18 | async function createContextMenu(e, target) {
19 | // 防止默认事件和事件冒泡
20 | e.preventDefault();
21 | e.stopPropagation();
22 |
23 | // 移除可能存在的旧菜单
24 | removeContextMenu();
25 |
26 | // 确保目标是列表项
27 | if (!target.classList.contains("list-item")) {
28 | target = target.closest(".list-item");
29 | }
30 |
31 | if (!target) return;
32 |
33 | // 获取项目数据
34 | const index = parseInt(target.dataset.index);
35 | const currentItems = await window.electronAPI.getItems();
36 | const item = currentItems[index];
37 |
38 | if (!item) return;
39 |
40 | // 创建上下文菜单元素
41 | contextMenu = document.createElement("div");
42 | contextMenu.className = "context-menu";
43 |
44 | // 设置菜单位置
45 | contextMenu.style.left = `${e.pageX}px`;
46 | contextMenu.style.top = `${e.pageY}px`;
47 |
48 | // 应用当前主题到右键菜单
49 | const appContainer = document.querySelector(".app-container");
50 | if (appContainer && appContainer.classList.contains("dark-theme")) {
51 | contextMenu.classList.add("dark-theme");
52 | } else if (appContainer && appContainer.classList.contains("light-theme")) {
53 | contextMenu.classList.add("light-theme");
54 | }
55 |
56 | // 创建常用菜单项目
57 | const menuItems = await createMenuItems(item, index);
58 |
59 | // 将菜单项添加到菜单
60 | menuItems.forEach(menuItem => {
61 | contextMenu.appendChild(menuItem);
62 | });
63 |
64 | // 添加菜单到DOM
65 | document.body.appendChild(contextMenu);
66 |
67 | // 确保菜单不超出视窗
68 | adjustMenuPosition(contextMenu);
69 |
70 | // 高亮显示选中的项目
71 | document.querySelectorAll(".list-item.active").forEach(el => {
72 | el.classList.remove("active");
73 | });
74 | target.classList.add("active");
75 |
76 | // 添加点击事件监听器,点击外部关闭菜单
77 | setTimeout(() => {
78 | document.addEventListener("click", handleDocumentClick);
79 | }, 0);
80 | }
81 |
82 | /**
83 | * 根据项目类型创建对应的菜单项
84 | * @param {Object} item 项目对象
85 | * @param {number} index 项目索引
86 | * @returns {HTMLElement[]} 菜单项元素数组
87 | */
88 | async function createMenuItems(item, index) {
89 | const menuItems = [];
90 |
91 | // 打开项目
92 | const openItem = document.createElement("div");
93 | openItem.className = "menu-item";
94 | openItem.textContent = await i18n.t("open");
95 | openItem.addEventListener("click", () => {
96 | window.electronAPI.openItem(item);
97 | removeContextMenu(); // 点击后关闭菜单
98 | });
99 | menuItems.push(openItem);
100 |
101 | // 根据项目类型添加特定菜单项
102 | if (item.type === "file" || item.type === "folder") {
103 | // 在文件夹中显示
104 | const showInFolder = document.createElement("div");
105 | showInFolder.className = "menu-item";
106 | showInFolder.textContent = await i18n.t("show-in-folder");
107 | showInFolder.addEventListener("click", () => {
108 | window.electronAPI.showItemInFolder(item.path);
109 | removeContextMenu(); // 点击后关闭菜单
110 | });
111 | menuItems.push(showInFolder);
112 | }
113 |
114 | // 复制路径
115 | const copyPath = document.createElement("div");
116 | copyPath.className = "menu-item";
117 | copyPath.textContent = await i18n.t("copy-path");
118 | copyPath.addEventListener("click", () => {
119 | window.electronAPI.copyText(item.path);
120 | // 显示成功提示
121 | if (window.appFunctions && window.appFunctions.showToast) {
122 | window.appFunctions.showToast(i18n.t("path-copied"));
123 | }
124 | removeContextMenu(); // 点击后关闭菜单
125 | });
126 | menuItems.push(copyPath);
127 |
128 | // 分隔线
129 | const divider = document.createElement("div");
130 | divider.className = "menu-divider";
131 | menuItems.push(divider);
132 |
133 | // 编辑项目
134 | const editItem = document.createElement("div");
135 | editItem.className = "menu-item";
136 | editItem.textContent = await i18n.t("edit");
137 | editItem.addEventListener("click", () => {
138 | window.electronAPI.showEditItemDialog(item, index);
139 | removeContextMenu(); // 点击后关闭菜单
140 | });
141 | menuItems.push(editItem);
142 |
143 | // 删除项目
144 | const removeItem = document.createElement("div");
145 | removeItem.className = "menu-item";
146 | removeItem.textContent = await i18n.t("remove");
147 | removeItem.addEventListener("click", () => {
148 | if (window.appFunctions && window.appFunctions.removeItem) {
149 | window.appFunctions.removeItem(index);
150 | }
151 | removeContextMenu(); // 点击后关闭菜单
152 | });
153 | menuItems.push(removeItem);
154 |
155 | return menuItems;
156 | }
157 |
158 | /**
159 | * 调整菜单位置,确保不超出视窗
160 | * @param {HTMLElement} menu 菜单元素
161 | */
162 | function adjustMenuPosition(menu) {
163 | const rect = menu.getBoundingClientRect();
164 | const windowWidth = window.innerWidth;
165 | const windowHeight = window.innerHeight;
166 |
167 | // 检查右侧是否超出视窗
168 | if (rect.right > windowWidth) {
169 | menu.style.left = `${windowWidth - rect.width - 5}px`;
170 | }
171 |
172 | // 检查底部是否超出视窗
173 | if (rect.bottom > windowHeight) {
174 | menu.style.top = `${windowHeight - rect.height - 5}px`;
175 | }
176 | }
177 |
178 | /**
179 | * 处理点击事件,关闭上下文菜单
180 | */
181 | function handleDocumentClick(e) {
182 | if (contextMenu && !contextMenu.contains(e.target)) {
183 | removeContextMenu();
184 | }
185 | }
186 |
187 | /**
188 | * 移除上下文菜单
189 | * 从文档中移除现有的上下文菜单
190 | */
191 | function removeContextMenu() {
192 | if (contextMenu) {
193 | document.body.removeChild(contextMenu);
194 | contextMenu = null;
195 | document.removeEventListener("click", handleDocumentClick);
196 | }
197 | }
198 |
199 | /**
200 | * 设置列表项的右键菜单
201 | * 为列表容器添加右键菜单事件监听
202 | */
203 | function setupContextMenu() {
204 | const listContainer = document.getElementById("list-container");
205 |
206 | if (listContainer) {
207 | listContainer.addEventListener("contextmenu", (e) => {
208 | // 查找点击的列表项
209 | const target = e.target.closest(".list-item");
210 | if (target) {
211 | createContextMenu(e, target);
212 | }
213 | });
214 | }
215 | }
216 |
217 | // 设置右键菜单
218 | setupContextMenu();
219 | });
--------------------------------------------------------------------------------
/src/renderer/js/edit-item.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 项目编辑窗口脚本
3 | * 负责处理项目的添加和编辑功能
4 | * 包括表单验证、文件选择和保存操作
5 | */
6 | document.addEventListener("DOMContentLoaded", async () => {
7 | // DOM元素引用
8 | const itemPathInput = document.getElementById("item-path");
9 | const itemNameInput = document.getElementById("item-name");
10 | const itemTypeSelect = document.getElementById("item-type");
11 | const saveBtn = document.getElementById("save-btn");
12 | const cancelBtn = document.getElementById("cancel-btn");
13 | const selectFileBtn = document.getElementById("select-file-btn");
14 | const selectFolderBtn = document.getElementById("select-folder-btn");
15 | const commandTip = document.getElementById("command-tip");
16 |
17 | // 导入i18n模块、PathType常量
18 | const i18n = window.electronAPI.i18n;
19 | const PathType = window.defines.PathType;
20 |
21 | // 初始化UI,保存返回的解绑函数对象
22 | const uiCleanup = window.uiManager.initUI({
23 | containerSelector: ".modal",
24 | windowType: "edit-item" // 指定窗口类型为项目编辑窗口
25 | });
26 |
27 | // 当页面卸载时清理监听器
28 | window.addEventListener('beforeunload', () => {
29 | if (uiCleanup && typeof uiCleanup.unbindAll === 'function') {
30 | uiCleanup.unbindAll();
31 | }
32 | });
33 |
34 | // 跟踪是否处于编辑模式
35 | let isEditMode = false;
36 | let editingItemIndex = -1;
37 |
38 | // 初始化页面
39 | initPage();
40 |
41 | /**
42 | * 初始化页面
43 | * 应用主题和语言设置,设置事件监听器
44 | */
45 | async function initPage() {
46 | // 监听编辑项目数据事件,注意要在其他异步操作之前注册监听器,避免错过事件窗口
47 | window.electronAPI.onEditItemData(({ item, index }) => {
48 | console.log("Editing item data:", item, index);
49 | // 进入编辑模式
50 | isEditMode = true;
51 | editingItemIndex = index;
52 |
53 | // 填充表单数据
54 | itemPathInput.value = item.path;
55 | itemTypeSelect.value = item.type;
56 | // 如果有名称,填充名称字段
57 | if (item.name) {
58 | itemNameInput.value = item.name;
59 | }
60 |
61 | updateCommandTipVisibility();
62 |
63 | // 启用保存按钮
64 | saveBtn.disabled = false;
65 |
66 | // 让路径输入框获得焦点
67 | setTimeout(() => itemPathInput.focus(), 100);
68 | });
69 |
70 | // 默认情况下(新增模式),让路径输入框获得焦点
71 | setTimeout(() => itemPathInput.focus(), 100);
72 |
73 | // 根据当前选择的类型控制提示信息的显示/隐藏
74 | updateCommandTipVisibility();
75 | }
76 |
77 | /**
78 | * 更新命令提示信息的可见性
79 | * 仅当类型为"指令"时显示提示
80 | */
81 | function updateCommandTipVisibility() {
82 | commandTip.style.display = itemTypeSelect.value === PathType.COMMAND ? "block" : "none";
83 | }
84 |
85 | /**
86 | * 项目类型选择变更事件
87 | * 控制命令提示信息的显示/隐藏
88 | */
89 | itemTypeSelect.addEventListener("change", () => {
90 | updateCommandTipVisibility();
91 | });
92 |
93 | /**
94 | * 阻止在路径输入框中输入换行符
95 | * 当用户按下Enter键时,阻止默认行为
96 | */
97 | itemPathInput.addEventListener("keydown", (e) => {
98 | if (e.key === "Enter" && !e.ctrlKey && !e.metaKey) {
99 | e.preventDefault();
100 | }
101 | });
102 |
103 | /**
104 | * 确保粘贴到路径输入框的内容不包含换行符
105 | */
106 | itemPathInput.addEventListener("paste", (e) => {
107 | // 阻止默认粘贴行为
108 | e.preventDefault();
109 |
110 | // 获取剪贴板数据
111 | let pasteData = (e.clipboardData || window.clipboardData).getData("text");
112 |
113 | // 移除所有换行符
114 | if (pasteData) {
115 |
116 | // 替换所有换行符(\n, \r, \r\n)为空格
117 | pasteData = pasteData.replace(/[\r\n]+/g, " ");
118 |
119 | // 在当前光标位置插入处理后的文本
120 | const selectionStart = itemPathInput.selectionStart;
121 | const selectionEnd = itemPathInput.selectionEnd;
122 | const currentValue = itemPathInput.value;
123 |
124 | itemPathInput.value = currentValue.substring(0, selectionStart) +
125 | pasteData +
126 | currentValue.substring(selectionEnd);
127 |
128 | // 更新光标位置
129 | itemPathInput.selectionStart = itemPathInput.selectionEnd =
130 | selectionStart + pasteData.length;
131 |
132 | // 手动触发input事件以更新验证
133 | itemPathInput.dispatchEvent(new Event("input"));
134 | }
135 | });
136 |
137 | /**
138 | * 文件选择按钮点击事件
139 | * 使用系统对话框选择文件
140 | */
141 | selectFileBtn.addEventListener("click", async () => {
142 | try {
143 | const result = await window.electronAPI.selectFile();
144 | if (!result.canceled) {
145 | itemPathInput.value = result.filePath;
146 | // 自动设置类型为文件
147 | itemTypeSelect.value = PathType.FILE;
148 | // 更新命令提示可见性
149 | updateCommandTipVisibility();
150 | saveBtn.disabled = false;
151 | }
152 | } catch (error) {
153 | console.error("Error selecting file:", error);
154 | const errorMessage = await i18n.t("select-file-failed");
155 | window.uiManager.showToast(errorMessage, true);
156 | }
157 | });
158 |
159 | /**
160 | * 文件夹选择按钮点击事件
161 | * 使用系统对话框选择文件夹
162 | */
163 | selectFolderBtn.addEventListener("click", async () => {
164 | try {
165 | const result = await window.electronAPI.selectFolder();
166 | if (!result.canceled) {
167 | itemPathInput.value = result.filePath;
168 | // 自动设置类型为文件夹
169 | itemTypeSelect.value = PathType.FOLDER;
170 | // 更新命令提示可见性
171 | updateCommandTipVisibility();
172 | saveBtn.disabled = false;
173 | }
174 | } catch (error) {
175 | console.error("Error selecting folder:", error);
176 | const errorMessage = await i18n.t("select-folder-failed");
177 | window.uiManager.showToast(errorMessage, true);
178 | }
179 | });
180 |
181 | /**
182 | * 路径输入变化事件处理
183 | * 自动推断项目类型,启用/禁用保存按钮
184 | */
185 | itemPathInput.addEventListener("input", async () => {
186 | const path = itemPathInput.value.trim();
187 | // 获取路径对应的项目类型
188 | const type = await window.electronAPI.getItemType(path);
189 |
190 | if (path && type) {
191 | // 有效路径,启用添加按钮
192 | saveBtn.disabled = false;
193 | // 自动设置类型
194 | itemTypeSelect.value = type;
195 | // 更新命令提示可见性
196 | updateCommandTipVisibility();
197 | } else {
198 | // 无效路径,禁用添加按钮
199 | saveBtn.disabled = true;
200 | }
201 | });
202 |
203 | /**
204 | * 保存按钮点击事件处理
205 | * 添加新项目或更新现有项目
206 | */
207 | saveBtn.addEventListener("click", async () => {
208 | // 获取表单数据
209 | const path = itemPathInput.value.trim();
210 | const type = itemTypeSelect.value;
211 | const name = itemNameInput.value.trim();
212 |
213 | // 表单验证
214 | if (!path) {
215 | const errorMessage = await i18n.t("enter-path-required");
216 | window.uiManager.showToast(errorMessage, true);
217 | return;
218 | }
219 |
220 | if (!type) {
221 | const errorMessage = await i18n.t("select-type-required");
222 | window.uiManager.showToast(errorMessage, true);
223 | return;
224 | }
225 |
226 | // 创建项目对象
227 | const newItem = {
228 | type: type,
229 | path: path,
230 | };
231 |
232 | // 如果用户提供了名称,则添加到项目中
233 | if (name) {
234 | newItem.name = name;
235 | }
236 |
237 | try {
238 | let result;
239 |
240 | // 根据模式决定是更新还是添加
241 | if (isEditMode) {
242 | // 编辑现有项目
243 | result = await window.electronAPI.updateItem(editingItemIndex, newItem);
244 | } else {
245 | // 添加新项目
246 | result = await window.electronAPI.addItem(newItem);
247 | }
248 |
249 | // 处理操作结果
250 | if (result.success) {
251 | // 保存成功,关闭窗口
252 | window.electronAPI.closeAddItemWindow();
253 | } else {
254 | // 显示错误提示
255 | window.uiManager.showToast(result.message, true);
256 | }
257 | } catch (error) {
258 | console.error(`Error ${isEditMode ? "updating" : "adding"} item:`, error);
259 | const errorMessage = await i18n.t(
260 | isEditMode ? "update-failed" : "add-failed"
261 | );
262 | window.uiManager.showToast(errorMessage, true);
263 | }
264 | });
265 |
266 | /**
267 | * 取消按钮点击事件
268 | * 关闭编辑窗口,不保存任何更改
269 | */
270 | cancelBtn.addEventListener("click", () => {
271 | window.electronAPI.closeAddItemWindow();
272 | });
273 |
274 | /**
275 | * 键盘事件处理
276 | * - Escape: 关闭窗口
277 | * - Enter+Ctrl: 提交表单
278 | * - F12: 开发者工具
279 | */
280 | document.addEventListener("keydown", (e) => {
281 | if (e.key === "Escape") {
282 | window.electronAPI.closeAddItemWindow();
283 | e.preventDefault();
284 | } else if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
285 | // 使用 Ctrl+Enter / Cmd+Enter 提交表单
286 | if (!saveBtn.disabled) {
287 | saveBtn.click();
288 | }
289 | e.preventDefault();
290 | } else if (e.key === "F12") {
291 | window.electronAPI.openDevTools();
292 | e.preventDefault();
293 | }
294 | });
295 | });
296 |
--------------------------------------------------------------------------------
/src/main/data-store.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 数据存储模块
3 | * 负责应用程序数据的持久化存储和管理
4 | * 包括项目列表和窗口配置等数据
5 | */
6 | const { app } = require("electron");
7 | const fs = require("fs");
8 | const path = require("path");
9 |
10 | /**
11 | * 数据文件路径
12 | * 使用Electron的app.getPath("userData")获取跨平台的用户数据目录
13 | * - Windows: %APPDATA%\[appname]\
14 | * - macOS: ~/Library/Application Support/[appname]/
15 | * - Linux: ~/.config/[appname]/
16 | */
17 | const userDataFolder = path.join(app.getPath("userData"), "UserData");
18 | const dataFilePath = path.join(userDataFolder, "items.json");
19 | const configFilePath = path.join(userDataFolder, "configs.json");
20 |
21 | // 全局变量存储项目列表
22 | let items = [];
23 | // 全局变量存储配置
24 | let appConfig = {
25 | /**
26 | * 主窗口配置
27 | */
28 | mainWindow: {
29 | width: 400,
30 | height: 600,
31 | x: undefined,
32 | y: undefined
33 | },
34 | /**
35 | * 主题配置
36 | */
37 | theme: "system",
38 | /**
39 | * 语言配置
40 | */
41 | language: "system",
42 | /**
43 | * 快捷键配置
44 | */
45 | shortcut: {
46 | enabled: true,
47 | shortcut: "Alt+Shift+Q"
48 | },
49 | /**
50 | * 自启动配置
51 | */
52 | autoLaunch: {
53 | enabled: false
54 | }
55 | };
56 |
57 | // 存储数据变化监听器 - 观察者模式实现
58 | const changeListeners = [];
59 | // 存储快捷键配置变化监听器
60 | const shortcutChangeListeners = [];
61 |
62 | /**
63 | * 添加数据变化监听器
64 | * @param {Function} listener 监听函数
65 | */
66 | function addChangeListener(listener) {
67 | if (typeof listener === 'function' && !changeListeners.includes(listener)) {
68 | changeListeners.push(listener);
69 | }
70 | }
71 |
72 | /**
73 | * 移除数据变化监听器
74 | * @param {Function} listener 要移除的监听函数
75 | */
76 | function removeChangeListener(listener) {
77 | const index = changeListeners.indexOf(listener);
78 | if (index !== -1) {
79 | changeListeners.splice(index, 1);
80 | }
81 | }
82 |
83 | /**
84 | * 通知所有监听器数据已变化
85 | */
86 | function notifyChangeListeners() {
87 | for (const listener of changeListeners) {
88 | try {
89 | listener();
90 | } catch (error) {
91 | console.error("Error executing listener:", error);
92 | }
93 | }
94 | }
95 |
96 | /**
97 | * 加载保存的项目列表
98 | * @returns {Array} 项目列表
99 | */
100 | function loadItems() {
101 | try {
102 | if (fs.existsSync(dataFilePath)) {
103 | const data = fs.readFileSync(dataFilePath, "utf8");
104 | items = JSON.parse(data);
105 | return items;
106 | }
107 | return [];
108 | } catch (error) {
109 | console.error("Error loading items:", error);
110 | items = [];
111 | return [];
112 | }
113 | }
114 |
115 | /**
116 | * 保存项目列表到磁盘
117 | * @returns {boolean} 是否保存成功
118 | */
119 | function saveItems() {
120 | try {
121 | const dirPath = path.dirname(dataFilePath);
122 | if (!fs.existsSync(dirPath)) {
123 | fs.mkdirSync(dirPath, { recursive: true });
124 | }
125 | fs.writeFileSync(dataFilePath, JSON.stringify(items, null, 2), "utf8");
126 | notifyChangeListeners();
127 | return true;
128 | } catch (error) {
129 | console.error("Error saving items:", error);
130 | return false;
131 | }
132 | }
133 |
134 | /**
135 | * 添加新项目
136 | * @param {Object} item 要添加的项目
137 | * @returns {Object} 结果对象,包含成功标志和可能的错误消息
138 | */
139 | function addItem(item) {
140 | // 检查是否已存在相同路径的项目
141 | const exists = items.some((i) => i.path === item.path);
142 | if (exists) {
143 | return { success: false, message: "Item already exists" };
144 | }
145 |
146 | items.push(item);
147 | const saved = saveItems();
148 | return { success: saved, message: saved ? "" : "Save failed" };
149 | }
150 |
151 | /**
152 | * 更新指定索引的项目
153 | * @param {number} index 项目索引
154 | * @param {Object} updatedItem 更新后的项目
155 | * @returns {Object} 结果对象
156 | */
157 | function updateItem(index, updatedItem) {
158 | if (index < 0 || index >= items.length) {
159 | return { success: false, message: "Item does not exist" };
160 | }
161 |
162 | items[index] = updatedItem;
163 | const saved = saveItems();
164 | return { success: saved, message: saved ? "" : "Save failed" };
165 | }
166 |
167 | /**
168 | * 移除指定索引的项目
169 | * @param {number} index 项目索引
170 | * @returns {Object} 结果对象
171 | */
172 | function removeItem(index) {
173 | if (index < 0 || index >= items.length) {
174 | return { success: false, message: "Item does not exist" };
175 | }
176 |
177 | items.splice(index, 1);
178 | const saved = saveItems();
179 | return { success: saved, message: saved ? "" : "Save failed" };
180 | }
181 |
182 | /**
183 | * 更新项目顺序
184 | * @param {Array} newItems 新的项目列表
185 | * @returns {Object} 结果对象
186 | */
187 | function updateItemsOrder(newItems) {
188 | items = newItems;
189 | const saved = saveItems();
190 | return { success: saved, message: saved ? "" : "Save failed" };
191 | }
192 |
193 | /**
194 | * 获取当前项目列表
195 | * @returns {Array} 项目列表
196 | */
197 | function getItems() {
198 | return items;
199 | }
200 |
201 | /**
202 | * 清除所有项目
203 | * @returns {Object} 结果对象
204 | */
205 | function clearAllItems() {
206 | items = [];
207 | const saved = saveItems();
208 | return { success: saved, message: saved ? "" : "Clear failed" };
209 | }
210 |
211 | /**
212 | * 获取存储文件路径
213 | * @returns {string} 存储文件的绝对路径
214 | */
215 | function getStoragePath() {
216 | return dataFilePath;
217 | }
218 |
219 | /**
220 | * 获取应用数据文件夹路径
221 | * @returns {string} 应用数据文件夹的绝对路径
222 | */
223 | function getUserDataPath() {
224 | return app.getPath("userData");
225 | }
226 |
227 | /**
228 | * 获取App配置
229 | * @returns {Object} App配置对象
230 | */
231 | function getAppConfig() {
232 | return appConfig;
233 | }
234 |
235 | /**
236 | * 加载App配置
237 | * @returns {Object} App配置
238 | */
239 | function loadAppConfig() {
240 | try {
241 | if (fs.existsSync(configFilePath)) {
242 | const config = fs.readFileSync(configFilePath, "utf8");
243 | appConfig = JSON.parse(config);
244 | return appConfig;
245 | }
246 | return appConfig;
247 | } catch (error) {
248 | console.error("Error loading window config:", error);
249 | return appConfig;
250 | }
251 | }
252 |
253 | /**
254 | * 保存App配置到磁盘
255 | * @param {Object} config 要保存的App配置
256 | * @returns {boolean} 是否保存成功
257 | */
258 | function saveAppConfig(config = null) {
259 | if (config) {
260 | appConfig = config;
261 | }
262 |
263 | try {
264 | const dirPath = path.dirname(configFilePath);
265 | if (!fs.existsSync(dirPath)) {
266 | fs.mkdirSync(dirPath, { recursive: true });
267 | }
268 | fs.writeFileSync(configFilePath, JSON.stringify(appConfig, null, 2), "utf8");
269 | return true;
270 | } catch (error) {
271 | console.error("Error saving window config:", error);
272 | return false;
273 | }
274 | }
275 |
276 | /**
277 | * 更新主题配置
278 | */
279 | function updateThemeConfig(theme) {
280 | appConfig.theme = theme;
281 | return saveAppConfig();
282 | }
283 |
284 | /**
285 | * 更新语言配置
286 | */
287 | function updateLanguageConfig(language) {
288 | appConfig.language = language;
289 | return saveAppConfig();
290 | }
291 |
292 | /**
293 | * 更新主窗口配置
294 | * @param {Object} bounds 窗口的边界配置 {x, y, width, height}
295 | * @returns {boolean} 是否保存成功
296 | */
297 | function updateMainWindowConfig(bounds) {
298 | appConfig.mainWindow = { ...appConfig.mainWindow, ...bounds };
299 | return saveAppConfig();
300 | }
301 |
302 | /**
303 | * 更新快捷键配置
304 | * @param {Object} config 新的快捷键配置
305 | * @returns {boolean} 是否保存成功
306 | */
307 | function updateShortcutConfig(config) {
308 | appConfig.shortcut = { ...appConfig.shortcut, ...config };
309 | const result = saveAppConfig();
310 | if (result) {
311 | notifyShortcutChangeListeners();
312 | }
313 | return result;
314 | }
315 |
316 | /**
317 | * 更新自启动配置
318 | * @param {Object} config 新的自启动配置
319 | * @returns {boolean} 是否保存成功
320 | */
321 | function updateAutoLaunchConfig(config) {
322 | appConfig.autoLaunch = { ...appConfig.autoLaunch, ...config };
323 | return saveAppConfig();
324 | }
325 |
326 | /**
327 | * 添加快捷键配置变化监听器
328 | * @param {Function} listener 监听函数
329 | */
330 | function addShortcutChangeListener(listener) {
331 | if (typeof listener === 'function' && !shortcutChangeListeners.includes(listener)) {
332 | shortcutChangeListeners.push(listener);
333 | }
334 | }
335 |
336 | /**
337 | * 移除快捷键配置变化监听器
338 | * @param {Function} listener 要移除的监听函数
339 | */
340 | function removeShortcutChangeListener(listener) {
341 | const index = shortcutChangeListeners.indexOf(listener);
342 | if (index !== -1) {
343 | shortcutChangeListeners.splice(index, 1);
344 | }
345 | }
346 |
347 | /**
348 | * 通知所有快捷键配置变化监听器
349 | */
350 | function notifyShortcutChangeListeners() {
351 | for (const listener of shortcutChangeListeners) {
352 | try {
353 | listener(appConfig.shortcut);
354 | } catch (error) {
355 | console.error("Error executing shortcut listener:", error);
356 | }
357 | }
358 | }
359 |
360 | // 导出模块函数
361 | module.exports = {
362 | // App配置
363 | loadAppConfig,
364 | saveAppConfig,
365 | getAppConfig,
366 | getStoragePath,
367 | getUserDataPath,
368 |
369 | // 语言配置
370 | updateLanguageConfig,
371 |
372 | // 主题配置
373 | updateThemeConfig,
374 |
375 | // 主窗口配置
376 | updateMainWindowConfig,
377 |
378 | // 快捷键配置
379 | updateShortcutConfig,
380 | addShortcutChangeListener,
381 | removeShortcutChangeListener,
382 |
383 | // 自启动配置
384 | updateAutoLaunchConfig,
385 |
386 | // 列表项目
387 | getItems,
388 | loadItems,
389 | saveItems,
390 | addItem,
391 | updateItem,
392 | removeItem,
393 | updateItemsOrder,
394 | clearAllItems,
395 |
396 | // 监听器
397 | addChangeListener,
398 | removeChangeListener,
399 | };
400 |
--------------------------------------------------------------------------------
/src/main/ipc-handler.js:
--------------------------------------------------------------------------------
1 | /**
2 | * IPC通信处理模块
3 | * 负责处理主进程和渲染进程之间的所有通信
4 | * 所有渲染进程通过预加载脚本暴露的API与主进程通信
5 | */
6 | const { ipcMain, dialog, app } = require('electron');
7 |
8 | // 导入其他模块
9 | const windowManager = require('./window-manager');
10 | const dataStore = require('./data-store');
11 | const itemHandler = require('./item-handler');
12 | const i18n = require('../shared/i18n');
13 | const appUtils = require('../shared/appUtils');
14 |
15 | /**
16 | * 设置所有 IPC 通信处理器
17 | * IPC通信类型分为两种:
18 | * 1. on/send - 单向通信,不等待回复
19 | * 2. handle/invoke - 请求/响应模式,返回Promise
20 | */
21 | function setupIpcHandlers() {
22 | /**
23 | * 窗口控制相关IPC处理
24 | */
25 |
26 | ipcMain.on('close-main-window', () => {
27 | windowManager.hideMainWindow();
28 | });
29 |
30 | ipcMain.on('close-add-item-window', () => {
31 | windowManager.closeAddItemWindow();
32 | });
33 |
34 | ipcMain.on('close-settings-window', () => {
35 | windowManager.closeSettingsWindow();
36 | });
37 |
38 | ipcMain.on('show-settings-window', () => {
39 | windowManager.createSettingsWindow();
40 | });
41 |
42 | /**
43 | * UI就绪通知处理
44 | * 当渲染进程完成UI初始化(主题和语言应用完成)后,通知主进程显示窗口
45 | */
46 | ipcMain.on('ui-ready', (event, windowType) => {
47 | if (windowType === 'main') {
48 | windowManager.showMainWindowWhenReady();
49 | } else if (windowType === 'settings') {
50 | windowManager.showSettingsWindowWhenReady();
51 | } else if (windowType === 'edit-item') {
52 | windowManager.showAddItemWindowWhenReady();
53 | }
54 | });
55 |
56 | /**
57 | * 项目数据管理相关IPC处理
58 | * 处理项目的获取、添加、更新、移除和重排序
59 | */
60 | ipcMain.handle('get-items', () => {
61 | return dataStore.getItems();
62 | });
63 |
64 | ipcMain.handle('add-item', (event, item) => {
65 | const result = dataStore.addItem(item);
66 | if (result.success) {
67 | // 获取新添加项目的索引(在数组末尾)
68 | const itemIndex = dataStore.getItems().length - 1;
69 |
70 | // 通知所有窗口数据已更新,同时传递新添加的项目索引
71 | windowManager.notifyItemsUpdated(itemIndex);
72 |
73 | // 返回结果时包含新项目的索引
74 | return { ...result, itemIndex };
75 | }
76 | return result;
77 | });
78 |
79 | ipcMain.handle('update-item', (event, { index, updatedItem }) => {
80 | const result = dataStore.updateItem(index, updatedItem);
81 | if (result.success) {
82 | windowManager.notifyItemsUpdated();
83 | }
84 | return result;
85 | });
86 |
87 | ipcMain.handle('remove-item', (event, index) => {
88 | const result = dataStore.removeItem(index);
89 | if (result.success) {
90 | windowManager.notifyItemsUpdated();
91 | }
92 | return result;
93 | });
94 |
95 | ipcMain.handle('update-items-order', (event, newItems) => {
96 | const result = dataStore.updateItemsOrder(newItems);
97 | return result;
98 | });
99 |
100 | /**
101 | * 项目编辑窗口相关IPC处理
102 | * 处理添加和编辑项目的对话框
103 | */
104 | ipcMain.on('show-add-item-dialog', () => {
105 | windowManager.createAddItemWindow();
106 | });
107 |
108 | ipcMain.on('show-edit-item-dialog', (event, { item, index }) => {
109 | windowManager.createEditItemWindow(item, index);
110 | });
111 |
112 | /**
113 | * 项目类型判断
114 | * 根据路径确定项目类型(文件、文件夹、URL或命令)
115 | */
116 | ipcMain.handle('get-item-type', (event, path) => {
117 | return itemHandler.getItemType(path);
118 | });
119 |
120 | /**
121 | * 项目操作相关IPC处理
122 | * 处理打开项目、在文件夹中显示、复制等操作
123 | */
124 | ipcMain.on('open-item', (event, item) => {
125 | itemHandler.handleItemAction(item);
126 | });
127 |
128 | ipcMain.on('show-item-in-folder', (event, path) => {
129 | itemHandler.showItemInFolder(path);
130 | });
131 |
132 | ipcMain.on('copy-text', (event, text) => {
133 | itemHandler.copyText(text);
134 | });
135 |
136 | /**
137 | * 文件和文件夹选择对话框
138 | * 使用系统原生对话框选择文件或文件夹
139 | */
140 | ipcMain.handle('select-file', async () => {
141 | const addItemWindow = windowManager.getAddItemWindow();
142 | if (!addItemWindow) return { canceled: true };
143 |
144 | const { canceled, filePaths } = await dialog.showOpenDialog(addItemWindow, {
145 | properties: ['openFile']
146 | });
147 |
148 | if (canceled || !filePaths || filePaths.length === 0) {
149 | return { canceled: true };
150 | }
151 |
152 | return {
153 | canceled: false,
154 | filePath: filePaths[0],
155 | };
156 | });
157 |
158 | ipcMain.handle('select-folder', async () => {
159 | const addItemWindow = windowManager.getAddItemWindow();
160 | if (!addItemWindow) return { canceled: true };
161 |
162 | const { canceled, filePaths } = await dialog.showOpenDialog(addItemWindow, {
163 | properties: ['openDirectory']
164 | });
165 |
166 | if (canceled || !filePaths || filePaths.length === 0) {
167 | return { canceled: true };
168 | }
169 |
170 | return {
171 | canceled: false,
172 | filePath: filePaths[0],
173 | };
174 | });
175 |
176 | /**
177 | * 主题相关IPC处理
178 | */
179 | ipcMain.handle('get-theme-config', () => {
180 | return dataStore.getAppConfig().theme;
181 | });
182 |
183 | ipcMain.on('theme-changed', (event, theme) => {
184 | // 保存主题配置到配置文件
185 | dataStore.updateThemeConfig(theme);
186 |
187 | // 获取主窗口并发送主题变更通知
188 | const mainWindow = windowManager.getMainWindow();
189 | if (mainWindow && !mainWindow.isDestroyed()) {
190 | mainWindow.webContents.send('theme-changed', theme);
191 | }
192 |
193 | // 通知编辑窗口(如果存在)
194 | const editItemWindow = windowManager.getAddItemWindow();
195 | if (editItemWindow && !editItemWindow.isDestroyed()) {
196 | editItemWindow.webContents.send('theme-changed', theme);
197 | }
198 |
199 | // 存储主题设置到全局变量,以便在创建新窗口时使用
200 | global.appTheme = theme;
201 | });
202 |
203 | /**
204 | * 语言相关IPC处理
205 | */
206 | ipcMain.handle('get-language-config', () => {
207 | return dataStore.getAppConfig().language;
208 | });
209 |
210 | ipcMain.on('language-changed', (event, language) => {
211 | // 保存语言配置到配置文件
212 | dataStore.updateLanguageConfig(language);
213 |
214 | // 获取所有窗口并发送语言变更通知
215 | const mainWindow = windowManager.getMainWindow();
216 | if (mainWindow && !mainWindow.isDestroyed()) {
217 | mainWindow.webContents.send('language-changed', language);
218 | }
219 |
220 | // 通知编辑窗口(如果存在)
221 | const editItemWindow = windowManager.getAddItemWindow();
222 | if (editItemWindow && !editItemWindow.isDestroyed()) {
223 | editItemWindow.webContents.send('language-changed', language);
224 | }
225 |
226 | // 存储语言设置到全局变量,以便在创建新窗口时使用
227 | global.appLanguage = language;
228 | });
229 |
230 | /**
231 | * 设置相关 IPC 处理
232 | * 处理打开存储位置、清除所有项目等操作
233 | */
234 | ipcMain.on('open-storage-location', () => {
235 | const userDataPath = dataStore.getUserDataPath();
236 | if (userDataPath) {
237 | require('electron').shell.openPath(userDataPath);
238 | }
239 | });
240 |
241 | ipcMain.on('clear-all-items', () => {
242 | dataStore.clearAllItems();
243 | windowManager.notifyItemsUpdated();
244 | });
245 |
246 | /**
247 | * 开发者工具相关IPC处理
248 | */
249 | ipcMain.on('open-devtools', (event) => {
250 | // 检查事件来源是哪个窗口
251 | const webContents = event.sender;
252 | webContents.openDevTools({ mode: 'detach' });
253 | });
254 |
255 | /**
256 | * 外部链接相关IPC处理
257 | */
258 | ipcMain.on('open-external-link', (event, url) => {
259 | require('electron').shell.openExternal(url);
260 | });
261 |
262 | /**
263 | * 应用信息相关IPC处理
264 | */
265 | ipcMain.handle('get-app-info', () => {
266 | const { app } = require('electron');
267 | return {
268 | version: app.getVersion(),
269 | name: app.getName(),
270 | electronVersion: process.versions.electron,
271 | };
272 | });
273 |
274 | /**
275 | * 国际化(i18n)相关IPC处理
276 | * 在主进程中处理所有i18n功能调用
277 | */
278 |
279 | // 翻译文本
280 | ipcMain.handle('i18n-translate', (event, { key, params }) => {
281 | return i18n.t(key, params);
282 | });
283 |
284 | // 设置语言
285 | ipcMain.handle('i18n-set-language', (event, language) => {
286 | return i18n.setLanguage(language);
287 | });
288 |
289 | // 获取当前语言
290 | ipcMain.handle('i18n-get-current-language', () => {
291 | return i18n.getCurrentLanguage();
292 | });
293 |
294 | // 获取系统语言
295 | ipcMain.handle('i18n-get-system-language', () => {
296 | return i18n.getSystemLanguage();
297 | });
298 |
299 | // 获取可用语言列表
300 | ipcMain.handle('i18n-get-available-languages', () => {
301 | return i18n.getAvailableLanguages();
302 | });
303 |
304 | // 获取语言名称
305 | ipcMain.handle('i18n-get-language-name', (event, langCode) => {
306 | return i18n.getLanguageName(langCode);
307 | });
308 |
309 | /**
310 | * 快捷键配置相关IPC处理
311 | * 获取和更新快捷键配置
312 | */
313 | ipcMain.handle('get-shortcut-config', () => {
314 | return dataStore.getAppConfig().shortcut;
315 | });
316 |
317 | ipcMain.handle('update-shortcut-config', (event, config) => {
318 | return dataStore.updateShortcutConfig(config);
319 | });
320 |
321 | ipcMain.handle('test-shortcut', (event, shortcut) => {
322 | try {
323 | // 尝试注册快捷键,如果成功就立即注销
324 | const success = require('electron').globalShortcut.register(shortcut, () => { });
325 | if (success) {
326 | require('electron').globalShortcut.unregister(shortcut);
327 | return { success: true };
328 | }
329 | return { success: false, message: i18n.t('shortcut-taken') };
330 | } catch (error) {
331 | return { success: false, message: `${i18n.t('shortcut-invalid')}: ${error.message}` };
332 | }
333 | });
334 |
335 | /**
336 | * 获取和更新自启动配置
337 | */
338 | ipcMain.handle('get-auto-launch-config', () => {
339 | return dataStore.getAppConfig().autoLaunch;
340 | });
341 |
342 | ipcMain.handle('update-auto-launch-config', (event, config) => {
343 | // 更新自启动设置
344 | const result = appUtils.updateAutoLaunchSettings(config.enabled);
345 | if (result) {
346 | dataStore.updateAutoLaunchConfig(config);
347 | }
348 |
349 | return result;
350 | });
351 | }
352 |
353 | // 导出模块函数
354 | module.exports = {
355 | setupIpcHandlers
356 | };
--------------------------------------------------------------------------------
/src/preload/preload.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 预加载脚本
3 | * 在渲染进程加载前执行,安全地暴露特定API给渲染进程
4 | * 作为渲染进程和主进程之间的桥梁,确保渲染进程不直接访问Node.js API
5 | */
6 | const { contextBridge, ipcRenderer, webUtils } = require("electron");
7 |
8 | /**
9 | * 保持与defines.js中的PathType一致
10 | */
11 | const PathType = {
12 | FILE: "file", // 文件类型
13 | FOLDER: "folder", // 文件夹类型
14 | URL: "url", // 网址类型
15 | COMMAND: "command", // 命令类型
16 | };
17 |
18 | /**
19 | * 共享常量
20 | */
21 | contextBridge.exposeInMainWorld("defines", {
22 | PathType: PathType,
23 | });
24 |
25 | /**
26 | * 通过contextBridge安全地暴露API给渲染进程
27 | * 所有渲染进程的JavaScript可以通过window.electronAPI访问这些函数
28 | */
29 | contextBridge.exposeInMainWorld("electronAPI", {
30 | /**
31 | * 窗口控制相关API
32 | * 允许渲染进程控制应用窗口
33 | */
34 | closeMainWindow: () => ipcRenderer.send("close-main-window"),
35 | closeAddItemWindow: () => ipcRenderer.send("close-add-item-window"),
36 | closeSettingsWindow: () => ipcRenderer.send("close-settings-window"),
37 | showSettingsWindow: () => ipcRenderer.send("show-settings-window"),
38 | notifyUIReady: (windowType) => ipcRenderer.send("ui-ready", windowType),
39 |
40 | /**
41 | * 主题相关API
42 | */
43 | themeChanged: (theme) => ipcRenderer.send("theme-changed", theme),
44 | getThemeConfig: () => ipcRenderer.invoke("get-theme-config"),
45 |
46 | /**
47 | * 语言相关API
48 | */
49 | languageChanged: (language) => ipcRenderer.send("language-changed", language),
50 | getLanguageConfig: () => ipcRenderer.invoke("get-language-config"),
51 |
52 | /**
53 | * 事件监听相关API
54 | * 允许渲染进程注册对主进程事件的监听
55 | */
56 | // 监听列表更新事件
57 | onItemsUpdated: (callback) => {
58 | const listener = (event, newItemIndex) => callback(newItemIndex);
59 | ipcRenderer.on("items-updated", listener);
60 |
61 | // 返回清理函数,用于移除事件监听,防止内存泄漏
62 | return () => {
63 | ipcRenderer.removeListener("items-updated", listener);
64 | };
65 | },
66 |
67 | // 监听编辑项目数据
68 | onEditItemData: (callback) => {
69 | const listener = (event, data) => callback(data);
70 | ipcRenderer.on("edit-item-data", listener);
71 | // 返回清理函数
72 | return () => {
73 | ipcRenderer.removeListener("edit-item-data", listener);
74 | };
75 | },
76 |
77 | // 监听主题变更
78 | onThemeChanged: (callback) => {
79 | const listener = (event, theme) => callback(theme);
80 | ipcRenderer.on("theme-changed", listener);
81 | return () => {
82 | ipcRenderer.removeListener("theme-changed", listener);
83 | };
84 | },
85 |
86 | // 监听语言变更
87 | onLanguageChanged: (callback) => {
88 | const listener = (event, language) => callback(language);
89 | ipcRenderer.on("language-changed", listener);
90 | return () => {
91 | ipcRenderer.removeListener("language-changed", listener);
92 | };
93 | },
94 |
95 | /**
96 | * 项目管理相关API
97 | * 允许渲染进程获取、添加、更新和移除项目
98 | */
99 | getItems: () => ipcRenderer.invoke("get-items"),
100 | addItem: (item) => ipcRenderer.invoke("add-item", item),
101 | removeItem: (index) => ipcRenderer.invoke("remove-item", index),
102 | updateItemsOrder: (items) => ipcRenderer.invoke("update-items-order", items),
103 | showAddItemDialog: () => ipcRenderer.send("show-add-item-dialog"),
104 | showEditItemDialog: (item, index) =>
105 | ipcRenderer.send("show-edit-item-dialog", { item, index }),
106 |
107 | /**
108 | * 项目类型判断API
109 | * 根据路径判断项目类型(文件、文件夹、URL或命令)
110 | */
111 | getItemType: (path) => ipcRenderer.invoke("get-item-type", path),
112 |
113 | /**
114 | * 项目操作相关API
115 | * 允许渲染进程对项目执行各种操作
116 | */
117 | updateItem: (index, updatedItem) =>
118 | ipcRenderer.invoke("update-item", { index, updatedItem }),
119 | openItem: (item) => ipcRenderer.send("open-item", item),
120 | showItemInFolder: (path) => ipcRenderer.send("show-item-in-folder", path),
121 | copyText: (text) => ipcRenderer.send("copy-text", text),
122 |
123 | /**
124 | * 文件路径获取API
125 | * 使用webUtils.getPathForFile获取文件路径
126 | * 注意:最新版Electron中,无法使用event.dataTransfer.files[0].path
127 | * 必须使用webUtils.getPathForFile来安全获取文件路径
128 | */
129 | getFileOrFolderPath: (item) => {
130 | if (!item) return undefined;
131 | return webUtils.getPathForFile(item);
132 | },
133 |
134 | /**
135 | * 文件和文件夹选择对话框API
136 | * 允许渲染进程调用原生文件选择对话框
137 | */
138 | selectFile: () => ipcRenderer.invoke("select-file"),
139 | selectFolder: () => ipcRenderer.invoke("select-folder"),
140 |
141 | /**
142 | * 快捷键配置相关API
143 | * 获取和更新快捷键配置
144 | */
145 | getShortcutConfig: () => ipcRenderer.invoke("get-shortcut-config"),
146 | updateShortcutConfig: (config) => ipcRenderer.invoke("update-shortcut-config", config),
147 | testShortcut: (shortcut) => ipcRenderer.invoke("test-shortcut", shortcut),
148 |
149 | /**
150 | * 自启动配置相关API
151 | * 获取和更新自启动配置
152 | */
153 | getAutoLaunchConfig: () => ipcRenderer.invoke("get-auto-launch-config"),
154 | updateAutoLaunchConfig: (config) => ipcRenderer.invoke("update-auto-launch-config", config),
155 |
156 | /**
157 | * 平台信息API
158 | * 获取当前运行平台信息(win32、darwin、linux)
159 | */
160 | getPlatform: () => process.platform,
161 |
162 | /**
163 | * 设置相关API
164 | * 提供各种设置和实用功能
165 | */
166 | openStorageLocation: () => ipcRenderer.send("open-storage-location"),
167 | clearAllItems: () => ipcRenderer.send("clear-all-items"),
168 | openDevTools: () => ipcRenderer.send("open-devtools"),
169 | openExternalLink: (url) => ipcRenderer.send("open-external-link", url),
170 | getAppInfo: () => ipcRenderer.invoke("get-app-info"),
171 |
172 | /**
173 | * 国际化(i18n)API
174 | * 与主进程通信获取多语言支持功能
175 | */
176 | i18n: {
177 | t: (key, params = null) => ipcRenderer.invoke("i18n-translate", { key, params }),
178 | setLanguage: (language) => ipcRenderer.invoke("i18n-set-language", language),
179 | getCurrentLanguage: () => ipcRenderer.invoke("i18n-get-current-language"),
180 | getSystemLanguage: () => ipcRenderer.invoke("i18n-get-system-language"),
181 | getAvailableLanguages: () => ipcRenderer.invoke("i18n-get-available-languages"),
182 | getLanguageName: (langCode) => ipcRenderer.invoke("i18n-get-language-name", langCode),
183 | addLanguageChangeListener: (callback) => {
184 | const listener = (event, language) => callback(language);
185 | ipcRenderer.on("language-changed", listener);
186 | return () => {
187 | ipcRenderer.removeListener("language-changed", listener);
188 | };
189 | }
190 | },
191 | });
192 |
193 | /**
194 | * 暴露UI工具函数给渲染进程
195 | * 提供共享的UI功能,如主题应用和语言处理
196 | */
197 | contextBridge.exposeInMainWorld("uiUtils", {
198 | /**
199 | * 应用主题设置
200 | * @param {string} theme 主题类型:"system", "light", "dark"
201 | * @param {HTMLElement} container 要应用主题的容器元素
202 | */
203 | applyTheme: (theme, container) => {
204 | // 移除所有主题类
205 | container.classList.remove("dark-theme", "light-theme");
206 |
207 | // 根据主题类型应用相应的CSS类
208 | if (theme === "system") {
209 | // 使用内部函数处理系统主题
210 | internalApplySystemTheme(container);
211 | } else if (theme === "dark") {
212 | container.classList.add("dark-theme");
213 | } else if (theme === "light") {
214 | container.classList.add("light-theme");
215 | }
216 | },
217 |
218 | /**
219 | * 应用系统主题
220 | * 根据系统深色/浅色模式设置相应的主题
221 | * @param {HTMLElement} container 要应用主题的容器元素
222 | */
223 | applySystemTheme: (container) => {
224 | internalApplySystemTheme(container);
225 | },
226 |
227 | /**
228 | * 更新页面文本
229 | * 根据当前语言设置更新所有带有特定属性的元素文本
230 | * @param {Object} i18n 国际化模块实例
231 | */
232 | updatePageTexts: async (i18n) => {
233 | try {
234 | // 更新普通文本元素
235 | const elements = document.querySelectorAll('[data-i18n]');
236 | for (const el of elements) {
237 | const key = el.getAttribute('data-i18n');
238 | el.textContent = await i18n.t(key);
239 | }
240 |
241 | // 更新带有 title 属性的元素
242 | const titleElements = document.querySelectorAll('[data-i18n-title]');
243 | for (const el of titleElements) {
244 | const key = el.getAttribute('data-i18n-title');
245 | el.title = await i18n.t(key);
246 | }
247 |
248 | // 更新带有 placeholder 属性的元素
249 | const placeholderElements = document.querySelectorAll('[data-i18n-placeholder]');
250 | for (const el of placeholderElements) {
251 | const key = el.getAttribute('data-i18n-placeholder');
252 | el.placeholder = await i18n.t(key);
253 | }
254 |
255 | // 更新select元素的选项文本
256 | const selects = document.querySelectorAll("select");
257 | for (const select of selects) {
258 | const options = Array.from(select.options);
259 | for (const option of options) {
260 | if (option.hasAttribute("data-i18n")) {
261 | const key = option.getAttribute("data-i18n");
262 | option.textContent = await i18n.t(key);
263 | }
264 | }
265 | }
266 | } catch (error) {
267 | console.error("Error updating page texts:", error);
268 | }
269 | },
270 |
271 | /**
272 | * 监听系统主题变化
273 | * 当系统主题变化时,如果应用设置为跟随系统,则自动更新主题
274 | * @param {HTMLElement} container 要应用主题的容器元素
275 | */
276 | setupSystemThemeListener: (container) => {
277 | if (window.matchMedia) {
278 | window
279 | .matchMedia("(prefers-color-scheme: dark)")
280 | .addEventListener("change", async () => {
281 | // 获取当前主题配置
282 | const themeConfig = await ipcRenderer.invoke("get-theme-config");
283 | if (themeConfig === "system") {
284 | // 调用内部函数
285 | internalApplySystemTheme(container);
286 | }
287 | });
288 | }
289 | },
290 | });
291 |
292 | /**
293 | * 内部辅助函数,应用系统主题
294 | * 抽取为内部函数以避免重复代码
295 | * @param {HTMLElement} container 要应用主题的容器元素
296 | */
297 | function internalApplySystemTheme(container) {
298 | // 检测系统是否处于深色模式
299 | const isDarkMode =
300 | window.matchMedia &&
301 | window.matchMedia("(prefers-color-scheme: dark)").matches;
302 |
303 | // 应用相应的主题类
304 | if (isDarkMode) {
305 | container.classList.add("dark-theme");
306 | } else {
307 | container.classList.remove("dark-theme");
308 | }
309 | }
310 |
--------------------------------------------------------------------------------
/src/main/window-manager.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 窗口管理模块
3 | * 负责创建和管理应用程序中的所有窗口
4 | */
5 | const { BrowserWindow, app, nativeTheme } = require('electron');
6 | const path = require('path');
7 | const dataStore = require('./data-store');
8 |
9 | /**
10 | * titleBarOverlay: 在Windows上自定义标题栏外观
11 | * 配合titleBarStyle: 'hidden'使用
12 | */
13 | const titleBarOverlay = {
14 | height: 30,
15 | color: 'rgba(0, 0, 0, 0)',
16 | symbolColor: 'white',
17 | };
18 |
19 | // 全局窗口引用 - 防止垃圾回收导致窗口被关闭
20 | let mainWindow = null;
21 | let addItemWindow = null;
22 | let settingsWindow = null; // 添加设置窗口引用
23 |
24 | /**
25 | * 根据当前主题获取适当的窗口背景色
26 | * @returns {string} 适合当前主题的背景色
27 | */
28 | function getThemeBackgroundColor() {
29 | const theme = global.appTheme || 'system';
30 |
31 | if (theme === 'light') {
32 | return '#f5f5f5'; // 浅色主题背景色
33 | } else if (theme === 'dark') {
34 | return '#1e1e1e'; // 深色主题背景色
35 | } else {
36 | // 跟随系统
37 | return nativeTheme.shouldUseDarkColors ? '#1e1e1e' : '#f5f5f5';
38 | }
39 | }
40 |
41 | /**
42 | * 执行窗口淡出动画
43 | * 通过逐渐降低窗口透明度实现平滑淡出效果,避免在macOS上出现闪烁
44 | * @param {BrowserWindow} window 需要淡出的窗口对象
45 | * @param {Function} onComplete 动画完成后的回调函数
46 | * @param {Object} options 动画选项
47 | * @param {number} options.fadeStep 每次淡出的透明度步长,默认 0.1
48 | * @param {number} options.fadeInterval 淡出动画的时间间隔(毫秒),默认 10ms
49 | */
50 | function fadeWindowOut(window, onComplete, options = {}) {
51 | if (!window || window.isDestroyed()) {
52 | return;
53 | }
54 |
55 | // 如果不是macOS,则跳过淡出动画直接执行回调
56 | if (process.platform !== 'darwin') {
57 | onComplete();
58 | return;
59 | }
60 |
61 | // 动画参数
62 | const fadeStep = options.fadeStep || 0.1;
63 | const fadeInterval = options.fadeInterval || 10;
64 |
65 | // 确保起始透明度为1
66 | window.setOpacity(1.0);
67 |
68 | // 执行淡出动画
69 | const fade = () => {
70 | if (!window || window.isDestroyed()) return;
71 |
72 | let opacity = window.getOpacity() - fadeStep;
73 | if (opacity > 0) {
74 | window.setOpacity(opacity);
75 | setTimeout(fade, fadeInterval);
76 | } else {
77 | // 完全透明后执行回调
78 | window.setOpacity(0);
79 | onComplete();
80 | }
81 | };
82 |
83 | // 开始淡出动画
84 | fade();
85 | }
86 |
87 | /**
88 | * 创建主窗口
89 | * @returns {BrowserWindow} 创建的主窗口对象
90 | */
91 | function createMainWindow(showOnReady = true) {
92 | // 如果主窗口已存在,则返回现有窗口
93 | if (mainWindow && !mainWindow.isDestroyed()) {
94 | return mainWindow;
95 | }
96 |
97 | // 加载保存的窗口配置
98 | const appConfig = dataStore.loadAppConfig();
99 | const mainWindowConfig = appConfig.mainWindow;
100 |
101 | /**
102 | * 设置窗口选项
103 | * 注意:某些配置是平台特定的,如titleBarStyle和titleBarOverlay
104 | */
105 | const windowOptions = {
106 | width: mainWindowConfig.width,
107 | height: mainWindowConfig.height,
108 | minWidth: 300,
109 | minHeight: 300,
110 | maximizable: false,
111 | minimizable: false,
112 | fullscreenable: false,
113 | /**
114 | * titleBarStyle: 在macOS上使用自定义标题栏
115 | * 'hidden' - 隐藏标题栏,内容延伸到整个窗口,窗口控制按钮可见
116 | * Windows平台通过设置frame: false实现
117 | */
118 | titleBarStyle: 'hidden',
119 | titleBarOverlay: titleBarOverlay,
120 | show: false, // 先创建隐藏窗口,等UI准备好后再显示,避免闪烁
121 | frame: false, // 无框窗口
122 | backgroundColor: getThemeBackgroundColor(), // 添加暗色背景,避免白色闪烁
123 | webPreferences: {
124 | preload: path.join(__dirname, '..', 'preload', 'preload.js'),
125 | },
126 | };
127 |
128 | // 如果存在保存的位置坐标,则使用它们
129 | if (mainWindowConfig.x !== undefined && mainWindowConfig.y !== undefined) {
130 | windowOptions.x = mainWindowConfig.x;
131 | windowOptions.y = mainWindowConfig.y;
132 | }
133 |
134 | // 创建主窗口实例
135 | mainWindow = new BrowserWindow(windowOptions);
136 |
137 | // 加载渲染进程的HTML文件
138 | mainWindow.loadFile(path.join(__dirname, '..', 'renderer', 'index.html'));
139 |
140 | // 打开开发者工具
141 | // mainWindow.webContents.openDevTools();
142 |
143 | // 注意:不再在这里显示窗口,而是等待渲染进程通知UI准备完成
144 | // UI准备完成后会调用 showMainWindowWhenReady 函数
145 |
146 | // 不在任务栏显示图标
147 | // mainWindow.setSkipTaskbar(true);
148 |
149 | // 保存窗口调整大小时的尺寸
150 | mainWindow.on('resize', () => {
151 | if (!mainWindow.isMaximized()) {
152 | const bounds = mainWindow.getBounds();
153 | dataStore.updateMainWindowConfig({
154 | width: bounds.width,
155 | height: bounds.height
156 | });
157 | }
158 | });
159 |
160 | // 保存窗口移动时的位置
161 | mainWindow.on('move', () => {
162 | if (!mainWindow.isMaximized()) {
163 | const bounds = mainWindow.getBounds();
164 | dataStore.updateMainWindowConfig({
165 | x: bounds.x,
166 | y: bounds.y
167 | });
168 | }
169 | });
170 |
171 | // 拦截关闭事件,在Windows上点击关闭按钮时隐藏窗口而非退出应用
172 | mainWindow.on('close', (event) => {
173 | // 如果不是真正要退出应用程序(如app.quit()或app.exit())
174 | // 且不在macOS上(macOS有自己的窗口管理行为)
175 | if (!app.isQuitting && process.platform !== 'darwin') {
176 | event.preventDefault(); // 阻止默认关闭行为
177 | mainWindow.hide(); // 隐藏窗口
178 | }
179 | });
180 |
181 | // 窗口关闭时清除引用,避免内存泄漏
182 | mainWindow.on('closed', () => {
183 | mainWindow = null;
184 | });
185 |
186 | return mainWindow;
187 | }
188 |
189 | /**
190 | * 创建添加项目窗口
191 | * @returns {BrowserWindow} 创建的添加项目窗口对象
192 | */
193 | function createAddItemWindow() {
194 | // 如果窗口已存在则聚焦并返回
195 | if (addItemWindow && !addItemWindow.isDestroyed()) {
196 | addItemWindow.focus();
197 | return addItemWindow;
198 | }
199 |
200 | addItemWindow = new BrowserWindow({
201 | width: 400,
202 | height: 370,
203 | frame: false,
204 | modal: true,
205 | show: false,
206 | parent: mainWindow,
207 | resizable: false,
208 | maximizable: false,
209 | minimizable: false,
210 | fullscreenable: false,
211 | titleBarStyle: 'hidden',
212 | titleBarOverlay: titleBarOverlay,
213 | backgroundColor: getThemeBackgroundColor(), // 添加背景色,避免白色闪烁
214 | webPreferences: {
215 | preload: path.join(__dirname, '..', 'preload', 'preload.js'),
216 | },
217 | });
218 |
219 | addItemWindow.loadFile(path.join(__dirname, '..', 'renderer', 'edit-item.html'));
220 |
221 | // 注意:不再在ready-to-show事件中显示窗口,而是等待渲染进程通知UI准备完成
222 |
223 | addItemWindow.on('closed', () => {
224 | addItemWindow = null;
225 | });
226 |
227 | return addItemWindow;
228 | }
229 |
230 | /**
231 | * 创建编辑项目窗口
232 | * @param {Object} item 要编辑的项目
233 | * @param {number} index 项目索引
234 | * @returns {BrowserWindow} 创建的编辑窗口对象
235 | */
236 | function createEditItemWindow(item, index) {
237 | // 使用相同的窗口处理编辑功能
238 | const window = createAddItemWindow();
239 |
240 | // 窗口准备好后发送编辑数据
241 | window.once('ready-to-show', () => {
242 | window.webContents.send('edit-item-data', { item, index });
243 | });
244 |
245 | return window;
246 | }
247 |
248 | /**
249 | * 创建设置窗口
250 | * @returns {BrowserWindow} 创建的设置窗口对象
251 | */
252 | function createSettingsWindow() {
253 | // 如果窗口已存在则聚焦并返回
254 | if (settingsWindow && !settingsWindow.isDestroyed()) {
255 | settingsWindow.focus();
256 | return settingsWindow;
257 | }
258 |
259 | settingsWindow = new BrowserWindow({
260 | width: 400,
261 | height: 600,
262 | frame: false,
263 | modal: true,
264 | show: false,
265 | resizable: false,
266 | maximizable: false,
267 | minimizable: false,
268 | fullscreenable: false,
269 | titleBarStyle: 'hidden',
270 | titleBarOverlay: titleBarOverlay,
271 | parent: mainWindow,
272 | backgroundColor: getThemeBackgroundColor(), // 添加背景色,避免白色闪烁
273 | webPreferences: {
274 | preload: path.join(__dirname, '..', 'preload', 'preload.js'),
275 | },
276 | });
277 |
278 | settingsWindow.loadFile(path.join(__dirname, '..', 'renderer', 'settings.html'));
279 |
280 | // 注意:不再在ready-to-show事件中显示窗口,而是等待渲染进程通知UI准备完成
281 |
282 | settingsWindow.on('closed', () => {
283 | settingsWindow = null;
284 | });
285 |
286 | return settingsWindow;
287 | }
288 |
289 | /**
290 | * 关闭设置窗口
291 | * 使用平滑动画避免闪烁
292 | */
293 | function closeSettingsWindow() {
294 | if (settingsWindow && !settingsWindow.isDestroyed()) {
295 | // 使用公共的淡出函数处理窗口关闭
296 | fadeWindowOut(settingsWindow, () => {
297 | settingsWindow.close();
298 | settingsWindow = null;
299 | });
300 | }
301 | }
302 |
303 | /**
304 | * 显示主窗口(如不存在则创建)
305 | */
306 | function showMainWindow() {
307 | // 检查窗口是否存在且未被销毁
308 | if (!mainWindow || mainWindow.isDestroyed()) {
309 | // 如果窗口不存在或已销毁,则创建新窗口
310 | createMainWindow();
311 | return;
312 | }
313 |
314 | // 无论窗口是否可见,都确保显示并聚焦
315 | mainWindow.show();
316 | mainWindow.focus();
317 | }
318 |
319 | /**
320 | * 关闭添加/编辑项目窗口
321 | * 使用平滑动画避免闪烁
322 | */
323 | function closeAddItemWindow() {
324 | if (addItemWindow && !addItemWindow.isDestroyed()) {
325 | // 使用公共的淡出函数处理窗口关闭
326 | fadeWindowOut(addItemWindow, () => {
327 | addItemWindow.close();
328 | addItemWindow = null;
329 | });
330 | }
331 | }
332 |
333 | /**
334 | * 隐藏主窗口
335 | * 在macOS上使用淡出动画避免闪烁
336 | */
337 | function hideMainWindow() {
338 | if (mainWindow && !mainWindow.isDestroyed()) {
339 | // 使用公共的淡出函数处理窗口隐藏
340 | fadeWindowOut(mainWindow, () => {
341 | mainWindow.hide();
342 | // 重置透明度,以便下次显示
343 | setTimeout(() => {
344 | if (mainWindow && !mainWindow.isDestroyed()) {
345 | mainWindow.setOpacity(1.0);
346 | }
347 | }, 100);
348 | });
349 | }
350 | }
351 |
352 | /**
353 | * 通知主窗口项目列表已更新
354 | * @param {number} newItemIndex 新添加项目的索引(可选)
355 | */
356 | function notifyItemsUpdated(newItemIndex) {
357 | if (mainWindow && !mainWindow.isDestroyed()) {
358 | mainWindow.webContents.send('items-updated', newItemIndex);
359 | }
360 | }
361 |
362 | /**
363 | * 获取主窗口引用
364 | * @returns {BrowserWindow} 主窗口对象
365 | */
366 | function getMainWindow() {
367 | return mainWindow;
368 | }
369 |
370 | /**
371 | * 获取添加项目窗口引用
372 | * @returns {BrowserWindow} 添加项目窗口对象
373 | */
374 | function getAddItemWindow() {
375 | return addItemWindow;
376 | }
377 |
378 | /**
379 | * 获取设置窗口引用
380 | * @returns {BrowserWindow} 设置窗口对象
381 | */
382 | function getSettingsWindow() {
383 | return settingsWindow;
384 | }
385 |
386 | /**
387 | * 当UI准备好后显示主窗口
388 | * 由渲染进程通知UI(主题和语言)已完全准备就绪时调用
389 | */
390 | function showMainWindowWhenReady() {
391 | if (mainWindow && !mainWindow.isDestroyed()) {
392 | mainWindow.show();
393 | mainWindow.focus();
394 | }
395 | }
396 |
397 | /**
398 | * 当UI准备好后显示设置窗口
399 | */
400 | function showSettingsWindowWhenReady() {
401 | if (settingsWindow && !settingsWindow.isDestroyed()) {
402 | settingsWindow.show();
403 | settingsWindow.focus();
404 | }
405 | }
406 |
407 | /**
408 | * 当UI准备好后显示添加/编辑项目窗口
409 | */
410 | function showAddItemWindowWhenReady() {
411 | if (addItemWindow && !addItemWindow.isDestroyed()) {
412 | addItemWindow.show();
413 | addItemWindow.focus();
414 | }
415 | }
416 |
417 | // 导出模块函数
418 | module.exports = {
419 | createMainWindow,
420 | createAddItemWindow,
421 | createEditItemWindow,
422 | createSettingsWindow,
423 | showMainWindow,
424 | closeAddItemWindow,
425 | closeSettingsWindow,
426 | hideMainWindow,
427 | notifyItemsUpdated,
428 | getMainWindow,
429 | getAddItemWindow,
430 | getSettingsWindow,
431 | showMainWindowWhenReady,
432 | showSettingsWindowWhenReady,
433 | showAddItemWindowWhenReady
434 | };
--------------------------------------------------------------------------------
/src/shared/i18n.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 国际化支持模块 (i18n)
3 | * 提供多语言支持功能,支持从外部 JSON 文件动态加载翻译
4 | * 允许用户自定义添加新的语言文件
5 | */
6 |
7 | const fs = require('fs');
8 | const path = require('path');
9 | const { app } = require('electron');
10 |
11 | // 获取应用的路径(区分开发环境和打包后环境)
12 | const isPackaged = app && app.isPackaged;
13 | const appPath = isPackaged ? path.dirname(process.resourcesPath) : process.cwd();
14 |
15 | // 语言文件目录路径 - 修正资源路径
16 | const localesDir = isPackaged
17 | ? path.join(process.resourcesPath, 'app', 'src', 'assets', 'locales')
18 | : path.join(appPath, 'src', 'assets', 'locales');
19 |
20 | // 用户自定义语言文件目录,放在用户数据目录下
21 | const userLocalesDir = path.join(
22 | app ? app.getPath('userData') : path.join(appPath, 'user-data'),
23 | 'locales'
24 | );
25 |
26 | console.log('Locale directories:', {
27 | appPath,
28 | isPackaged,
29 | localesDir,
30 | userLocalesDir,
31 | localesDirExists: fs.existsSync(localesDir)
32 | });
33 |
34 | // 确保用户自定义语言文件目录存在
35 | try {
36 | if (!fs.existsSync(userLocalesDir)) {
37 | fs.mkdirSync(userLocalesDir, { recursive: true });
38 | }
39 | } catch (error) {
40 | console.error('Error creating user locale directory:', error);
41 | }
42 |
43 | // 存储已加载的翻译内容
44 | const translations = {};
45 |
46 | // 存储可用的语言列表
47 | let availableLanguages = [];
48 |
49 | /**
50 | * 从文件加载翻译内容
51 | * @param {string} langCode 语言代码
52 | * @returns {Object} 翻译内容
53 | */
54 | function loadTranslationFile(langCode) {
55 | let translation = {};
56 |
57 | // 先尝试从用户自定义目录加载
58 | const userFilePath = path.join(userLocalesDir, `${langCode}.json`);
59 | let userTranslation = {};
60 |
61 | try {
62 | if (fs.existsSync(userFilePath)) {
63 | const fileContent = fs.readFileSync(userFilePath, 'utf8');
64 | userTranslation = JSON.parse(fileContent);
65 | console.log(`Loaded user custom language file: ${langCode}`);
66 | }
67 | } catch (error) {
68 | console.error(`Error loading user language file ${langCode}.json:`, error);
69 | }
70 |
71 | // 再从应用内置目录加载
72 | const builtinFilePath = path.join(localesDir, `${langCode}.json`);
73 | let builtinTranslation = {};
74 |
75 | try {
76 | if (fs.existsSync(builtinFilePath)) {
77 | const fileContent = fs.readFileSync(builtinFilePath, 'utf8');
78 | builtinTranslation = JSON.parse(fileContent);
79 | console.log(`Loaded built-in language file: ${langCode}`);
80 | } else {
81 | console.warn(`Built-in language file not found: ${builtinFilePath}`);
82 |
83 | // 尝试备用路径(开发环境中可能的路径)
84 | const altPath = path.join(appPath, 'src', 'assets', 'locales', `${langCode}.json`);
85 | if (fs.existsSync(altPath)) {
86 | const fileContent = fs.readFileSync(altPath, 'utf8');
87 | builtinTranslation = JSON.parse(fileContent);
88 | console.log(`Loaded language file from alternative path: ${langCode}`);
89 | }
90 | }
91 | } catch (error) {
92 | console.error(`Error loading built-in language file ${langCode}.json:`, error);
93 | }
94 |
95 | // 用户翻译覆盖内置翻译
96 | translation = { ...builtinTranslation, ...userTranslation };
97 |
98 | return translation;
99 | }
100 |
101 | /**
102 | * 加载所有可用的语言文件
103 | */
104 | function loadAllLanguages() {
105 | const builtinLangs = new Set();
106 | const userLangs = new Set();
107 |
108 | // 获取内置语言文件列表
109 | try {
110 | if (fs.existsSync(localesDir)) {
111 | fs.readdirSync(localesDir)
112 | .filter(file => file.endsWith('.json'))
113 | .forEach(file => {
114 | const langCode = file.replace('.json', '');
115 | builtinLangs.add(langCode);
116 |
117 | // 加载内置语言
118 | translations[langCode] = loadTranslationFile(langCode);
119 | });
120 | console.log('Loaded built-in language files from standard path');
121 | } else {
122 | // 尝试备用路径(开发环境中可能的路径)
123 | const altLocalesDir = path.join(appPath, 'src', 'assets', 'locales');
124 | if (fs.existsSync(altLocalesDir)) {
125 | fs.readdirSync(altLocalesDir)
126 | .filter(file => file.endsWith('.json'))
127 | .forEach(file => {
128 | const langCode = file.replace('.json', '');
129 | builtinLangs.add(langCode);
130 |
131 | // 加载内置语言
132 | translations[langCode] = loadTranslationFile(langCode);
133 | });
134 | console.log('Loaded built-in language files from alternative path');
135 | } else {
136 | console.error('Built-in language directory not found:', { localesDir, altLocalesDir });
137 | }
138 | }
139 | } catch (error) {
140 | console.error('Error reading built-in language directory:', error);
141 | }
142 |
143 | // 获取用户自定义语言文件列表
144 | try {
145 | if (fs.existsSync(userLocalesDir)) {
146 | fs.readdirSync(userLocalesDir)
147 | .filter(file => file.endsWith('.json'))
148 | .forEach(file => {
149 | const langCode = file.replace('.json', '');
150 | userLangs.add(langCode);
151 |
152 | // 如果之前没有加载过这个语言,现在加载它
153 | if (!translations[langCode]) {
154 | translations[langCode] = loadTranslationFile(langCode);
155 | }
156 | });
157 | }
158 | } catch (error) {
159 | console.error('Error reading user language directory:', error);
160 | }
161 |
162 | // 合并语言列表
163 | availableLanguages = [...new Set([...builtinLangs, ...userLangs])];
164 | console.log('Available languages:', availableLanguages);
165 | }
166 |
167 | // 首次加载所有语言
168 | loadAllLanguages();
169 |
170 | // 获取系统语言
171 | function getSystemLanguage() {
172 | // 获取系统语言,如果无法获取则默认使用英文
173 | const systemLang = (typeof navigator !== 'undefined' ? navigator.language : null) ||
174 | (app ? app.getLocale() : null) || 'en-US';
175 |
176 | console.log('System language:', systemLang);
177 |
178 | // 将系统语言映射到支持的语言
179 | if (systemLang.startsWith('zh')) {
180 | return 'zh-CN';
181 | } else {
182 | return 'en-US'; // 默认英文
183 | }
184 | }
185 |
186 | // 当前语言,默认跟随系统
187 | let currentLanguage = null;
188 |
189 | // 存储语言变化监听器 - 观察者模式实现
190 | const languageChangeListeners = [];
191 |
192 | /**
193 | * 添加语言变化监听器
194 | * @param {Function} listener 监听函数
195 | */
196 | function addLanguageChangeListener(listener) {
197 | if (typeof listener === 'function' && !languageChangeListeners.includes(listener)) {
198 | languageChangeListeners.push(listener);
199 | }
200 | }
201 |
202 | /**
203 | * 移除语言变化监听器
204 | * @param {Function} listener 要移除的监听函数
205 | */
206 | function removeLanguageChangeListener(listener) {
207 | const index = languageChangeListeners.indexOf(listener);
208 | if (index !== -1) {
209 | languageChangeListeners.splice(index, 1);
210 | }
211 | }
212 |
213 | /**
214 | * 通知所有监听器语言已变化
215 | */
216 | function notifyLanguageChangeListeners(newLanguage) {
217 | for (const listener of languageChangeListeners) {
218 | try {
219 | listener(newLanguage);
220 | } catch (error) {
221 | console.error("Error executing language change listener:", error);
222 | }
223 | }
224 | }
225 |
226 | /**
227 | * 获取当前使用的语言
228 | * @returns {string} 当前语言代码
229 | */
230 | function getCurrentLanguage() {
231 | if (!currentLanguage) {
232 | try {
233 | // 尝试从 main 进程中获取语言设置
234 | const { app } = require('electron');
235 | if (app) {
236 | const dataStore = require('../main/data-store');
237 | const appConfig = dataStore.getAppConfig();
238 | if (appConfig.language === 'system' || !appConfig.language) {
239 | currentLanguage = getSystemLanguage();
240 | } else {
241 | currentLanguage = appConfig.language;
242 | }
243 | } else {
244 | // 在渲染进程中,无法直接访问 dataStore
245 | // 此时应该已经由主进程设置了当前语言
246 | currentLanguage = getSystemLanguage();
247 | }
248 | } catch (error) {
249 | console.error('Error getting language configuration:', error);
250 | currentLanguage = getSystemLanguage();
251 | }
252 | }
253 | return currentLanguage;
254 | }
255 |
256 | /**
257 | * 设置当前语言
258 | * @param {string} lang 语言代码或"system"
259 | */
260 | function setLanguage(lang) {
261 | let newLang;
262 |
263 | if (lang === 'system') {
264 | newLang = getSystemLanguage();
265 | } else if (translations[lang]) {
266 | newLang = lang;
267 | } else {
268 | // 尝试先加载该语言文件
269 | try {
270 | translations[lang] = loadTranslationFile(lang);
271 | if (Object.keys(translations[lang]).length > 0) {
272 | // 成功加载语言文件
273 | newLang = lang;
274 | if (!availableLanguages.includes(lang)) {
275 | availableLanguages.push(lang);
276 | }
277 | } else {
278 | newLang = 'en-US'; // 默认回退到英文
279 | console.error(`Language file is empty: ${lang}`);
280 | }
281 | } catch (error) {
282 | newLang = 'en-US'; // 默认回退到英文
283 | console.error(`Unsupported language: ${lang}`, error);
284 | }
285 | }
286 |
287 | // 保存到全局变量
288 | currentLanguage = newLang;
289 |
290 | // 尝试保存到 data-store
291 | try {
292 | const { app } = require('electron');
293 | if (app) {
294 | const dataStore = require('../main/data-store');
295 | dataStore.updateLanguageConfig(lang);
296 | }
297 | } catch (error) {
298 | console.error('Error saving language configuration:', error);
299 | }
300 |
301 | // 通知语言变化
302 | notifyLanguageChangeListeners(newLang);
303 |
304 | return newLang;
305 | }
306 |
307 | /**
308 | * 获取所有可用的语言列表
309 | * @returns {Array} 语言代码数组
310 | */
311 | function getAvailableLanguages() {
312 | // 重新加载语言文件,以确保获取最新列表
313 | loadAllLanguages();
314 | return availableLanguages;
315 | }
316 |
317 | /**
318 | * 获取语言名称
319 | * @param {string} langCode 语言代码
320 | * @returns {string} 语言名称
321 | */
322 | function getLanguageName(langCode) {
323 | // 使用当前语言显示目标语言的名称
324 | const lang = getCurrentLanguage();
325 | const translationSet = translations[lang] || translations['en-US'];
326 |
327 | // 如果有该语言的翻译,则使用翻译
328 | if (translationSet && translationSet[langCode]) {
329 | return translationSet[langCode];
330 | }
331 |
332 | // 如果没有翻译,则使用语言代码
333 | return langCode;
334 | }
335 |
336 | /**
337 | * 添加或更新用户自定义语言
338 | * @param {string} langCode 语言代码
339 | * @param {Object} translation 翻译内容
340 | */
341 | function addUserLanguage(langCode, translation) {
342 | try {
343 | const filePath = path.join(userLocalesDir, `${langCode}.json`);
344 | fs.writeFileSync(filePath, JSON.stringify(translation, null, 2), 'utf8');
345 |
346 | // 重新加载该语言
347 | translations[langCode] = loadTranslationFile(langCode);
348 |
349 | // 更新可用语言列表
350 | if (!availableLanguages.includes(langCode)) {
351 | availableLanguages.push(langCode);
352 | }
353 |
354 | return true;
355 | } catch (error) {
356 | console.error(`Error adding user custom language ${langCode}:`, error);
357 | return false;
358 | }
359 | }
360 |
361 | /**
362 | * 获取翻译文本
363 | * @param {string} key 翻译键
364 | * @param {Object} params 替换参数 (可选)
365 | * @returns {string} 翻译后的文本
366 | */
367 | function t(key, params = null) {
368 | const lang = getCurrentLanguage();
369 | const translationSet = translations[lang] || translations['en-US'];
370 |
371 | let text = translationSet[key];
372 | if (text === undefined) {
373 | console.warn(`Missing translation: ${key} for language: ${lang}`);
374 | // 尝试从英文中获取
375 | text = translations['en-US'][key];
376 | // 如果英文也没有,则返回键名
377 | if (text === undefined) {
378 | return key;
379 | }
380 | }
381 |
382 | // 处理参数替换
383 | if (params) {
384 | Object.keys(params).forEach(param => {
385 | text = text.replace(`{${param}}`, params[param]);
386 | });
387 | }
388 |
389 | return text;
390 | }
391 |
392 | // 导出模块功能
393 | module.exports = {
394 | t,
395 | setLanguage,
396 | getCurrentLanguage,
397 | getSystemLanguage,
398 | addLanguageChangeListener,
399 | removeLanguageChangeListener,
400 | getAvailableLanguages,
401 | getLanguageName,
402 | addUserLanguage
403 | };
--------------------------------------------------------------------------------
/src/renderer/js/settings.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 设置页面的脚本
3 | * 处理设置窗口的所有功能,包括主题设置、数据管理和应用信息显示
4 | */
5 | document.addEventListener("DOMContentLoaded", async () => {
6 | // 导入i18n模块
7 | const i18n = window.electronAPI.i18n;
8 |
9 | // DOM 元素引用
10 | const themeSelect = document.getElementById("theme-select");
11 | const languageSelect = document.getElementById("language-select");
12 | const clearDataBtn = document.getElementById("clear-data-btn");
13 | const openStorageBtn = document.getElementById("open-storage-btn");
14 | const githubLink = document.getElementById("github-link");
15 | const reportIssueLink = document.getElementById("report-issue");
16 |
17 | // 快捷键设置元素引用
18 | const enableShortcutCheckbox = document.getElementById("enable-shortcut");
19 | const shortcutInput = document.getElementById("shortcut-input");
20 | const recordShortcutBtn = document.getElementById("record-shortcut-btn");
21 | const resetShortcutBtn = document.getElementById("reset-shortcut-btn");
22 |
23 | // 自启动设置元素引用
24 | const enableAutoLaunchCheckbox = document.getElementById("enable-auto-launch");
25 |
26 | const modalContainer = document.querySelector(".modal");
27 |
28 | // 初始化UI,保存返回的解绑函数对象
29 | const uiCleanup = window.uiManager.initUI({
30 | containerSelector: ".modal",
31 | windowType: "settings" // 指定窗口类型为设置窗口
32 | });
33 |
34 | // 当页面卸载时清理监听器
35 | window.addEventListener('beforeunload', () => {
36 | if (uiCleanup && typeof uiCleanup.unbindAll === 'function') {
37 | uiCleanup.unbindAll();
38 | }
39 | });
40 |
41 | // 初始化设置页面
42 | await initSettingsPage();
43 |
44 | // 加载已保存的主题设置
45 | await loadThemeSetting();
46 |
47 | // 加载可用语言列表和已保存的语言设置
48 | await loadLanguages();
49 |
50 | // 加载快捷键设置
51 | await loadShortcutSettings();
52 |
53 | // 加载自启动设置
54 | await loadAutoLaunchSetting();
55 |
56 | /**
57 | * 事件监听设置部分
58 | */
59 |
60 | /**
61 | * 主题选择变化事件
62 | * 保存并应用用户选择的主题
63 | */
64 | themeSelect.addEventListener("change", () => {
65 | const theme = themeSelect.value;
66 | // 立即应用主题到当前窗口
67 | window.uiUtils.applyTheme(theme, modalContainer);
68 | // 通知主进程和其他窗口主题已更改
69 | window.electronAPI.themeChanged(theme);
70 | });
71 |
72 | /**
73 | * 语言选择变化事件
74 | * 保存并应用用户选择的语言
75 | */
76 | languageSelect.addEventListener("change", async () => {
77 | const language = languageSelect.value;
78 | // 应用新语言
79 | await i18n.setLanguage(language);
80 | // 更新页面文本
81 | await window.uiUtils.updatePageTexts(i18n);
82 | // 通知主进程和其他窗口语言已更改
83 | window.electronAPI.languageChanged(language);
84 | });
85 |
86 | /**
87 | * 启用全局快捷键复选框变化事件
88 | * 保存设置并更新界面状态
89 | */
90 | enableShortcutCheckbox.addEventListener("change", async () => {
91 | if (!enableShortcutCheckbox.checked && recordingShortcut) {
92 | // 如果取消选中时正在录制快捷键,停止录制
93 | // 恢复上次的有效值
94 | const config = await window.electronAPI.getShortcutConfig();
95 | setShortcutInputValueWithFormat(config.shortcut);
96 | stopRecordingShortcut();
97 | }
98 |
99 | updateShortcutConfig({ enabled: enableShortcutCheckbox.checked });
100 | updateShortcutInputState();
101 | });
102 |
103 | /**
104 | * 清空数据按钮点击事件
105 | * 显示确认对话框,确认后清空所有项目
106 | */
107 | clearDataBtn.addEventListener("click", async () => {
108 | const confirmMessage = await i18n.t("confirm-clear-data");
109 | if (confirm(confirmMessage)) {
110 | window.electronAPI.clearAllItems();
111 | window.uiManager.showToast(await i18n.t("data-cleared"));
112 | }
113 | });
114 |
115 | /**
116 | * 打开存储位置按钮点击事件
117 | * 打开应用数据存储目录
118 | */
119 | openStorageBtn.addEventListener("click", () => {
120 | window.electronAPI.openStorageLocation();
121 | });
122 |
123 | /**
124 | * GitHub链接点击事件处理
125 | * 使用默认浏览器打开GitHub仓库
126 | */
127 | githubLink.addEventListener("click", (e) => {
128 | e.preventDefault();
129 | window.electronAPI.openExternalLink("https://github.com/SolarianZ/launcher-app-electron");
130 | });
131 |
132 | /**
133 | * 报告问题链接点击事件处理
134 | * 使用默认浏览器打开GitHub issues页面
135 | */
136 | reportIssueLink.addEventListener("click", (e) => {
137 | e.preventDefault();
138 | window.electronAPI.openExternalLink("https://github.com/SolarianZ/launcher-app-electron/issues");
139 | });
140 |
141 | /**
142 | * 启用自启动复选框变化事件
143 | * 保存设置并更新系统配置
144 | */
145 | enableAutoLaunchCheckbox.addEventListener("change", async () => {
146 | try {
147 | const result = await window.electronAPI.updateAutoLaunchConfig({
148 | enabled: enableAutoLaunchCheckbox.checked
149 | });
150 | if (result) {
151 | window.uiManager.showToast(await i18n.t(
152 | enableAutoLaunchCheckbox.checked ? "auto-launch-enabled" : "auto-launch-disabled"
153 | ));
154 | } else {
155 | enableAutoLaunchCheckbox.checked = !enableAutoLaunchCheckbox.checked;
156 | window.uiManager.showToast(await i18n.t("auto-launch-update-failed"), true);
157 | }
158 | } catch (error) {
159 | console.error("Error updating auto launch config:", error);
160 | window.uiManager.showToast(await i18n.t("auto-launch-update-failed"), true);
161 | }
162 | });
163 |
164 | /**
165 | * 全局键盘事件处理
166 | * - Escape: 关闭窗口
167 | * - F12: 开发者工具
168 | */
169 | document.addEventListener("keydown", (e) => {
170 | if (e.key === "Escape") {
171 | window.electronAPI.closeSettingsWindow();
172 | e.preventDefault();
173 | } else if (e.key === "F12") {
174 | window.electronAPI.openDevTools();
175 | e.preventDefault();
176 | }
177 | });
178 |
179 | function setShortcutInputValueWithFormat(shortcut) {
180 | // 将快捷键中的字母键转换为大写显示
181 | const parts = shortcut.split('+');
182 | const formattedParts = parts.map(part => {
183 | // 如果是单个字符且是字母,转换为大写
184 | if (part.length === 1 && part.match(/[a-zA-Z]/)) {
185 | return part.toUpperCase();
186 | }
187 | // 其他情况(修饰键等)保持不变
188 | return part;
189 | });
190 | shortcutInput.value = formattedParts.join('+');
191 | }
192 |
193 | // 处理快捷键录入的事件监听
194 | setupShortcutRecording();
195 |
196 | /**
197 | * 初始化设置页面
198 | * 获取应用信息并显示版本号
199 | */
200 | async function initSettingsPage() {
201 | // 获取应用信息
202 | try {
203 | const appInfo = await window.electronAPI.getAppInfo();
204 | document.getElementById("version-number").textContent = appInfo.version;
205 | } catch (error) {
206 | console.error("Error getting app info:", error);
207 | }
208 | }
209 |
210 | /**
211 | * 加载主题设置
212 | * 从主进程获取主题设置并设置到下拉选择框
213 | */
214 | async function loadThemeSetting() {
215 | // 通过 API 获取保存的主题设置
216 | const savedTheme = await window.electronAPI.getThemeConfig();
217 | themeSelect.value = savedTheme;
218 | }
219 |
220 | /**
221 | * 加载可用语言列表和已保存的语言设置
222 | * 从i18n模块获取所有可用语言并填充到下拉选择框
223 | */
224 | async function loadLanguages() {
225 | try {
226 | // 清空现有选项,只保留"系统"选项
227 | while (languageSelect.options.length > 1) {
228 | languageSelect.remove(1);
229 | }
230 |
231 | // 获取所有可用语言
232 | const languages = await i18n.getAvailableLanguages();
233 |
234 | // 添加语言选项
235 | for (const langCode of languages) {
236 | const langName = await i18n.getLanguageName(langCode);
237 | const option = document.createElement('option');
238 | option.value = langCode;
239 | option.textContent = langName;
240 | languageSelect.appendChild(option);
241 | }
242 |
243 | // 获取保存的语言设置
244 | const savedLanguage = await window.electronAPI.getLanguageConfig();
245 | languageSelect.value = savedLanguage;
246 | } catch (error) {
247 | console.error("Error loading language list:", error);
248 | }
249 | }
250 |
251 | /**
252 | * 加载快捷键配置
253 | * 从主进程获取快捷键配置并设置到界面
254 | */
255 | async function loadShortcutSettings() {
256 | try {
257 | const config = await window.electronAPI.getShortcutConfig();
258 | enableShortcutCheckbox.checked = config.enabled;
259 | setShortcutInputValueWithFormat(config.shortcut);
260 | updateShortcutInputState();
261 | } catch (error) {
262 | console.error("Error loading shortcut settings:", error);
263 | }
264 | }
265 |
266 | /**
267 | * 加载自启动设置
268 | * 从主进程获取自启动设置并设置到界面
269 | */
270 | async function loadAutoLaunchSetting() {
271 | try {
272 | const config = await window.electronAPI.getAutoLaunchConfig();
273 | enableAutoLaunchCheckbox.checked = config.enabled;
274 | } catch (error) {
275 | console.error("Error loading auto-launch setting:", error);
276 | }
277 | }
278 |
279 | /**
280 | * 更新快捷键配置
281 | * 向主进程发送更新的快捷键配置
282 | * @param {Object} config 配置对象,可以包含 enabled 和 shortcut 属性
283 | */
284 | async function updateShortcutConfig(config) {
285 | try {
286 | await window.electronAPI.updateShortcutConfig(config);
287 | } catch (error) {
288 | console.error("Error updating shortcut config:", error);
289 | }
290 | }
291 |
292 | /**
293 | * 更新快捷键输入框状态
294 | * 根据启用状态设置输入框和按钮的可用性
295 | */
296 | function updateShortcutInputState() {
297 | const enabled = enableShortcutCheckbox.checked;
298 | shortcutInput.disabled = !enabled;
299 | recordShortcutBtn.disabled = !enabled;
300 | resetShortcutBtn.disabled = !enabled;
301 |
302 | // 根据启用状态设置快捷键相关控件的样式
303 | if (enabled) {
304 | shortcutInput.classList.remove('disabled-input');
305 | recordShortcutBtn.classList.remove('disabled-btn');
306 | resetShortcutBtn.classList.remove('disabled-btn');
307 | } else {
308 | shortcutInput.classList.add('disabled-input');
309 | recordShortcutBtn.classList.add('disabled-btn');
310 | resetShortcutBtn.classList.add('disabled-btn');
311 | }
312 | }
313 |
314 | /**
315 | * 是否正在录制快捷键
316 | */
317 | let recordingShortcut = false;
318 |
319 | /**
320 | * 进入快捷键录入模式
321 | */
322 | async function startRecordingShortcut() {
323 | shortcutInput.classList.add("recording");
324 | recordingShortcut = true;
325 | recordShortcutBtn.textContent = await i18n.t("cancel");
326 | shortcutInput.value = await i18n.t("press-new-shortcut");
327 | }
328 |
329 | /**
330 | * 结束快捷键录入模式
331 | */
332 | async function stopRecordingShortcut() {
333 | shortcutInput.classList.remove("recording");
334 | recordingShortcut = false;
335 | recordShortcutBtn.textContent = await i18n.t("record-shortcut");
336 | }
337 |
338 | /**
339 | * 设置快捷键录入的事件监听
340 | */
341 | function setupShortcutRecording() {
342 | let pressedKeys = new Set();
343 |
344 | /**
345 | * 快捷键记录按钮点击事件
346 | * 进入快捷键记录模式
347 | */
348 | recordShortcutBtn.addEventListener("click", async () => {
349 | if (!recordingShortcut) {
350 | startRecordingShortcut();
351 | } else {
352 | // 恢复上次的有效值
353 | const config = await window.electronAPI.getShortcutConfig();
354 | setShortcutInputValueWithFormat(config.shortcut);
355 |
356 | stopRecordingShortcut();
357 | }
358 | });
359 |
360 | /**
361 | * 快捷键重置按钮点击事件
362 | * 重置快捷键为默认值
363 | */
364 | resetShortcutBtn.addEventListener("click", async () => {
365 | // 如果在录制模式中,先退出录制模式
366 | if (recordingShortcut) {
367 | await stopRecordingShortcut();
368 | }
369 |
370 | pressedKeys.clear();
371 |
372 | setShortcutInputValueWithFormat("Alt+Shift+Q");
373 | updateShortcutConfig({ shortcut: "Alt+Shift+Q" });
374 | });
375 |
376 | // 录制时捕获按键
377 | document.addEventListener("keydown", async (e) => {
378 | if (!recordingShortcut)
379 | return;
380 |
381 | e.preventDefault();
382 |
383 | const key = e.key;
384 | pressedKeys.add(key);
385 | if (key === "Control" || key === "Alt" || key === "Shift" || key === "Meta")
386 | return;
387 |
388 | // 如果按下了非修饰键,尝试提交按键组合
389 |
390 | // 检查是否包含除Shift以外的修饰键
391 | const hasRequiredModifier = pressedKeys.has("Control") || pressedKeys.has("Alt") || pressedKeys.has("Meta");
392 | if (!hasRequiredModifier) {
393 | // 如果不含必需的修饰键,显示错误消息
394 | window.uiManager.showToast(await i18n.t("shortcut-need-modifier"), true);
395 |
396 | // 恢复上次的有效值
397 | const config = await window.electronAPI.getShortcutConfig();
398 | setShortcutInputValueWithFormat(config.shortcut);
399 |
400 | stopRecordingShortcut();
401 | pressedKeys.clear();
402 |
403 | return;
404 | }
405 |
406 | // 创建快捷键字符串
407 | const shortcut = Array.from(pressedKeys).join("+");
408 | pressedKeys.clear();
409 |
410 | // 测试快捷键是否可用
411 | const testResult = await window.electronAPI.testShortcut(shortcut);
412 | if (testResult.success) {
413 | setShortcutInputValueWithFormat(shortcut);
414 |
415 | // 保存新快捷键
416 | updateShortcutConfig({ shortcut });
417 |
418 | stopRecordingShortcut();
419 | } else {
420 | // 失败,显示错误消息
421 | window.uiManager.showToast(testResult.message, true);
422 |
423 | // 恢复上次的有效值
424 | const config = await window.electronAPI.getShortcutConfig();
425 | setShortcutInputValueWithFormat(config.shortcut);
426 |
427 | stopRecordingShortcut();
428 | }
429 | });
430 |
431 | // 监听按键释放,从集合中移除
432 | document.addEventListener("keyup", (e) => {
433 | if (!recordingShortcut)
434 | return;
435 |
436 | e.preventDefault();
437 | pressedKeys.delete(e.key);
438 | });
439 | }
440 | });
441 |
--------------------------------------------------------------------------------
/src/renderer/css/styles.css:
--------------------------------------------------------------------------------
1 | /* 全局变量 */
2 | :root {
3 | --primary-color: #4a86e8;
4 | --secondary-color: #6aa84f;
5 | --text-color: #333;
6 | --bg-color: #f5f5f5;
7 | --card-bg: #fff;
8 | --border-color: #ddd;
9 | --hover-bg: #e9e9e9;
10 | --active-bg: #d9d9d9;
11 | --disabled-bg: #f0f0f0;
12 | --disabled-text: #999;
13 | --shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
14 | --title-bar-bg: #e0e0e0;
15 | --title-bar-text: #333;
16 | --search-bg: #fff;
17 | --modal-overlay: rgba(0, 0, 0, 0.5);
18 | --tooltip-bg: #333;
19 | --tooltip-text: #fff;
20 | --drag-indicator: #4a86e8;
21 | --icon-color: #555;
22 | --menu-bg: #fff;
23 | --menu-hover: #f0f0f0;
24 | --menu-border: #ddd;
25 | --button-text: #fff;
26 | --button-bg: #4a86e8;
27 | --button-hover: #3a76d8;
28 | --button-active: #2a66c8;
29 | --button-disabled: #a0b8e0;
30 | --cancel-button-bg: #f0f0f0;
31 | --cancel-button-text: #333;
32 | --cancel-button-hover: #e0e0e0;
33 | --cancel-button-active: #d0d0d0;
34 | --error-color: #d23f31;
35 | }
36 |
37 | /* 全局样式 */
38 | * {
39 | margin: 0;
40 | padding: 0;
41 | box-sizing: border-box;
42 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
43 | }
44 |
45 | body {
46 | background-color: transparent;
47 | overflow: hidden;
48 | }
49 |
50 | /* 禁用状态样式 */
51 | .disabled-input {
52 | background-color: var(--disabled-bg) !important;
53 | color: var(--disabled-text) !important;
54 | cursor: not-allowed !important;
55 | border-color: var(--disabled-bg) !important;
56 | }
57 |
58 | .disabled-btn {
59 | background-color: var(--disabled-bg) !important;
60 | color: var(--disabled-text) !important;
61 | cursor: not-allowed !important;
62 | border-color: var(--disabled-bg) !important;
63 | opacity: 0.7;
64 | }
65 |
66 | /* 应用容器 */
67 | .app-container {
68 | width: 100vw;
69 | height: 100vh;
70 | background-color: var(--bg-color);
71 | overflow: hidden;
72 | display: flex;
73 | flex-direction: column;
74 | transition: all 0.3s ease;
75 | box-shadow: var(--shadow);
76 | }
77 |
78 | /* 标题栏 */
79 | .title-bar {
80 | height: 30px;
81 | flex-shrink: 0;
82 | background-color: var(--title-bar-bg);
83 | display: flex;
84 | align-items: center;
85 | justify-content: space-between;
86 | padding: 0 10px;
87 | user-select: none;
88 | -webkit-app-region: drag;
89 | }
90 |
91 | .title-bar-left,
92 | .title-bar-right {
93 | flex: 1;
94 | display: flex;
95 | gap: 10px;
96 | -webkit-app-region: no-drag;
97 | }
98 |
99 | /* 标题容器 */
100 | .title-container {
101 | display: flex;
102 | align-items: center;
103 | justify-content: center;
104 | position: relative;
105 | -webkit-app-region: drag;
106 | }
107 |
108 | .window-title {
109 | color: var(--title-bar-text);
110 | font-weight: 500;
111 | text-align: center;
112 | }
113 |
114 | /* 设置按钮样式 */
115 | .settings-button {
116 | width: 18px;
117 | height: 18px;
118 | border: none;
119 | background-color: transparent;
120 | cursor: pointer;
121 | outline: none;
122 | display: flex;
123 | align-items: center;
124 | justify-content: center;
125 | margin-left: 8px;
126 | -webkit-app-region: no-drag;
127 | }
128 |
129 | .settings-button:hover {
130 | background-color: var(--hover-bg);
131 | }
132 |
133 | .settings-button:active {
134 | background-color: var(--active-bg);
135 | }
136 |
137 | /* 设置模态窗口 */
138 | .settings-modal {
139 | width: 100%;
140 | height: 100%;
141 | background-color: var(--modal-overlay);
142 | z-index: 1000;
143 | }
144 |
145 | /* 设置页样式 */
146 | .settings-section {
147 | margin-bottom: 20px;
148 | border-bottom: 1px solid var(--border-color);
149 | padding-bottom: 15px;
150 | }
151 |
152 | .settings-section:last-child {
153 | border-bottom: none;
154 | margin-bottom: 0;
155 | }
156 |
157 | .settings-section h3 {
158 | font-size: 16px;
159 | margin-bottom: 10px;
160 | color: var(--text-color);
161 | }
162 |
163 | .settings-item {
164 | display: flex;
165 | flex-direction: column;
166 | margin-bottom: 15px;
167 | }
168 |
169 | .settings-item:last-child {
170 | margin-bottom: 0;
171 | }
172 |
173 | .settings-item label {
174 | margin-bottom: 5px;
175 | color: var(--text-color);
176 | }
177 |
178 | /* 提示文本样式 */
179 | .common-tip {
180 | font-size: 12px;
181 | color: var(--disabled-text);
182 | margin-top: 2px;
183 | }
184 |
185 | .settings-btn {
186 | background-color: var(--button-bg);
187 | color: var(--button-text);
188 | border: none;
189 | padding: 8px 12px;
190 | border-radius: 4px;
191 | cursor: pointer;
192 | margin-bottom: 5px;
193 | transition: background-color 0.2s;
194 | text-align: left;
195 | }
196 |
197 | .settings-btn:hover {
198 | background-color: var(--button-hover);
199 | }
200 |
201 | /* 快捷键设置样式 */
202 | .checkbox-container {
203 | display: flex;
204 | align-items: center;
205 | gap: 8px;
206 | margin-bottom: 5px;
207 | }
208 |
209 | .toggle-checkbox {
210 | width: 16px;
211 | height: 16px;
212 | cursor: pointer;
213 | }
214 |
215 | .shortcut-input-container {
216 | display: flex;
217 | align-items: center;
218 | gap: 8px;
219 | margin-bottom: 5px;
220 | }
221 |
222 | .shortcut-input {
223 | flex: 1;
224 | padding: 8px 12px;
225 | border-radius: 4px;
226 | border: 1px solid var(--border-color);
227 | background-color: var(--search-bg);
228 | color: var(--text-color);
229 | font-family: monospace;
230 | text-align: center;
231 | }
232 |
233 | .settings-btn.small {
234 | padding: 4px 8px;
235 | font-size: 12px;
236 | }
237 |
238 | .recording {
239 | background-color: var(--error-color);
240 | color: white;
241 | }
242 |
243 | /* 关于部分样式 */
244 | .about-section {
245 | display: flex;
246 | flex-direction: column;
247 | align-items: center;
248 | text-align: center;
249 | }
250 |
251 | .app-logo {
252 | font-size: 48px;
253 | margin-bottom: 10px;
254 | }
255 |
256 | .app-title {
257 | font-size: 18px;
258 | font-weight: bold;
259 | margin-bottom: 5px;
260 | color: var(--text-color);
261 | }
262 |
263 | .app-version {
264 | font-size: 14px;
265 | color: var(--disabled-text);
266 | margin-bottom: 10px;
267 | }
268 |
269 | .app-desc {
270 | margin-bottom: 10px;
271 | color: var(--text-color);
272 | }
273 |
274 | .app-links {
275 | margin-top: 10px;
276 | }
277 |
278 | .app-links a {
279 | color: var(--primary-color);
280 | text-decoration: none;
281 | }
282 |
283 | .app-links a:hover {
284 | text-decoration: underline;
285 | }
286 |
287 | .window-title {
288 | flex-grow: 1;
289 | text-align: center;
290 | color: var(--title-bar-text);
291 | font-weight: 500;
292 | }
293 |
294 | .window-control {
295 | width: 16px;
296 | height: 16px;
297 | border-radius: 50%;
298 | cursor: pointer;
299 | display: flex;
300 | align-items: center;
301 | justify-content: center;
302 | font-size: 12px;
303 | color: var(--title-bar-text);
304 | }
305 |
306 | /* 搜索框 */
307 | .search-container {
308 | padding: 10px;
309 | background-color: var(--bg-color);
310 | display: flex;
311 | align-items: center;
312 | gap: 8px;
313 | }
314 |
315 | .search-input {
316 | flex: 1;
317 | padding: 8px 12px;
318 | border-radius: 4px;
319 | border: 1px solid var(--border-color);
320 | background-color: var(--search-bg);
321 | color: var(--text-color);
322 | }
323 |
324 | /* 按钮 */
325 | .add-button {
326 | background-color: var(--button-bg);
327 | color: var(--button-text);
328 | width: 32px;
329 | height: 32px;
330 | border-radius: 4px;
331 | border: none;
332 | display: flex;
333 | align-items: center;
334 | justify-content: center;
335 | cursor: pointer;
336 | font-size: 18px;
337 | transition: background-color 0.2s;
338 | }
339 |
340 | .add-button:hover {
341 | background-color: var(--button-hover);
342 | }
343 |
344 | .add-button:active {
345 | background-color: var(--button-active);
346 | }
347 |
348 | /* 列表 */
349 | .list-container {
350 | flex-grow: 1;
351 | overflow-y: auto;
352 | padding: 10px;
353 | }
354 |
355 | /* 空列表消息 */
356 | .empty-list-message {
357 | text-align: center;
358 | padding: 20px;
359 | color: var(--text-color);
360 | display: flex;
361 | flex-direction: column;
362 | align-items: center;
363 | justify-content: center;
364 | height: 100px;
365 | margin-top: 20px;
366 | }
367 |
368 | .empty-text {
369 | font-size: 16px;
370 | color: var(--disabled-text);
371 | }
372 |
373 | .list-item {
374 | display: flex;
375 | align-items: center;
376 | padding: 10px;
377 | border-radius: 4px;
378 | margin-bottom: 8px;
379 | background-color: var(--card-bg);
380 | cursor: pointer;
381 | user-select: none;
382 | transition: background-color 0.2s;
383 | border: 1px solid var(--border-color);
384 | }
385 |
386 | .list-item:hover {
387 | background-color: var(--hover-bg);
388 | }
389 |
390 | .list-item.active {
391 | background-color: var(--active-bg);
392 | border-color: var(--primary-color);
393 | }
394 |
395 | .list-item.dragging {
396 | opacity: 0.5;
397 | border: 1px dashed var(--drag-indicator);
398 | }
399 |
400 | .list-item.drop-target {
401 | border-bottom: 2px solid var(--drag-indicator);
402 | }
403 |
404 | .item-icon {
405 | width: 24px;
406 | height: 24px;
407 | margin-right: 12px;
408 | color: var(--icon-color);
409 | display: flex;
410 | align-items: center;
411 | justify-content: center;
412 | }
413 |
414 | .item-text {
415 | flex-grow: 1;
416 | white-space: nowrap;
417 | overflow: hidden;
418 | text-overflow: ellipsis;
419 | color: var(--text-color);
420 | }
421 |
422 | /* 工具提示 */
423 | .tooltip {
424 | /* fixed 相对于视口定位 */
425 | position: fixed;
426 | background-color: var(--tooltip-bg);
427 | color: var(--tooltip-text);
428 | padding: 5px 10px;
429 | border-radius: 4px;
430 | font-size: 14px;
431 | /* 提高z-index确保在所有元素之上 */
432 | z-index: 10000;
433 | max-width: 300px;
434 | word-wrap: break-word;
435 | box-shadow: var(--shadow);
436 | /* 确保tooltip不会捕获鼠标事件 */
437 | pointer-events: none;
438 | }
439 |
440 | /* 右键菜单 */
441 | .context-menu {
442 | /* fixed 相对于视口定位,而不是相对于父容器 */
443 | position: fixed;
444 | width: 200px;
445 | background-color: var(--menu-bg);
446 | border: 1px solid var(--menu-border);
447 | border-radius: 4px;
448 | box-shadow: var(--shadow);
449 | /* 提高 z-index 确保在所有元素之上 */
450 | z-index: 10000;
451 | }
452 |
453 | .menu-item {
454 | padding: 8px 12px;
455 | cursor: pointer;
456 | color: var(--text-color);
457 | }
458 |
459 | .menu-item:hover {
460 | background-color: var(--menu-hover);
461 | }
462 |
463 | .menu-divider {
464 | height: 1px;
465 | background-color: var(--menu-border);
466 | margin: 5px 0;
467 | }
468 |
469 | /* 模态框 */
470 | .modal {
471 | /* width: 400px; */
472 | max-width: 100vw;
473 | max-height: 100vh;
474 | /* 确保模态框填满整个视口高度 */
475 | height: 100vh;
476 | background-color: var(--card-bg);
477 | overflow: hidden;
478 | box-shadow: var(--shadow);
479 | display: flex;
480 | flex-direction: column;
481 | }
482 |
483 | .modal-title-bar {
484 | height: 30px;
485 | background-color: var(--title-bar-bg);
486 | display: flex;
487 | align-items: center;
488 | padding: 0 15px;
489 | color: var(--title-bar-text);
490 | font-weight: 500;
491 | -webkit-app-region: drag;
492 | }
493 |
494 | .modal-content {
495 | /* 让内容区域占据剩余空间 */
496 | flex: 1;
497 | display: flex;
498 | flex-direction: column;
499 | padding: 20px;
500 | overflow-y: auto;
501 | }
502 |
503 | /* 表单 */
504 | .form-group {
505 | margin-bottom: 15px;
506 | }
507 |
508 | .form-input {
509 | width: 100%;
510 | padding: 8px 12px;
511 | border-radius: 4px;
512 | border: 1px solid var(--border-color);
513 | background-color: var(--search-bg);
514 | color: var(--text-color);
515 | }
516 |
517 | .form-select {
518 | width: 100%;
519 | padding: 8px 12px;
520 | border-radius: 4px;
521 | border: 1px solid var(--border-color);
522 | background-color: var(--search-bg);
523 | color: var(--text-color);
524 | }
525 |
526 | /* 按钮组 */
527 | .button-group {
528 | display: flex;
529 | justify-content: flex-end;
530 | gap: 10px;
531 | margin-top: 20px;
532 | }
533 |
534 | .btn {
535 | padding: 8px 16px;
536 | border-radius: 4px;
537 | border: none;
538 | cursor: pointer;
539 | font-weight: 500;
540 | transition: background-color 0.2s;
541 | }
542 |
543 | .btn-primary {
544 | background-color: var(--button-bg);
545 | color: var(--button-text);
546 | }
547 |
548 | .btn-primary:hover {
549 | background-color: var(--button-hover);
550 | }
551 |
552 | .btn-primary:active {
553 | background-color: var(--button-active);
554 | }
555 |
556 | .btn-primary:disabled {
557 | background-color: var(--button-disabled);
558 | cursor: not-allowed;
559 | }
560 |
561 | .btn-secondary {
562 | background-color: var(--cancel-button-bg);
563 | color: var(--cancel-button-text);
564 | }
565 |
566 | .btn-secondary:hover {
567 | background-color: var(--cancel-button-hover);
568 | }
569 |
570 | .btn-secondary:active {
571 | background-color: var(--cancel-button-active);
572 | }
573 |
574 | /* 提示框 */
575 | .toast {
576 | position: fixed;
577 | bottom: 20px;
578 | left: 50%;
579 | transform: translateX(-50%);
580 | background-color: var(--tooltip-bg);
581 | color: var(--tooltip-text);
582 | padding: 10px 20px;
583 | border-radius: 4px;
584 | box-shadow: var(--shadow);
585 | z-index: 1000;
586 | opacity: 0;
587 | transition: opacity 0.3s;
588 | }
589 |
590 | .error-toast {
591 | background-color: var(--error-color);
592 | color: white;
593 | }
594 |
595 | /* 拖拽指示器 */
596 | .drag-indicator {
597 | height: 2px;
598 | background-color: var(--drag-indicator);
599 | margin: 4px 0;
600 | }
601 |
602 | /* 滚动条样式 */
603 | .list-container::-webkit-scrollbar,
604 | .modal-content::-webkit-scrollbar {
605 | width: 8px;
606 | }
607 |
608 | .list-container::-webkit-scrollbar-track,
609 | .modal-content::-webkit-scrollbar-track {
610 | background: var(--bg-color);
611 | }
612 |
613 | .list-container::-webkit-scrollbar-thumb,
614 | .modal-content::-webkit-scrollbar-thumb {
615 | background-color: var(--border-color);
616 | border-radius: 4px;
617 | }
618 |
619 | .list-container::-webkit-scrollbar-thumb:hover,
620 | .modal-content::-webkit-scrollbar-thumb:hover {
621 | background-color: var(--icon-color);
622 | }
--------------------------------------------------------------------------------
/src/renderer/js/renderer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 主窗口渲染进程脚本
3 | * 负责项目列表的渲染、项目操作、搜索过滤、拖放功能等
4 | * 包含用户界面交互逻辑和事件处理
5 | */
6 | document.addEventListener("DOMContentLoaded", () => {
7 | // DOM元素引用
8 | const addBtn = document.getElementById("add-button");
9 | const searchInput = document.getElementById("search-input");
10 | const listContainer = document.getElementById("list-container");
11 | const settingsButton = document.getElementById("settings-button");
12 | const appContainer = document.querySelector(".app-container");
13 |
14 | // 导入i18n模块
15 | const i18n = window.electronAPI.i18n;
16 |
17 | // 当前项目列表(内存中存储)
18 | let currentItems = [];
19 |
20 | // 预先设置与主题匹配的背景色,避免背景闪烁
21 | const setInitialThemeBackground = async () => {
22 | const savedTheme = await window.electronAPI.getThemeConfig();
23 | const isDark = savedTheme === "dark" ||
24 | (savedTheme === "system" && window.matchMedia &&
25 | window.matchMedia("(prefers-color-scheme: dark)").matches);
26 |
27 | // 确保背景色与主题一致,避免闪烁
28 | if (isDark) {
29 | listContainer.style.backgroundColor = "var(--bg-color)";
30 | appContainer.style.backgroundColor = "var(--bg-color)";
31 | } else {
32 | listContainer.style.backgroundColor = "var(--bg-color)";
33 | appContainer.style.backgroundColor = "var(--bg-color)";
34 | }
35 | };
36 |
37 | // 立即应用初始背景色
38 | setInitialThemeBackground();
39 |
40 | // 初始时将列表容器设置为不可见,避免主题应用前闪烁
41 | listContainer.style.visibility = 'hidden';
42 | // 设置占位符,保持布局,避免内容加载后出现跳动
43 | listContainer.innerHTML = '';
44 |
45 | // 初始化UI,保存返回的解绑函数对象
46 | const uiCleanup = window.uiManager.initUI({
47 | containerSelector: ".app-container",
48 | windowType: "main", // 指定窗口类型为主窗口
49 | onUIReady: async () => {
50 | // UI准备好后,加载并显示项目列表
51 | await initPage();
52 | // 显示列表容器
53 | listContainer.style.visibility = 'visible';
54 | }
55 | });
56 |
57 | // 当页面卸载时清理监听器
58 | window.addEventListener('beforeunload', () => {
59 | if (uiCleanup && typeof uiCleanup.unbindAll === 'function') {
60 | uiCleanup.unbindAll();
61 | }
62 | });
63 |
64 | /**
65 | * 事件监听设置部分
66 | */
67 |
68 | // 添加按钮点击事件 - 显示添加项目对话框
69 | addBtn.addEventListener("click", () => {
70 | window.electronAPI.showAddItemDialog();
71 | });
72 |
73 | // 搜索框输入事件 - 实时过滤列表项目
74 | searchInput.addEventListener("input", () => {
75 | filterItems(searchInput.value.toLowerCase());
76 | });
77 |
78 | // 设置按钮点击事件 - 打开独立设置窗口
79 | settingsButton.addEventListener("click", () => {
80 | window.electronAPI.showSettingsWindow();
81 | });
82 |
83 | /**
84 | * 全局键盘事件处理
85 | * - Escape: 关闭窗口
86 | * - Delete: 移除选中项目
87 | * - Enter: 打开选中项目
88 | * - 方向键: 列表导航
89 | * - F12: 打开开发者工具
90 | * - Ctrl+F: 搜索框获得焦点
91 | */
92 | document.addEventListener("keydown", (e) => {
93 | if (e.key === "Escape") {
94 | window.electronAPI.closeMainWindow();
95 | } else if (e.key === "Delete") {
96 | const activeItem = document.querySelector(".list-item.active");
97 | if (activeItem) {
98 | const index = parseInt(activeItem.dataset.index);
99 | removeItem(index);
100 | }
101 | } else if (e.key === "Enter") {
102 | const activeItem = document.querySelector(".list-item.active");
103 | if (activeItem) {
104 | const index = parseInt(activeItem.dataset.index);
105 | window.electronAPI.openItem(currentItems[index]);
106 | }
107 | } else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
108 | navigateList(e.key === "ArrowUp" ? -1 : 1);
109 | } else if (e.key === "F12") {
110 | window.electronAPI.openDevTools();
111 | e.preventDefault();
112 | } else if (e.key === "f" && (e.ctrlKey || e.metaKey)) {
113 | // Ctrl+F (Windows/Linux) 或 Command+F (macOS) 使搜索框获得焦点
114 | searchInput.focus();
115 | e.preventDefault();
116 | }
117 | });
118 |
119 | // 处理拖放文件功能
120 | let draggingItem = false;
121 |
122 | listContainer.addEventListener("dragover", (e) => {
123 | e.preventDefault();
124 | listContainer.classList.add("drag-over");
125 | });
126 |
127 | listContainer.addEventListener("dragleave", () => {
128 | listContainer.classList.remove("drag-over");
129 | });
130 |
131 | listContainer.addEventListener("drop", async (e) => {
132 | e.preventDefault();
133 | listContainer.classList.remove("drag-over");
134 |
135 | // 检查是否是内部拖拽排序操作
136 | if (draggingItem) {
137 | // 是内部拖拽排序,不处理外部文件拖入逻辑
138 | return;
139 | }
140 |
141 | // 使用 webUtils 获取文件路径
142 | const filePath = await window.electronAPI.getFileOrFolderPath(e.dataTransfer.files[0]);
143 | if (!filePath) {
144 | window.uiManager.showToast(await i18n.t("cannot-get-file-path"), true);
145 | return;
146 | }
147 |
148 | // 检查是否已存在
149 | const exists = currentItems.some((item) => item.path === filePath);
150 |
151 | if (exists) {
152 | window.uiManager.showToast(await i18n.t("item-already-exists"));
153 | return;
154 | }
155 |
156 | // 判断项目类型
157 | const itemType = await window.electronAPI.getItemType(filePath);
158 |
159 | // 创建新项目
160 | const newItem = {
161 | type: itemType,
162 | path: filePath,
163 | name: filePath.split("/").pop().split("\\").pop(), // 从路径中提取文件名
164 | };
165 |
166 | // 添加项目并刷新列表
167 | const result = await window.electronAPI.addItem(newItem);
168 | if (result.success) {
169 | await loadItems();
170 | // 如果成功添加,且有返回的索引,则直接使用这个索引
171 | // result.itemIndex 包含了新添加的项目索引
172 | if (result.itemIndex !== undefined) {
173 | // 先刷新列表以确保项目显示
174 | // 选中并滚动到该项目
175 | selectItemByIndex(result.itemIndex);
176 | }
177 | } else {
178 | window.uiManager.showToast(result.message, true);
179 | }
180 | });
181 |
182 | // 初始化页面
183 | async function initPage() {
184 | await loadItems();
185 |
186 | // 添加对列表更新的监听
187 | window.electronAPI.onItemsUpdated(async (newItemIndex) => {
188 | console.log("Items updated, refreshing list...", newItemIndex ? `New item index: ${newItemIndex}` : "");
189 | await loadItems();
190 |
191 | // 如果有新添加的项目索引,选中并滚动到该项目
192 | if (newItemIndex !== undefined) {
193 | selectItemByIndex(newItemIndex);
194 | }
195 | });
196 | }
197 |
198 | // 加载项目列表
199 | async function loadItems() {
200 | currentItems = await window.electronAPI.getItems();
201 | if (currentItems.length > 0) {
202 | document.querySelector(".empty-list-message")?.remove();
203 | renderItems(currentItems);
204 | } else {
205 | // 显示空列表消息
206 | listContainer.innerHTML = `
207 |
点击 + 按钮添加新的项目
208 |
`;
209 |
210 | // 更新空列表消息的翻译
211 | await window.uiUtils.updatePageTexts(i18n);
212 | }
213 | }
214 |
215 | // 渲染项目列表
216 | function renderItems(items) {
217 | listContainer.innerHTML = "";
218 |
219 | items.forEach((item, index) => {
220 | const listItem = document.createElement("div");
221 | listItem.classList.add("list-item");
222 | listItem.dataset.index = index;
223 | listItem.dataset.type = item.type;
224 | listItem.dataset.path = item.path;
225 |
226 | // 设置拖拽属性
227 | listItem.draggable = true;
228 |
229 | // 图标
230 | const icon = getIconForType(item.type);
231 |
232 | listItem.innerHTML = `
233 | ${icon}
234 | ${item.name || item.path}
235 | `;
236 |
237 | // 事件监听
238 | listItem.addEventListener("click", () => {
239 | // 激活选中项
240 | document
241 | .querySelectorAll(".list-item.active")
242 | .forEach((el) => el.classList.remove("active"));
243 | listItem.classList.add("active");
244 | });
245 |
246 | listItem.addEventListener("dblclick", () => {
247 | window.electronAPI.openItem(item);
248 | });
249 |
250 | listContainer.appendChild(listItem);
251 | });
252 |
253 | // 添加拖拽排序功能
254 | setupDragAndSort();
255 | }
256 |
257 | // 根据类型获取图标
258 | function getIconForType(type) {
259 | switch (type) {
260 | case "file":
261 | return "📄";
262 | case "folder":
263 | return "📁";
264 | case "url":
265 | return "🌐";
266 | case "command":
267 | return "⌨️";
268 | default:
269 | return "❓";
270 | }
271 | }
272 |
273 | // 过滤项目
274 | function filterItems(query) {
275 | const filteredItems = query
276 | ? currentItems.filter(
277 | (item) =>
278 | (item.name && item.name.toLowerCase().includes(query)) ||
279 | item.path.toLowerCase().includes(query)
280 | )
281 | : currentItems;
282 |
283 | renderItems(filteredItems);
284 | }
285 |
286 | // 移除项目
287 | async function removeItem(index) {
288 | const result = await window.electronAPI.removeItem(index);
289 | if (result.success) {
290 | await loadItems();
291 | }
292 | }
293 |
294 | // 设置拖拽排序
295 | function setupDragAndSort() {
296 | const items = document.querySelectorAll(".list-item");
297 | let draggedItem = null;
298 | let indicator = document.createElement("div");
299 | indicator.classList.add("drag-indicator");
300 | let dropPosition = null; // 添加变量跟踪放置位置
301 |
302 | // 添加自动滚动相关变量
303 | let autoScrollInterval = null;
304 | const SCROLL_SPEED = 5; // 滚动速度
305 | const SCROLL_THRESHOLD = 50; // 触发自动滚动的阈值(距离容器边缘的像素)
306 |
307 | // 停止自动滚动
308 | const stopAutoScroll = () => {
309 | if (autoScrollInterval) {
310 | clearInterval(autoScrollInterval);
311 | autoScrollInterval = null;
312 | }
313 | };
314 |
315 | items.forEach((item) => {
316 | item.addEventListener("dragstart", (e) => {
317 | draggedItem = item;
318 | item.classList.add("dragging");
319 | e.dataTransfer.setData("text/plain", item.dataset.index);
320 | setTimeout(() => { item.style.opacity = "0.5"; }, 0);
321 | draggingItem = true;
322 | });
323 |
324 | item.addEventListener("dragend", () => {
325 | draggedItem = null;
326 | item.classList.remove("dragging");
327 | item.style.opacity = "1";
328 | indicator.remove();
329 | dropPosition = null; // 重置放置位置
330 | stopAutoScroll(); // 停止自动滚动
331 | draggingItem = false;
332 | });
333 |
334 | item.addEventListener("dragover", (e) => {
335 | e.preventDefault();
336 | if (draggedItem && draggedItem !== item) {
337 | // 计算应该插在当前项的上方还是下方
338 | const rect = item.getBoundingClientRect();
339 | const y = e.clientY - rect.top;
340 | const isBelow = y > rect.height / 2;
341 |
342 | // 移除所有现有的drop-target类
343 | items.forEach((i) => i.classList.remove("drop-before", "drop-after"));
344 |
345 | // 根据放置位置添加相应的类
346 | if (isBelow) {
347 | item.classList.add("drop-after");
348 | dropPosition = { target: item, position: "after" };
349 | } else {
350 | item.classList.add("drop-before");
351 | dropPosition = { target: item, position: "before" };
352 | }
353 |
354 | // 添加指示器
355 | if (isBelow) {
356 | if (item.nextSibling !== indicator) {
357 | item.after(indicator);
358 | }
359 | } else {
360 | if (item.previousSibling !== indicator) {
361 | item.before(indicator);
362 | }
363 | }
364 | }
365 | });
366 | });
367 |
368 | // 当鼠标在列表容器内移动时,处理自动滚动
369 | listContainer.addEventListener("dragover", (e) => {
370 | e.preventDefault();
371 | if (!draggedItem) return;
372 |
373 | const containerRect = listContainer.getBoundingClientRect();
374 | const mouseY = e.clientY;
375 |
376 | // 判断是否需要向上滚动
377 | if (mouseY < containerRect.top + SCROLL_THRESHOLD) {
378 | stopAutoScroll();
379 | autoScrollInterval = setInterval(() => {
380 | listContainer.scrollTop -= SCROLL_SPEED;
381 | }, 16);
382 | }
383 | // 判断是否需要向下滚动
384 | else if (mouseY > containerRect.bottom - SCROLL_THRESHOLD) {
385 | stopAutoScroll();
386 | autoScrollInterval = setInterval(() => {
387 | listContainer.scrollTop += SCROLL_SPEED;
388 | }, 16);
389 | }
390 | });
391 |
392 | // 当鼠标离开列表容器时停止滚动
393 | listContainer.addEventListener("dragleave", () => {
394 | stopAutoScroll();
395 | });
396 |
397 | // 在列表容器上监听drop事件,而不是在每个项目上
398 | listContainer.addEventListener("drop", async (e) => {
399 | e.preventDefault();
400 | stopAutoScroll(); // 确保停止自动滚动
401 |
402 | if (draggedItem && dropPosition) {
403 | // 获取拖拽项的索引
404 | const draggedIndex = parseInt(draggedItem.dataset.index);
405 | const targetIndex = parseInt(dropPosition.target.dataset.index);
406 | let newIndex;
407 |
408 | // 根据放置位置计算新索引
409 | if (dropPosition.position === "after") {
410 | newIndex = targetIndex + 1;
411 | } else {
412 | newIndex = targetIndex;
413 | }
414 |
415 | // 调整索引,考虑拖拽项被移除后的影响
416 | if (draggedIndex < newIndex) {
417 | newIndex--;
418 | }
419 |
420 | // 重新排序
421 | const itemsCopy = [...currentItems];
422 | const [removed] = itemsCopy.splice(draggedIndex, 1);
423 | itemsCopy.splice(newIndex, 0, removed);
424 |
425 | // 更新后端存储
426 | const result = await window.electronAPI.updateItemsOrder(itemsCopy);
427 | if (result.success) {
428 | await loadItems();
429 | console.log("Order updated:", draggedIndex, "->", newIndex);
430 | }
431 | }
432 |
433 | indicator.style.display = "none";
434 | dropPosition = null; // 重置放置位置
435 | });
436 | }
437 |
438 | // 键盘导航列表
439 | function navigateList(direction) {
440 | const items = document.querySelectorAll(".list-item");
441 | const activeItem = document.querySelector(".list-item.active");
442 |
443 | if (!items.length) return;
444 |
445 | if (!activeItem) {
446 | // 如果没有选中项,选择第一个或最后一个
447 | items[direction > 0 ? 0 : items.length - 1].classList.add("active");
448 | return;
449 | }
450 |
451 | // 获取当前索引
452 | const currentIndex = Array.from(items).indexOf(activeItem);
453 | let nextIndex = currentIndex + direction;
454 |
455 | // 边界检查
456 | if (nextIndex < 0) nextIndex = items.length - 1;
457 | if (nextIndex >= items.length) nextIndex = 0;
458 |
459 | // 移除当前活动项
460 | activeItem.classList.remove("active");
461 |
462 | // 设置新的活动项
463 | items[nextIndex].classList.add("active");
464 |
465 | // 确保项目可见
466 | const newActiveItem = items[nextIndex];
467 | newActiveItem.scrollIntoView({ block: "nearest" });
468 | }
469 |
470 | // 根据索引选中项目并确保其可见
471 | function selectItemByIndex(index) {
472 | // 先取消所有选中状态
473 | document.querySelectorAll(".list-item.active").forEach(el => {
474 | el.classList.remove("active");
475 | });
476 |
477 | // 找到对应索引的项目
478 | const targetItem = document.querySelector(`.list-item[data-index="${index}"]`);
479 |
480 | if (targetItem) {
481 | // 选中该项目
482 | targetItem.classList.add("active");
483 |
484 | // 确保项目在视图中可见(滚动到该项目)
485 | targetItem.scrollIntoView({ block: "nearest", behavior: "smooth" });
486 | }
487 | }
488 |
489 | // 把loadItems和removeItem函数暴露到全局,供其他脚本使用
490 | window.appFunctions = {
491 | loadItems,
492 | removeItem
493 | };
494 | });
495 |
--------------------------------------------------------------------------------