From 83011e5d29bee9f5fb4fbf88891b7b6a074b72b5 Mon Sep 17 00:00:00 2001 From: cnb Date: Sun, 30 Aug 2026 01:37:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E6=8F=90=E4=BA=A4=EF=BC=9AWi?= =?UTF-8?q?nUI=203=20=E4=B8=80=E9=94=AE=E8=BF=90=E8=A1=8C=E8=BD=AE?= =?UTF-8?q?=E7=9B=98=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 34 ++ CHANGELOG.md | 43 +++ Directory.Build.props | 9 + OneClickRun.sln | 69 ++++ README.md | 74 ++++ requirements.md | 48 +++ scripts/e2e.ps1 | 329 +++++++++++++++++ scripts/gen-icon.ps1 | 19 + scripts/probe-hotkey.ps1 | 17 + scripts/smoke.ps1 | 85 +++++ src/OneClickRun.App/App.xaml | 27 ++ src/OneClickRun.App/App.xaml.cs | 114 ++++++ src/OneClickRun.App/Assets/AppIcon.png | Bin 0 -> 6331 bytes src/OneClickRun.App/Controls/WheelView.xaml | 24 ++ .../Controls/WheelView.xaml.cs | 241 +++++++++++++ src/OneClickRun.App/Converters/Converters.cs | 39 ++ src/OneClickRun.App/MainWindow.xaml | 44 +++ src/OneClickRun.App/MainWindow.xaml.cs | 95 +++++ src/OneClickRun.App/OneClickRun.App.csproj | 37 ++ src/OneClickRun.App/Pages/AboutPage.xaml | 30 ++ src/OneClickRun.App/Pages/AboutPage.xaml.cs | 37 ++ src/OneClickRun.App/Pages/BehaviorPage.xaml | 54 +++ .../Pages/BehaviorPage.xaml.cs | 209 +++++++++++ src/OneClickRun.App/Pages/CommandsPage.xaml | 55 +++ .../Pages/CommandsPage.xaml.cs | 341 ++++++++++++++++++ src/OneClickRun.App/Pages/GeneralPage.xaml | 30 ++ src/OneClickRun.App/Pages/GeneralPage.xaml.cs | 58 +++ src/OneClickRun.App/Services/AppLogger.cs | 52 +++ src/OneClickRun.App/Services/ColorHelper.cs | 23 ++ src/OneClickRun.App/Services/HotkeyService.cs | 156 ++++++++ src/OneClickRun.App/Services/MessageWindow.cs | 172 +++++++++ src/OneClickRun.App/Services/NativeMethods.cs | 132 +++++++ .../Services/StartupService.cs | 45 +++ src/OneClickRun.App/Services/ThemeService.cs | 22 ++ .../Services/TransparentBackdrop.cs | 69 ++++ .../Services/TrayIconService.cs | 130 +++++++ .../Services/WheelController.cs | 248 +++++++++++++ src/OneClickRun.App/WheelWindow.xaml | 9 + src/OneClickRun.App/WheelWindow.xaml.cs | 121 +++++++ src/OneClickRun.App/app.manifest | 15 + src/OneClickRun.Core/Data/GlyphCatalog.cs | 47 +++ src/OneClickRun.Core/Data/WheelPalette.cs | 19 + .../Logic/DoubleClickState.cs | 41 +++ src/OneClickRun.Core/Logic/HotkeyFormat.cs | 125 +++++++ src/OneClickRun.Core/Logic/WheelMath.cs | 50 +++ src/OneClickRun.Core/Models/AppSettings.cs | 56 +++ src/OneClickRun.Core/Models/CommandItem.cs | 29 ++ src/OneClickRun.Core/Models/Enums.cs | 44 +++ .../Models/HotkeyDefinition.cs | 25 ++ src/OneClickRun.Core/OneClickRun.Core.csproj | 6 + .../Services/CommandRunner.cs | 98 +++++ .../Services/SettingsStore.cs | 66 ++++ .../CommandRunnerTests.cs | 127 +++++++ .../DoubleClickStateTests.cs | 56 +++ .../HotkeyFormatTests.cs | 53 +++ .../OneClickRun.Core.Tests.csproj | 15 + .../SettingsStoreTests.cs | 79 ++++ .../OneClickRun.Core.Tests/WheelMathTests.cs | 81 +++++ 58 files changed, 4373 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 Directory.Build.props create mode 100644 OneClickRun.sln create mode 100644 README.md create mode 100644 requirements.md create mode 100644 scripts/e2e.ps1 create mode 100644 scripts/gen-icon.ps1 create mode 100644 scripts/probe-hotkey.ps1 create mode 100644 scripts/smoke.ps1 create mode 100644 src/OneClickRun.App/App.xaml create mode 100644 src/OneClickRun.App/App.xaml.cs create mode 100644 src/OneClickRun.App/Assets/AppIcon.png create mode 100644 src/OneClickRun.App/Controls/WheelView.xaml create mode 100644 src/OneClickRun.App/Controls/WheelView.xaml.cs create mode 100644 src/OneClickRun.App/Converters/Converters.cs create mode 100644 src/OneClickRun.App/MainWindow.xaml create mode 100644 src/OneClickRun.App/MainWindow.xaml.cs create mode 100644 src/OneClickRun.App/OneClickRun.App.csproj create mode 100644 src/OneClickRun.App/Pages/AboutPage.xaml create mode 100644 src/OneClickRun.App/Pages/AboutPage.xaml.cs create mode 100644 src/OneClickRun.App/Pages/BehaviorPage.xaml create mode 100644 src/OneClickRun.App/Pages/BehaviorPage.xaml.cs create mode 100644 src/OneClickRun.App/Pages/CommandsPage.xaml create mode 100644 src/OneClickRun.App/Pages/CommandsPage.xaml.cs create mode 100644 src/OneClickRun.App/Pages/GeneralPage.xaml create mode 100644 src/OneClickRun.App/Pages/GeneralPage.xaml.cs create mode 100644 src/OneClickRun.App/Services/AppLogger.cs create mode 100644 src/OneClickRun.App/Services/ColorHelper.cs create mode 100644 src/OneClickRun.App/Services/HotkeyService.cs create mode 100644 src/OneClickRun.App/Services/MessageWindow.cs create mode 100644 src/OneClickRun.App/Services/NativeMethods.cs create mode 100644 src/OneClickRun.App/Services/StartupService.cs create mode 100644 src/OneClickRun.App/Services/ThemeService.cs create mode 100644 src/OneClickRun.App/Services/TransparentBackdrop.cs create mode 100644 src/OneClickRun.App/Services/TrayIconService.cs create mode 100644 src/OneClickRun.App/Services/WheelController.cs create mode 100644 src/OneClickRun.App/WheelWindow.xaml create mode 100644 src/OneClickRun.App/WheelWindow.xaml.cs create mode 100644 src/OneClickRun.App/app.manifest create mode 100644 src/OneClickRun.Core/Data/GlyphCatalog.cs create mode 100644 src/OneClickRun.Core/Data/WheelPalette.cs create mode 100644 src/OneClickRun.Core/Logic/DoubleClickState.cs create mode 100644 src/OneClickRun.Core/Logic/HotkeyFormat.cs create mode 100644 src/OneClickRun.Core/Logic/WheelMath.cs create mode 100644 src/OneClickRun.Core/Models/AppSettings.cs create mode 100644 src/OneClickRun.Core/Models/CommandItem.cs create mode 100644 src/OneClickRun.Core/Models/Enums.cs create mode 100644 src/OneClickRun.Core/Models/HotkeyDefinition.cs create mode 100644 src/OneClickRun.Core/OneClickRun.Core.csproj create mode 100644 src/OneClickRun.Core/Services/CommandRunner.cs create mode 100644 src/OneClickRun.Core/Services/SettingsStore.cs create mode 100644 tests/OneClickRun.Core.Tests/CommandRunnerTests.cs create mode 100644 tests/OneClickRun.Core.Tests/DoubleClickStateTests.cs create mode 100644 tests/OneClickRun.Core.Tests/HotkeyFormatTests.cs create mode 100644 tests/OneClickRun.Core.Tests/OneClickRun.Core.Tests.csproj create mode 100644 tests/OneClickRun.Core.Tests/SettingsStoreTests.cs create mode 100644 tests/OneClickRun.Core.Tests/WheelMathTests.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d0c57f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# ===== 构建输出 ===== +[Bb]in/ +[Oo]bj/ +dist/ +out/ +artifacts/ +publish/ +TestResults/ + +# ===== IDE / 编辑器 ===== +.vs/ +.idea/ +.vscode/ +*.user +*.suo +*.userosscache +*.sln.docstates + +# ===== 临时与日志 ===== +*.tmp +*.log +*.bak +*.corrupt-* +Thumbs.db +Desktop.ini +.DS_Store + +# ===== NuGet / 测试产物 ===== +*.nupkg +packages/ +*.trx +*.coverage +.coverage +*.coveragexml diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0537e35 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,43 @@ +# Changelog + +本项目的所有重要变更都会记录在此文件中。 + +格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/), +版本号遵循 [语义化版本 SemVer](https://semver.org/lang/zh-CN/)。 + +## [Unreleased] + +### Added + +- 新增系统托盘图标:左键单击托盘图标显示设置界面;右键菜单提供 + 「启用/关闭轮盘」(控制轮盘全局开关)、「设置」「退出」三个选项 +- 新增端到端测试脚本 scripts\e2e.ps1,覆盖 11 个场景:点击/长按/按住呼出、 + Esc 关闭、单击与双击执行指令、滑动选择、鼠标位置显示、脚本执行、 + 开机启动注册表写入、轮盘全局开关 +- 快捷指令「运行脚本」支持 .cmd / .bat 批处理文件(通过 cmd /c 执行), + 文件选择器同步移除不再支持的 .psm1 + +### Changed + +- 点击设置窗口关闭按钮不再退出程序,改为最小化到系统托盘 + +### Fixed + +- 修复全局快捷键注册失败(Win32 错误 1408):RegisterHotKey 的 hWnd 必须 + 属于调用线程,现在把注册/注销封送到快捷键消息窗口线程执行 +- 修复开机启动写入到错误注册表位置:Run 键路径缺少反斜杠 +- 修复「长按时长」被滑块初始化覆盖:BehaviorPage 构造时设置 Slider.Value + 触发 ValueChanged,导致已保存的 400ms 被改回 200ms + +## [0.1.0] - 2026-08-30 + +### Added + +- 首个可用版本:WinUI 3 轮盘快捷启动工具 +- 全局快捷键呼出轮盘(默认 Ctrl+Alt+Q),点击显示/长按显示/按住显示三种呼出方式 +- 单击/双击/滑动三种指令选择方式,轮盘可显示在屏幕中心或鼠标位置 +- 快捷指令:打开软件、打开文件夹、打开网址、运行 PowerShell 脚本 +- 设置主界面:常规、呼出与快捷键、快捷指令、关于四个页面 +- 开机启动、轮盘全局开关、日间/夜间/跟随系统主题 +- OneClickRun.Core 纯逻辑层与 62 个单元测试 +- 冒烟测试脚本 scripts\smoke.ps1 diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..0b4a734 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,9 @@ + + + latest + enable + enable + 0.1.0 + 一键运行 OneClickRun + + diff --git a/OneClickRun.sln b/OneClickRun.sln new file mode 100644 index 0000000..5c3740b --- /dev/null +++ b/OneClickRun.sln @@ -0,0 +1,69 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneClickRun.Core", "src\OneClickRun.Core\OneClickRun.Core.csproj", "{FD7B90E4-12AB-4C6F-A906-305CDE27B15E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneClickRun.Core.Tests", "tests\OneClickRun.Core.Tests\OneClickRun.Core.Tests.csproj", "{5E9E323A-8D8E-4F7F-AADF-3179932DF0D2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneClickRun.App", "src\OneClickRun.App\OneClickRun.App.csproj", "{AB82B886-1BB7-4430-94E9-104C40348F33}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FD7B90E4-12AB-4C6F-A906-305CDE27B15E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD7B90E4-12AB-4C6F-A906-305CDE27B15E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD7B90E4-12AB-4C6F-A906-305CDE27B15E}.Debug|x64.ActiveCfg = Debug|Any CPU + {FD7B90E4-12AB-4C6F-A906-305CDE27B15E}.Debug|x64.Build.0 = Debug|Any CPU + {FD7B90E4-12AB-4C6F-A906-305CDE27B15E}.Debug|x86.ActiveCfg = Debug|Any CPU + {FD7B90E4-12AB-4C6F-A906-305CDE27B15E}.Debug|x86.Build.0 = Debug|Any CPU + {FD7B90E4-12AB-4C6F-A906-305CDE27B15E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD7B90E4-12AB-4C6F-A906-305CDE27B15E}.Release|Any CPU.Build.0 = Release|Any CPU + {FD7B90E4-12AB-4C6F-A906-305CDE27B15E}.Release|x64.ActiveCfg = Release|Any CPU + {FD7B90E4-12AB-4C6F-A906-305CDE27B15E}.Release|x64.Build.0 = Release|Any CPU + {FD7B90E4-12AB-4C6F-A906-305CDE27B15E}.Release|x86.ActiveCfg = Release|Any CPU + {FD7B90E4-12AB-4C6F-A906-305CDE27B15E}.Release|x86.Build.0 = Release|Any CPU + {5E9E323A-8D8E-4F7F-AADF-3179932DF0D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E9E323A-8D8E-4F7F-AADF-3179932DF0D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E9E323A-8D8E-4F7F-AADF-3179932DF0D2}.Debug|x64.ActiveCfg = Debug|Any CPU + {5E9E323A-8D8E-4F7F-AADF-3179932DF0D2}.Debug|x64.Build.0 = Debug|Any CPU + {5E9E323A-8D8E-4F7F-AADF-3179932DF0D2}.Debug|x86.ActiveCfg = Debug|Any CPU + {5E9E323A-8D8E-4F7F-AADF-3179932DF0D2}.Debug|x86.Build.0 = Debug|Any CPU + {5E9E323A-8D8E-4F7F-AADF-3179932DF0D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E9E323A-8D8E-4F7F-AADF-3179932DF0D2}.Release|Any CPU.Build.0 = Release|Any CPU + {5E9E323A-8D8E-4F7F-AADF-3179932DF0D2}.Release|x64.ActiveCfg = Release|Any CPU + {5E9E323A-8D8E-4F7F-AADF-3179932DF0D2}.Release|x64.Build.0 = Release|Any CPU + {5E9E323A-8D8E-4F7F-AADF-3179932DF0D2}.Release|x86.ActiveCfg = Release|Any CPU + {5E9E323A-8D8E-4F7F-AADF-3179932DF0D2}.Release|x86.Build.0 = Release|Any CPU + {AB82B886-1BB7-4430-94E9-104C40348F33}.Debug|Any CPU.ActiveCfg = Debug|x64 + {AB82B886-1BB7-4430-94E9-104C40348F33}.Debug|Any CPU.Build.0 = Debug|x64 + {AB82B886-1BB7-4430-94E9-104C40348F33}.Debug|x64.ActiveCfg = Debug|x64 + {AB82B886-1BB7-4430-94E9-104C40348F33}.Debug|x64.Build.0 = Debug|x64 + {AB82B886-1BB7-4430-94E9-104C40348F33}.Debug|x86.ActiveCfg = Debug|x86 + {AB82B886-1BB7-4430-94E9-104C40348F33}.Debug|x86.Build.0 = Debug|x86 + {AB82B886-1BB7-4430-94E9-104C40348F33}.Release|Any CPU.ActiveCfg = Release|x64 + {AB82B886-1BB7-4430-94E9-104C40348F33}.Release|Any CPU.Build.0 = Release|x64 + {AB82B886-1BB7-4430-94E9-104C40348F33}.Release|x64.ActiveCfg = Release|x64 + {AB82B886-1BB7-4430-94E9-104C40348F33}.Release|x64.Build.0 = Release|x64 + {AB82B886-1BB7-4430-94E9-104C40348F33}.Release|x86.ActiveCfg = Release|x86 + {AB82B886-1BB7-4430-94E9-104C40348F33}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {FD7B90E4-12AB-4C6F-A906-305CDE27B15E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {5E9E323A-8D8E-4F7F-AADF-3179932DF0D2} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {AB82B886-1BB7-4430-94E9-104C40348F33} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md new file mode 100644 index 0000000..5127173 --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# 一键运行 OneClickRun + +一键运行工具:以游戏快捷轮盘的形式,通过全局快捷键呼出一个圆形扇区轮盘, +快速启动软件、打开文件夹、打开网址或运行脚本。基于 WinUI 3 + C#(.NET 8), +面向 Windows 10 / Windows 11。 + +## 功能特性 + +- 圆形轮盘,扇区数量与快捷指令数量一致(上限 12 个,默认 5 个演示指令) +- 全局快捷键呼出/隐藏轮盘(默认 Ctrl+Alt+Q) +- 三种呼出方式:点击显示、长按显示、按住显示(松开即隐藏) +- 三种选择方式:单击选择、双击选择、滑动选择(仅按住显示时有效) +- 轮盘显示位置:屏幕中心 / 鼠标位置 +- 指令类型:打开软件、打开文件夹、打开网址、运行 PowerShell 脚本(.ps1/.cmd/.bat) +- 主设置界面:常规(开机启动、轮盘全局开关、主题)、行为、快捷指令、关于 +- 系统托盘:点击设置窗口关闭按钮最小化到托盘,左键点击托盘图标恢复设置窗口, + 右键菜单提供「启用/关闭轮盘」「设置」「退出」 +- 日间/夜间/跟随系统主题 +- 轮盘点击外部、Esc 或 8 秒超时自动关闭 + +## 目录结构 + + One_click_run/ + ├── OneClickRun.sln + ├── Directory.Build.props + ├── requirements.md # 开发需求文档 + ├── scripts/ + │ ├── smoke.ps1 # 冒烟测试:启动→呼出轮盘→Esc 关闭 + │ ├── e2e.ps1 # 端到端测试:11 个场景 + │ ├── gen-icon.ps1 # 生成应用图标 + │ └── probe-hotkey.ps1 # 探测系统快捷键占用 + ├── src/ + │ ├── OneClickRun.Core/ # 纯逻辑:设置、指令执行、轮盘几何、快捷键解析 + │ └── OneClickRun.App/ # WinUI 3 应用:窗口、页面、控件、服务 + └── tests/ + └── OneClickRun.Core.Tests/ # 单元测试(xUnit) + +## 环境要求 + +- Windows 10(19041 及以上)或 Windows 11 +- .NET 8 SDK +- Windows App SDK 2.4.0(NuGet 自动还原,应用已配置自包含 Windows App SDK) + +## 构建与运行 + + # 构建 + dotnet build OneClickRun.sln + + # 运行(调试构建输出) + src\OneClickRun.App\bin\x64\Debug\net8.0-windows10.0.19041.0\win-x64\OneClickRun.exe + +## 使用说明 + +- 默认快捷键 Ctrl+Alt+Q 呼出轮盘;在「呼出与快捷键」页可修改 +- 「常规」页可设置开机启动、轮盘全局开关与主题模式 +- 「快捷指令」页可新增/编辑/删除/排序指令,并支持「测试运行」 +- 点击设置窗口右上角关闭按钮不会退出程序,而是最小化到系统托盘; + 左键单击托盘图标恢复设置窗口,右键菜单可切换轮盘全局开关、打开设置或退出程序 +- 设置保存在 %APPDATA%\OneClickRun\settings.json +- 日志位于 %APPDATA%\OneClickRun\logs\app.log + +## 测试 + + # 单元测试(62 个) + dotnet test OneClickRun.sln + + # 冒烟测试(需要图形会话) + powershell -NoProfile -ExecutionPolicy Bypass -File scripts\smoke.ps1 + + # 端到端测试(11 个场景;需要图形会话) + powershell -NoProfile -ExecutionPolicy Bypass -File scripts\e2e.ps1 + +注意:e2e.ps1 会临时写入并恢复 %APPDATA%\OneClickRun\settings.json、 +临时写入并恢复开机启动注册表项,并会启动/关闭记事本等测试进程。 diff --git a/requirements.md b/requirements.md new file mode 100644 index 0000000..f008bfd --- /dev/null +++ b/requirements.md @@ -0,0 +1,48 @@ +# 一键运行工具开发需求 + +## 概述 + +一键运行工具是采用游戏中常见的快捷轮盘的形式,快速启动软件、打开文件夹、打开链接等快捷指令。 + +## 运行平台与技术栈 + +- 运行平台:Windows平台(Win10,Win11) +- 技术栈:WinUI3 + C# + +## 功能需求 + +- 轮盘:在屏幕上一个圆环区域分割出多个扇形区域,每个区域分配一个操作或指令 +- 轮盘的呼出和隐藏:默认轮盘隐藏,通过指定快捷键呼出轮盘,选择轮盘上的指令后,自动隐藏轮盘 +- 轮盘支持的指令:打开软件、打开指定文件夹、打开指定网站、运行指定的脚本(powershell)等 +- 主界面:提供一个主界面,用于设置软件的各项功能,具体设置参考后续 + +## 快捷指令 + +- 打开软件:指定程序路径 +- 打开文件夹:指定文件夹路径 +- 打开网址:以默认浏览器打开指定网址 +- 运行脚本:指定脚本文件 + +## 系统设置 + +系统的设置项包括: +- 开机启动开关 +- 轮盘全局开关:关闭时,不再呼出轮盘 +- 轮盘显示位置:可选项包括: + - 屏幕中心:呼出轮盘时在屏幕中心显示 + - 鼠标位置:呼出轮盘时在鼠标位置显示 +- 全局快捷键:呼出轮盘的快捷键,全局有效 +- 呼出轮盘操作: + - 点击显示:按一次快捷键即显示轮盘 + - 长按显示:长按快捷键一定时间显示轮盘 + - 按住显示:按住快捷键时显示轮盘,松开即隐藏 +- 轮盘指令选择操作: + - 单击选择:单击轮盘指令选择 + - 双击选择:双击轮盘指令选择 + - 滑动选择:选择鼠标滑动方向所指的指令(仅在按住显示轮盘时有效,以松开快捷键时,鼠标所在轮盘的方位选择指定的指令 +- 日间/黑夜模式切换:日间/黑夜/跟随系统 +- 轮盘指令设置,设置轮盘的各项指令 + +## UI风格 + +以现代简约风格为主,支持日间/黑夜模式 \ No newline at end of file diff --git a/scripts/e2e.ps1 b/scripts/e2e.ps1 new file mode 100644 index 0000000..32ad47b --- /dev/null +++ b/scripts/e2e.ps1 @@ -0,0 +1,329 @@ +# End-to-end test: summon modes (click / long-press / hold), execute-by-click, swipe select, mouse position, global switch. +param( + [string]$Exe = "src/OneClickRun.App/bin/x64/Debug/net8.0-windows10.0.19041.0/win-x64/OneClickRun.exe" +) +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent $PSScriptRoot +$exePath = Join-Path $root $Exe +if (-not (Test-Path $exePath)) { throw "App not found: $exePath" } + +$settingsDir = Join-Path $env:APPDATA "OneClickRun" +$settingsFile = Join-Path $settingsDir "settings.json" +$settingsBackup = $null +if (Test-Path $settingsFile) { + $settingsBackup = $settingsFile + ".e2e-backup" + Copy-Item $settingsFile $settingsBackup -Force +} + +Add-Type @" +using System; +using System.Text; +using System.Runtime.InteropServices; +public class E2EWin { + public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc cb, IntPtr lParam); + [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd); + [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid); + [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder sb, int max); + [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect); + [DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo); + [DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y); + [DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint dwData, UIntPtr dwExtraInfo); + [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; } +} +"@ + +$script:Proc = $null +$script:TargetPid = 0 +$script:WheelHwnd = [IntPtr]::Zero +$script:WheelRect = New-Object E2EWin+RECT +$script:WheelVisible = $false + +function Get-WheelInfo { + $script:WheelVisible = $false + $script:WheelHwnd = [IntPtr]::Zero + $cb = [E2EWin+EnumWindowsProc]{ + param($h, $l) + $wpid = 0 + [void][E2EWin]::GetWindowThreadProcessId($h, [ref]$wpid) + if ($wpid -eq $script:TargetPid) { + $sb = New-Object System.Text.StringBuilder 256 + [void][E2EWin]::GetWindowText($h, $sb, 256) + if ($sb.ToString() -like '*OneClickRun Wheel*') { + $script:WheelHwnd = $h + $rect = New-Object E2EWin+RECT + [void][E2EWin]::GetWindowRect($h, [ref]$rect) + $script:WheelRect = $rect + $script:WheelVisible = [E2EWin]::IsWindowVisible($h) + } + } + return $true + } + [void][E2EWin]::EnumWindows($cb, [IntPtr]::Zero) + return $script:WheelVisible +} + +function Write-Settings([hashtable]$s) { + $s | ConvertTo-Json -Depth 10 | Set-Content -Path $settingsFile -Encoding UTF8 +} + +function Start-App { + $script:Proc = Start-Process -FilePath $exePath -PassThru + $script:TargetPid = $script:Proc.Id + Start-Sleep -Seconds 4 + if ($script:Proc.HasExited) { throw "App exited early with code $($script:Proc.ExitCode)" } +} + +function Stop-App { + if ($script:Proc -and -not $script:Proc.HasExited) { Stop-Process -Id $script:TargetPid -Force } + Start-Sleep -Milliseconds 400 +} + +function Key-Down([byte]$vk) { [E2EWin]::keybd_event($vk, 0, 0, [UIntPtr]::Zero) } +function Key-Up([byte]$vk) { [E2EWin]::keybd_event($vk, 0, 2, [UIntPtr]::Zero) } + +function Press-Hotkey { Key-Down 0x11; Key-Down 0x12; Key-Down 0x51; Start-Sleep -Milliseconds 120; Key-Up 0x51; Key-Up 0x12; Key-Up 0x11 } +function Hold-Hotkey-Down { Key-Down 0x11; Key-Down 0x12; Key-Down 0x51 } +function Hold-Hotkey-Up { Key-Up 0x51; Key-Up 0x12; Key-Up 0x11 } +function Press-Esc { Key-Down 0x1B; Start-Sleep -Milliseconds 60; Key-Up 0x1B } + +function Assert([bool]$cond, [string]$msg) { if (-not $cond) { throw "Assertion failed: $msg" } } + +function Get-SectorPoint([int]$index, [int]$count) { + $w = $script:WheelRect.Right - $script:WheelRect.Left + $scale = $w / 560.0 + $cx = ($script:WheelRect.Left + $script:WheelRect.Right) / 2.0 + $cy = ($script:WheelRect.Top + $script:WheelRect.Bottom) / 2.0 + $span = 360.0 / $count + $mid = ($index + 0.5) * $span + $rad = $mid * [Math]::PI / 180.0 + $r = 177.5 * $scale + return @{ X = [int][Math]::Round($cx + $r * [Math]::Sin($rad)); Y = [int][Math]::Round($cy - $r * [Math]::Cos($rad)) } +} + +function Click-At([int]$x, [int]$y) { + [void][E2EWin]::SetCursorPos($x, $y) + Start-Sleep -Milliseconds 120 + [E2EWin]::mouse_event(0x0002, 0, 0, 0, [UIntPtr]::Zero) + Start-Sleep -Milliseconds 80 + [E2EWin]::mouse_event(0x0004, 0, 0, 0, [UIntPtr]::Zero) + Start-Sleep -Milliseconds 500 +} + +function Click-Once([int]$x, [int]$y) { + [void][E2EWin]::SetCursorPos($x, $y) + Start-Sleep -Milliseconds 60 + [E2EWin]::mouse_event(0x0002, 0, 0, 0, [UIntPtr]::Zero) + Start-Sleep -Milliseconds 40 + [E2EWin]::mouse_event(0x0004, 0, 0, 0, [UIntPtr]::Zero) + Start-Sleep -Milliseconds 30 +} + +function New-NotepadCommand { @{ Name = "Notepad"; ActionType = 0; Target = (Join-Path $env:WINDIR 'System32\notepad.exe'); IconGlyph = "A"; ColorIndex = 0 } } +function New-CalcCommand { @{ Name = "Calc"; ActionType = 0; Target = (Join-Path $env:WINDIR 'System32\calc.exe'); IconGlyph = "B"; ColorIndex = 1 } } +function New-BingCommand { @{ Name = "Bing"; ActionType = 2; Target = "https://www.bing.com"; IconGlyph = "C"; ColorIndex = 2 } } + +function New-BaseSettings([int]$summon, [int]$selection, [int]$position, [bool]$enabled, [bool]$startWithWindows = $false) { + return @{ + SchemaVersion = 1; StartWithWindows = $startWithWindows; WheelEnabled = $enabled + WheelPosition = $position; Hotkey = @{ Modifiers = 3; Key = 81 } + SummonMode = $summon; LongPressDelayMs = 400; SelectionMode = $selection; ThemeMode = 2 + Commands = @((New-NotepadCommand), (New-CalcCommand), (New-BingCommand)) + } +} + +$failed = 0 +$results = New-Object System.Collections.Generic.List[string] + +function Run-Test([string]$name, [scriptblock]$body) { + try { + & $body + $results.Add("PASS $name") + Write-Host "PASS $name" + } catch { + $script:failed++ + $results.Add("FAIL $name :: $($_.Exception.Message)") + Write-Host "FAIL $name :: $($_.Exception.Message)" -ForegroundColor Red + } finally { + Stop-App + } +} + +try { + Run-Test "click summon: show then toggle-hide" { + Remove-Item $settingsFile -ErrorAction SilentlyContinue + Start-App + Press-Hotkey; Start-Sleep -Milliseconds 500 + Assert (Get-WheelInfo) "hotkey did not show wheel" + Press-Hotkey; Start-Sleep -Milliseconds 500 + Assert (-not (Get-WheelInfo)) "second hotkey press did not hide wheel" + } + + Run-Test "click summon: Esc dismisses wheel" { + Remove-Item $settingsFile -ErrorAction SilentlyContinue + Start-App + Press-Hotkey; Start-Sleep -Milliseconds 500 + Assert (Get-WheelInfo) "hotkey did not show wheel" + Press-Esc; Start-Sleep -Milliseconds 500 + Assert (-not (Get-WheelInfo)) "wheel still visible after Esc" + } + + Run-Test "click execute: sector 0 starts Notepad" { + Remove-Item $settingsFile -ErrorAction SilentlyContinue + Start-App + Press-Hotkey; Start-Sleep -Milliseconds 500 + Assert (Get-WheelInfo) "hotkey did not show wheel" + $p = Get-SectorPoint 0 5 + Click-At $p.X $p.Y + Start-Sleep -Milliseconds 800 + Assert (-not (Get-WheelInfo)) "wheel not hidden after execute" + $np = Get-Process notepad -ErrorAction SilentlyContinue + Assert ($null -ne $np) "Notepad process was not started" + $np | Stop-Process -Force + } + + Run-Test "long-press summon: shows after 400ms and stays after release" { + Write-Settings (New-BaseSettings 1 0 0 $true) + Start-App + Hold-Hotkey-Down + Start-Sleep -Milliseconds 300 + Assert (-not (Get-WheelInfo)) "wheel shown too early (300ms)" + Start-Sleep -Milliseconds 250 + Assert (Get-WheelInfo) "wheel not shown after 400ms long press" + Hold-Hotkey-Up + Start-Sleep -Milliseconds 400 + Assert (Get-WheelInfo) "wheel disappeared after release (should stay)" + Press-Esc; Start-Sleep -Milliseconds 400 + Assert (-not (Get-WheelInfo)) "Esc did not close wheel" + } + + Run-Test "hold summon: visible while held, hidden on release" { + Write-Settings (New-BaseSettings 2 0 0 $true) + Start-App + Hold-Hotkey-Down + Start-Sleep -Milliseconds 400 + Assert (Get-WheelInfo) "wheel not visible while hotkey held" + Hold-Hotkey-Up + Start-Sleep -Milliseconds 400 + Assert (-not (Get-WheelInfo)) "wheel still visible after release" + } + + Run-Test "swipe select: hold + mouse up, release executes Notepad" { + Write-Settings (New-BaseSettings 2 2 0 $true) + Start-App + Hold-Hotkey-Down + Start-Sleep -Milliseconds 400 + Assert (Get-WheelInfo) "wheel not visible while hotkey held" + $p = Get-SectorPoint 0 3 + [void][E2EWin]::SetCursorPos($p.X, $p.Y) + Start-Sleep -Milliseconds 150 + Hold-Hotkey-Up + Start-Sleep -Milliseconds 800 + Assert (-not (Get-WheelInfo)) "wheel still visible after release" + $np = Get-Process notepad -ErrorAction SilentlyContinue + Assert ($null -ne $np) "swipe did not execute Notepad" + $np | Stop-Process -Force + } + + Run-Test "wheel position: mouse cursor" { + Write-Settings (New-BaseSettings 0 0 1 $true) + Start-App + [void][E2EWin]::SetCursorPos(400, 300) + Start-Sleep -Milliseconds 150 + Press-Hotkey; Start-Sleep -Milliseconds 500 + Assert (Get-WheelInfo) "hotkey did not show wheel" + $cx = ($script:WheelRect.Left + $script:WheelRect.Right) / 2.0 + $cy = ($script:WheelRect.Top + $script:WheelRect.Bottom) / 2.0 + $d = [Math]::Sqrt(($cx - 400) * ($cx - 400) + ($cy - 300) * ($cy - 300)) + Assert ($d -lt 12) "wheel center offset from cursor by $([int]$d)px" + Press-Esc + } + + Run-Test "double-click selection: first click pending, second click executes" { + Write-Settings (New-BaseSettings 0 1 0 $true) + Start-App + Press-Hotkey; Start-Sleep -Milliseconds 500 + Assert (Get-WheelInfo) "hotkey did not show wheel" + $p = Get-SectorPoint 0 3 + Click-Once $p.X $p.Y + Start-Sleep -Milliseconds 150 + Assert (Get-WheelInfo) "wheel hidden after first click in double-click mode" + $np = Get-Process notepad -ErrorAction SilentlyContinue + Assert ($null -eq $np) "Notepad started after single click in double-click mode" + Click-Once $p.X $p.Y + Start-Sleep -Milliseconds 600 + Assert (-not (Get-WheelInfo)) "wheel not hidden after double click" + $np = Get-Process notepad -ErrorAction SilentlyContinue + Assert ($null -ne $np) "Notepad not started after double click" + $np | Stop-Process -Force + } + + Run-Test "autostart: registry Run value written at launch" { + Write-Settings (New-BaseSettings 0 0 0 $true $true) + Start-App + $runKey = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' + $prev = (Get-ItemProperty -Path $runKey -Name OneClickRun -ErrorAction SilentlyContinue).OneClickRun + try { + $cur = (Get-ItemProperty -Path $runKey -Name OneClickRun -ErrorAction SilentlyContinue).OneClickRun + Assert ($null -ne $cur) "autostart registry value missing" + Assert ($cur -like '*OneClickRun*') "unexpected autostart value: $cur" + } finally { + if ($null -eq $prev) { + Remove-ItemProperty -Path $runKey -Name OneClickRun -ErrorAction SilentlyContinue + } else { + Set-ItemProperty -Path $runKey -Name OneClickRun -Value $prev + } + } + } + + Run-Test "script execute: cmd and ps1 sectors run" { + $cmdPath = Join-Path $env:TEMP "ocr-e2e-run.cmd" + $ps1Path = Join-Path $env:TEMP "ocr-e2e-run.ps1" + $cmdMarker = Join-Path $env:TEMP "ocr-e2e-cmd-marker.txt" + $ps1Marker = Join-Path $env:TEMP "ocr-e2e-ps1-marker.txt" + Remove-Item $cmdMarker, $ps1Marker -ErrorAction SilentlyContinue + $cmdLine = 'echo ok > "' + $cmdMarker + '"' + $ps1Line = "Set-Content -Path '" + $ps1Marker + "' -Value ok" + Set-Content -Path $cmdPath -Value @('@echo off', $cmdLine) -Encoding ASCII + Set-Content -Path $ps1Path -Value $ps1Line -Encoding ASCII + + $settings = New-BaseSettings 0 0 0 $true + $settings.Commands = @( + @{ Name = "RunCmd"; ActionType = 3; Target = $cmdPath; IconGlyph = "D"; ColorIndex = 0 }, + @{ Name = "RunPs1"; ActionType = 3; Target = $ps1Path; IconGlyph = "E"; ColorIndex = 1 } + ) + Write-Settings $settings + Start-App + + Press-Hotkey; Start-Sleep -Milliseconds 500 + Assert (Get-WheelInfo) "hotkey did not show wheel" + $p0 = Get-SectorPoint 0 2 + Click-At $p0.X $p0.Y + Start-Sleep -Milliseconds 1200 + Assert (Test-Path $cmdMarker) "cmd script did not run" + Assert (-not (Get-WheelInfo)) "wheel not hidden after cmd execute" + + Press-Hotkey; Start-Sleep -Milliseconds 500 + Assert (Get-WheelInfo) "hotkey did not re-show wheel" + $p1 = Get-SectorPoint 1 2 + Click-At $p1.X $p1.Y + Start-Sleep -Milliseconds 1200 + Assert (Test-Path $ps1Marker) "ps1 script did not run" + + Remove-Item $cmdMarker, $ps1Marker, $cmdPath, $ps1Path -ErrorAction SilentlyContinue + } + + Run-Test "global switch off: wheel cannot be summoned" { + Write-Settings (New-BaseSettings 0 0 0 $false) + Start-App + Press-Hotkey; Start-Sleep -Milliseconds 600 + Assert (-not (Get-WheelInfo)) "wheel summoned while global switch off" + } +} finally { + Stop-App + if ($settingsBackup) { Move-Item $settingsBackup $settingsFile -Force } else { Remove-Item $settingsFile -ErrorAction SilentlyContinue } +} + +Write-Host "" +Write-Host ("=== E2E RESULT: {0}/{1} passed ===" -f ($results.Count - $failed), $results.Count) +exit $failed diff --git a/scripts/gen-icon.ps1 b/scripts/gen-icon.ps1 new file mode 100644 index 0000000..525dabc --- /dev/null +++ b/scripts/gen-icon.ps1 @@ -0,0 +1,19 @@ +Add-Type -AssemblyName System.Drawing +$bmp = New-Object System.Drawing.Bitmap(256, 256) +$g = [System.Drawing.Graphics]::FromImage($bmp) +$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias +$colors = @('#E5484D', '#F76B15', '#F5A623', '#30A46C', '#12A594', '#0091FF', '#5E5CE6', '#BF5AF2') +$rect = New-Object System.Drawing.Rectangle(8, 8, 240, 240) +for ($i = 0; $i -lt 8; $i++) { + $brush = New-Object System.Drawing.SolidBrush([System.Drawing.ColorTranslator]::FromHtml($colors[$i])) + $g.FillPie($brush, $rect, ($i * 45 - 90), 46) + $brush.Dispose() +} +$white = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::White) +$g.FillEllipse($white, 78, 78, 100, 100) +$white.Dispose() +$g.Dispose() +$out = Join-Path (Split-Path -Parent $PSScriptRoot) 'src/OneClickRun.App/Assets/AppIcon.png' +$bmp.Save($out, [System.Drawing.Imaging.ImageFormat]::Png) +$bmp.Dispose() +Write-Host "icon saved to $out" diff --git a/scripts/probe-hotkey.ps1 b/scripts/probe-hotkey.ps1 new file mode 100644 index 0000000..ccbe252 --- /dev/null +++ b/scripts/probe-hotkey.ps1 @@ -0,0 +1,17 @@ +Add-Type @' +using System; +using System.Runtime.InteropServices; +public class HKTest { + [DllImport("user32.dll", SetLastError=true)] public static extern bool RegisterHotKey(IntPtr hWnd, int id, uint mods, uint vk); + [DllImport("user32.dll")] public static extern bool UnregisterHotKey(IntPtr hWnd, int id); + [DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow(); +} +'@ +$h = [HKTest]::GetConsoleWindow() +Write-Host "hwnd=$h" +foreach ($spec in @(@('Ctrl+Alt+Q', 3, 0x51), @('Ctrl+Alt+K', 3, 0x4B), @('Ctrl+Alt+W', 3, 0x57), @('Ctrl+Shift+Space', 6, 0x20))) { + $ok = [HKTest]::RegisterHotKey($h, 1, [uint32]$spec[1], [uint32]$spec[2]) + $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() + Write-Host ($spec[0] + ' => ' + $ok + ' err=' + $err) + if ($ok) { [void][HKTest]::UnregisterHotKey($h, 1) } +} diff --git a/scripts/smoke.ps1 b/scripts/smoke.ps1 new file mode 100644 index 0000000..01da138 --- /dev/null +++ b/scripts/smoke.ps1 @@ -0,0 +1,85 @@ +# 冒烟测试:启动应用 → 验证主窗口 → Ctrl+Alt+Q 呼出轮盘 → Esc 关闭轮盘 → 退出 +param( + [string]$Exe = "src/OneClickRun.App/bin/x64/Debug/net8.0-windows10.0.19041.0/win-x64/OneClickRun.exe" +) +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent $PSScriptRoot +$exePath = Join-Path $root $Exe +if (-not (Test-Path $exePath)) { throw "找不到应用: $exePath" } + +Add-Type @" +using System; +using System.Text; +using System.Runtime.InteropServices; +public class WinEnum { + public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc cb, IntPtr lParam); + [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd); + [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid); + [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder sb, int max); + [DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo); +} +"@ + +$script:Wins = New-Object System.Collections.Generic.List[object] +$script:TargetPid = 0 +$cb = [WinEnum+EnumWindowsProc]{ + param($h, $l) + $wpid = 0 + [void][WinEnum]::GetWindowThreadProcessId($h, [ref]$wpid) + if ($wpid -eq $script:TargetPid) { + $sb = New-Object System.Text.StringBuilder 256 + [void][WinEnum]::GetWindowText($h, $sb, 256) + $script:Wins.Add([pscustomobject]@{ Visible = [WinEnum]::IsWindowVisible($h); Title = $sb.ToString() }) + } + return $true +} +function Show-Windows { + $script:Wins.Clear() + [void][WinEnum]::EnumWindows($cb, [IntPtr]::Zero) + $script:Wins | ForEach-Object { Write-Host (" visible={0} title='{1}'" -f $_.Visible, $_.Title) } +} + +$proc = Start-Process -FilePath $exePath -PassThru +$script:TargetPid = $proc.Id +try { +Write-Host "已启动 PID=$($proc.Id)" +Start-Sleep -Seconds 5 +if ($proc.HasExited) { throw "进程提前退出,退出码 $($proc.ExitCode)" } +Write-Host "=== 启动后窗口 ===" +Show-Windows +if (-not ($script:Wins | Where-Object { $_.Title -like '*一键运行*' })) { throw "未找到主窗口" } + +# 发送 Ctrl+Alt+Q +[WinEnum]::keybd_event(0x11, 0, 0, [UIntPtr]::Zero) +[WinEnum]::keybd_event(0x12, 0, 0, [UIntPtr]::Zero) +[WinEnum]::keybd_event(0x51, 0, 0, [UIntPtr]::Zero) +Start-Sleep -Milliseconds 120 +[WinEnum]::keybd_event(0x51, 0, 2, [UIntPtr]::Zero) +[WinEnum]::keybd_event(0x12, 0, 2, [UIntPtr]::Zero) +[WinEnum]::keybd_event(0x11, 0, 2, [UIntPtr]::Zero) +Start-Sleep -Seconds 1 + +Write-Host "=== 按 Ctrl+Alt+Q 后 ===" +Show-Windows +$wheel = $script:Wins | Where-Object { $_.Title -like '*Wheel*' } +if (-not $wheel) { throw "热键未能呼出轮盘窗口" } +if (-not $wheel.Visible) { throw "轮盘窗口存在但不可见" } +Write-Host "PASS: 轮盘已呼出" + +# Esc 关闭 +[WinEnum]::keybd_event(0x1B, 0, 0, [UIntPtr]::Zero) +Start-Sleep -Milliseconds 80 +[WinEnum]::keybd_event(0x1B, 0, 2, [UIntPtr]::Zero) +Start-Sleep -Milliseconds 800 +Write-Host "=== 按 Esc 后 ===" +Show-Windows +$wheel2 = $script:Wins | Where-Object { $_.Title -like '*Wheel*' } +if ($wheel2 -and $wheel2.Visible) { throw "Esc 后轮盘仍可见" } +Write-Host "PASS: 轮盘已关闭" + +Write-Host "SMOKE OK" +} +finally { + if (-not $proc.HasExited) { Stop-Process -Id $proc.Id -Force } +} diff --git a/src/OneClickRun.App/App.xaml b/src/OneClickRun.App/App.xaml new file mode 100644 index 0000000..1a444e9 --- /dev/null +++ b/src/OneClickRun.App/App.xaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/OneClickRun.App/App.xaml.cs b/src/OneClickRun.App/App.xaml.cs new file mode 100644 index 0000000..90c23b8 --- /dev/null +++ b/src/OneClickRun.App/App.xaml.cs @@ -0,0 +1,114 @@ +using Microsoft.UI.Xaml; +using OneClickRun.App.Services; +using OneClickRun.Core.Models; +using OneClickRun.Core.Services; + +namespace OneClickRun.App; + +public partial class App : Application +{ + private Mutex? _singleInstanceMutex; + + public static App Instance => (App)Current; + + public AppLogger Log { get; private set; } = null!; + public SettingsStore Store { get; private set; } = null!; + public AppSettings Settings { get; private set; } = null!; + public HotkeyService Hotkeys { get; private set; } = null!; + public WheelController Wheel { get; private set; } = null!; + public StartupService Startup { get; private set; } = null!; + public TrayIconService Tray { get; private set; } = null!; + public MainWindow Main { get; private set; } = null!; + + public App() + { + InitializeComponent(); + UnhandledException += OnUnhandledException; + } + + protected override void OnLaunched(LaunchActivatedEventArgs args) + { + _singleInstanceMutex = new Mutex(initiallyOwned: true, "OneClickRun.SingleInstance", out bool isNewInstance); + if (!isNewInstance) + { + _ = NativeMethods.MessageBox(IntPtr.Zero, "一键运行已在运行,请在系统托盘查找图标,或使用全局快捷键呼出轮盘。", "一键运行", 0x40); + Exit(); + return; + } + + Log = new AppLogger(); + Log.Info("应用启动"); + Store = new SettingsStore(SettingsStore.DefaultDirectory); + Settings = Store.Load(); + Log.Info("设置加载完成"); + Startup = new StartupService(); + Hotkeys = new HotkeyService(); + Hotkeys.Start(); + Log.Info("快捷键服务启动完成"); + Wheel = new WheelController(this); + Log.Info("轮盘控制器创建完成"); + Main = new MainWindow(this); + Log.Info("主窗口创建完成"); + Tray = new TrayIconService(this); + Log.Info("系统托盘图标创建完成"); + Main.Activate(); + Log.Info("主窗口已激活"); + ApplySettingsToSystem(); + Log.Info("设置已应用"); + } + + /// 把内存中的设置应用到系统:主题、开机启动、快捷键、轮盘。 + public void ApplySettingsToSystem() + { + ThemeService.Apply(Settings.ThemeMode, Main, Wheel.Window); + Startup.Apply(Settings.StartWithWindows); + Hotkeys.ApplySettings(Settings.WheelEnabled, Settings.Hotkey); + Wheel.RefreshSettings(); + } + + public void SaveSettings() + { + try + { + Store.Save(Settings); + } + catch (Exception ex) + { + LogError("保存设置失败", ex); + } + } + + public void ReportError(string message) + { + Log.Info("报告错误:" + message); + Main?.ShowError(message); + } + + /// 退出整个应用:关闭托盘图标、轮盘与主窗口并结束进程。 + public void ExitApplication() + { + try { Tray?.Dispose(); } catch (Exception ex) { LogError("销毁托盘图标失败", ex); } + try { Wheel?.Dispose(); } catch (Exception ex) { LogError("销毁轮盘失败", ex); } + try { Main?.CloseForExit(); } catch (Exception ex) { LogError("关闭主窗口失败", ex); } + try { Hotkeys?.Dispose(); } catch (Exception ex) { LogError("释放快捷键失败", ex); } + Exit(); + } + + public void LogInfo(string message) => Log.Info(message); + + public void LogError(string context, Exception ex) => Log.Error(context, ex); + + private void OnUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) + { + Log.Error("未处理异常", e.Exception); + e.Handled = true; + try + { + Main?.ShowError("发生未处理的错误:" + e.Message); + } + catch + { + // 忽略二次异常 + } + } +} diff --git a/src/OneClickRun.App/Assets/AppIcon.png b/src/OneClickRun.App/Assets/AppIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..d9ca387926c95c0ec4436cba9d528f8ed522a262 GIT binary patch literal 6331 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 + + + + + + + + + diff --git a/src/OneClickRun.App/Controls/WheelView.xaml.cs b/src/OneClickRun.App/Controls/WheelView.xaml.cs new file mode 100644 index 0000000..97ccf46 --- /dev/null +++ b/src/OneClickRun.App/Controls/WheelView.xaml.cs @@ -0,0 +1,241 @@ +using Microsoft.UI.Text; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Shapes; +using OneClickRun.App.Services; +using OneClickRun.Core.Data; +using OneClickRun.Core.Logic; +using OneClickRun.Core.Models; +using Windows.Foundation; +using Windows.UI; +using Path = Microsoft.UI.Xaml.Shapes.Path; + +namespace OneClickRun.App.Controls; + +/// +/// 扇区轮盘控件:Path 绘制扇形 + 图标 + 名称;悬停高亮、点击/双击/方位选择均由上层驱动。 +/// 角度约定:0° 在 12 点钟方向,顺时针增大。 +/// +public sealed partial class WheelView : UserControl +{ + private const double Center = 280; + private const double OuterRadius = 278; + private const double InnerRadius = 76; + private static readonly FontFamily IconFontFamily = new("Segoe Fluent Icons, Segoe MDL2 Assets"); + + private IReadOnlyList _items = Array.Empty(); + private readonly List _sectors = new(); + private int _hoverIndex = -1; + private int _pendingIndex = -1; + private int _pressedIndex = -1; + + public int Count => _items.Count; + + public event Action? SectorClicked; + + /// 点击轮盘外空白区域(由窗口层处理关闭)。 + public event Action? DismissRequested; + + public WheelView() + { + InitializeComponent(); + } + + public void SetItems(IReadOnlyList items) + { + _items = items; + _hoverIndex = -1; + _pendingIndex = -1; + _pressedIndex = -1; + Rebuild(); + } + + public void SetHoverIndex(int index) + { + if (index == _hoverIndex) return; + _hoverIndex = index; + UpdateVisuals(); + } + + public void SetPendingIndex(int index) + { + _pendingIndex = index; + UpdateVisuals(); + } + + public void ClearPending() => SetPendingIndex(-1); + + private void Rebuild() + { + LayerCanvas.Children.Clear(); + _sectors.Clear(); + + if (_items.Count == 0) + { + CenterTitle.Text = "未配置指令"; + CenterHint.Text = "请到设置中添加快捷指令"; + return; + } + + CenterTitle.Text = "一键运行"; + CenterHint.Text = ""; + + double span = 360.0 / _items.Count; + for (int i = 0; i < _items.Count; i++) + { + var item = _items[i]; + var sector = CreateSector(i, span); + LayerCanvas.Children.Add(sector); + _sectors.Add(sector); + + double mid = (i + 0.5) * span; + var iconPos = WheelMath.PolarPoint(Center, Center, (InnerRadius + OuterRadius) / 2 - 16, mid); + var icon = new FontIcon + { + Glyph = item.IconGlyph, + FontFamily = IconFontFamily, + FontSize = 20, + Foreground = new SolidColorBrush(Color.FromArgb(255, 255, 255, 255)), + Width = 32, + Height = 32, + }; + Canvas.SetLeft(icon, iconPos.X - 16); + Canvas.SetTop(icon, iconPos.Y - 16); + LayerCanvas.Children.Add(icon); + + var textPos = WheelMath.PolarPoint(Center, Center, (InnerRadius + OuterRadius) / 2 + 24, mid); + var label = new TextBlock + { + Text = item.Name, + FontSize = 13, + FontWeight = FontWeights.SemiBold, + Foreground = new SolidColorBrush(Color.FromArgb(255, 255, 255, 255)), + TextTrimming = TextTrimming.CharacterEllipsis, + TextAlignment = TextAlignment.Center, + MaxWidth = 110, + }; + label.Measure(new Size(110, 40)); + Canvas.SetLeft(label, textPos.X - label.DesiredSize.Width / 2); + Canvas.SetTop(label, textPos.Y - label.DesiredSize.Height / 2); + LayerCanvas.Children.Add(label); + } + + UpdateVisuals(); + } + + private Path CreateSector(int index, double span) + { + var path = new Path + { + Stroke = SeparatorBrush, + StrokeThickness = 1.5, + }; + + if (_items.Count == 1) + { + path.Data = new EllipseGeometry { Center = new Point(Center, Center), RadiusX = OuterRadius, RadiusY = OuterRadius }; + return path; + } + + double start = index * span; + double end = (index + 1) * span; + var outerStart = ToPoint(WheelMath.PolarPoint(Center, Center, OuterRadius, start)); + var outerEnd = ToPoint(WheelMath.PolarPoint(Center, Center, OuterRadius, end)); + var innerEnd = ToPoint(WheelMath.PolarPoint(Center, Center, InnerRadius, end)); + var innerStart = ToPoint(WheelMath.PolarPoint(Center, Center, InnerRadius, start)); + + var figure = new PathFigure { StartPoint = outerStart, IsClosed = true }; + figure.Segments.Add(new ArcSegment + { + Point = outerEnd, + Size = new Size(OuterRadius, OuterRadius), + IsLargeArc = span > 180, + SweepDirection = SweepDirection.Clockwise, + }); + figure.Segments.Add(new LineSegment { Point = innerEnd }); + figure.Segments.Add(new ArcSegment + { + Point = innerStart, + Size = new Size(InnerRadius, InnerRadius), + IsLargeArc = span > 180, + SweepDirection = SweepDirection.Counterclockwise, + }); + + var geometry = new PathGeometry(); + geometry.Figures.Add(figure); + path.Data = geometry; + return path; + } + + private static Point ToPoint((double X, double Y) p) => new(p.X, p.Y); + + private SolidColorBrush SeparatorBrush => ActualTheme == ElementTheme.Dark + ? new SolidColorBrush(Color.FromArgb(0x59, 255, 255, 255)) + : new SolidColorBrush(Color.FromArgb(0x59, 0, 0, 0)); + + private void UpdateVisuals() + { + for (int i = 0; i < _sectors.Count; i++) + { + var color = ColorHelper.FromHex(WheelPalette.Get(_items[i].ColorIndex)); + bool active = i == _hoverIndex || i == _pendingIndex; + byte alpha = active ? (byte)0xEE : (byte)0xB8; + _sectors[i].Fill = new SolidColorBrush(Color.FromArgb(alpha, color.R, color.G, color.B)); + _sectors[i].StrokeThickness = i == _pendingIndex ? 3 : i == _hoverIndex ? 2.5 : 1.5; + } + CenterHint.Text = _hoverIndex >= 0 && _hoverIndex < _items.Count ? _items[_hoverIndex].Name : ""; + } + + private void OnPointerMoved(object sender, PointerRoutedEventArgs e) + { + if (TryGetSector(e, out int index)) SetHoverIndex(index); + } + + private void OnPointerPressed(object sender, PointerRoutedEventArgs e) + { + _pressedIndex = TryGetSector(e, out int index) ? index : -1; + } + + private void OnPointerReleased(object sender, PointerRoutedEventArgs e) + { + bool inSector = TryGetSector(e, out int index); + if (inSector && index == _pressedIndex && index >= 0) + { + SectorClicked?.Invoke(index); + } + else if (!inSector && _pressedIndex < 0 && IsOutsideWheel(e)) + { + DismissRequested?.Invoke(); + } + _pressedIndex = -1; + } + + private void OnPointerExited(object sender, PointerRoutedEventArgs e) + { + if (_pressedIndex < 0) SetHoverIndex(-1); + } + + private bool TryGetSector(PointerRoutedEventArgs e, out int index) + { + index = -1; + if (_items.Count == 0) return false; + var p = e.GetCurrentPoint(this).Position; + double dx = p.X - Center; + double dy = p.Y - Center; + double dist = Math.Sqrt(dx * dx + dy * dy); + if (dist < InnerRadius || dist > OuterRadius + 4) return false; + index = WheelMath.GetSectorIndex(_items.Count, WheelMath.AngleFromVector(dx, dy)); + return true; + } + + private bool IsOutsideWheel(PointerRoutedEventArgs e) + { + if (_items.Count == 0) return true; + var p = e.GetCurrentPoint(this).Position; + double dx = p.X - Center; + double dy = p.Y - Center; + return Math.Sqrt(dx * dx + dy * dy) > OuterRadius + 12; + } +} diff --git a/src/OneClickRun.App/Converters/Converters.cs b/src/OneClickRun.App/Converters/Converters.cs new file mode 100644 index 0000000..38fdbf9 --- /dev/null +++ b/src/OneClickRun.App/Converters/Converters.cs @@ -0,0 +1,39 @@ +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Media; +using OneClickRun.App.Services; +using OneClickRun.Core.Data; +using OneClickRun.Core.Models; + +namespace OneClickRun.App.Converters; + +/// 调色板索引 → 画刷(列表里的图标颜色点)。 +public sealed class ColorIndexToBrushConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + int index = value is int i ? i : 0; + return new SolidColorBrush(ColorHelper.FromHex(WheelPalette.Get(index))); + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) => 0; +} + +/// 指令类型 → 中文标签。 +public sealed class ActionTypeToTextConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + return value is ActionType t + ? t switch + { + ActionType.App => "软件", + ActionType.Folder => "文件夹", + ActionType.Url => "网址", + ActionType.Script => "脚本", + _ => "", + } + : ""; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) => ActionType.App; +} diff --git a/src/OneClickRun.App/MainWindow.xaml b/src/OneClickRun.App/MainWindow.xaml new file mode 100644 index 0000000..3aac99b --- /dev/null +++ b/src/OneClickRun.App/MainWindow.xaml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/OneClickRun.App/MainWindow.xaml.cs b/src/OneClickRun.App/MainWindow.xaml.cs new file mode 100644 index 0000000..6886a59 --- /dev/null +++ b/src/OneClickRun.App/MainWindow.xaml.cs @@ -0,0 +1,95 @@ +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using OneClickRun.App.Pages; +using Windows.Graphics; + +namespace OneClickRun.App; + +/// 设置主窗口:NavigationView 四页 + 底部错误提示条。 +public sealed partial class MainWindow : Window +{ + private readonly App _app; + private bool _allowClose; + private readonly GeneralPage _generalPage; + private readonly BehaviorPage _behaviorPage; + private readonly CommandsPage _commandsPage; + private readonly AboutPage _aboutPage; + + public MainWindow(App app) + { + _app = app; + InitializeComponent(); + Title = "一键运行 - 设置"; + SystemBackdrop = new MicaBackdrop(); + AppWindow.Resize(new SizeInt32(1040, 720)); + try + { + var iconPath = Path.Combine(AppContext.BaseDirectory, "Assets", "AppIcon.png"); + if (File.Exists(iconPath)) AppWindow.SetIcon(iconPath); + } + catch + { + // 图标缺失不影响运行 + } + + _generalPage = new GeneralPage(); + _behaviorPage = new BehaviorPage(); + _commandsPage = new CommandsPage(); + _aboutPage = new AboutPage(); + + Nav.SelectedItem = Nav.MenuItems[0]; + PageHost.Content = _generalPage; + + AppWindow.Closing += OnAppWindowClosing; + Closed += (_, _) => _app.Hotkeys.Dispose(); + } + + private void Nav_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args) + { + if (args.SelectedItem is NavigationViewItem item && item.Tag is string tag) + { + PageHost.Content = tag switch + { + "general" => _generalPage, + "behavior" => _behaviorPage, + "commands" => _commandsPage, + "about" => _aboutPage, + _ => PageHost.Content, + }; + } + } + + /// 点击关闭按钮时取消关闭并最小化到系统托盘。 + private void OnAppWindowClosing(AppWindow sender, AppWindowClosingEventArgs args) + { + if (_allowClose) return; + args.Cancel = true; + AppWindow.Hide(); + _app.LogInfo("设置窗口已最小化到系统托盘"); + } + + /// 从系统托盘恢复并激活设置窗口。 + public void ShowSettings() + { + if (!AppWindow.IsVisible) AppWindow.Show(); + Activate(); + } + + /// 真正关闭主窗口(退出应用时由 App 调用)。 + public void CloseForExit() + { + _allowClose = true; + Close(); + } + + /// 在设置窗口底部弹出错误提示(必要时先激活窗口)。 + public void ShowError(string message) + { + ErrorBar.Message = message; + ErrorBar.IsOpen = true; + if (!AppWindow.IsVisible) AppWindow.Show(); + Activate(); + } +} diff --git a/src/OneClickRun.App/OneClickRun.App.csproj b/src/OneClickRun.App/OneClickRun.App.csproj new file mode 100644 index 0000000..0309cbb --- /dev/null +++ b/src/OneClickRun.App/OneClickRun.App.csproj @@ -0,0 +1,37 @@ + + + WinExe + net8.0-windows10.0.19041.0 + 10.0.17763.0 + OneClickRun.App + OneClickRun + true + + true + false + None + true + x64;x86;ARM64 + win-x64;win-x86;win-arm64 + win-x64 + app.manifest + + + + + + + + + + + + + + + + + + + + diff --git a/src/OneClickRun.App/Pages/AboutPage.xaml b/src/OneClickRun.App/Pages/AboutPage.xaml new file mode 100644 index 0000000..9aa2a70 --- /dev/null +++ b/src/OneClickRun.App/Pages/AboutPage.xaml @@ -0,0 +1,30 @@ + + + + + + + + + + 以全局快捷键呼出圆形扇区轮盘,快速打开软件、文件夹、网址或运行 PowerShell 脚本。 + 支持点击显示 / 长按显示 / 按住显示三种呼出方式,单击 / 双击 / 滑动三种选择方式。 + + + + + + + +