diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ee972e2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,67 @@ +name: Release + +on: + push: + branches: + - dev-chinese_localization + workflow_dispatch: + +jobs: + build: + name: Build & Release + runs-on: windows-latest + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Get Version + id: version + shell: pwsh + run: | + $ver = Select-String -Path Sources/NoSleep/NoSleep.csproj -Pattern '(.*?)' | ForEach-Object { $_.Matches.Groups[1].Value } + Write-Host "Version=$ver" + echo "version=$ver" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + + - name: Restore dependencies + run: dotnet restore Sources/NoSleep/NoSleep.csproj + + - name: Build (net48 Release) + run: msbuild Sources/NoSleep/NoSleep.csproj /p:Configuration=Release /p:TargetFramework=net48 /restore + + - name: Pack Artifact + shell: pwsh + run: | + Compress-Archive -Path Sources/NoSleep/bin/Release/net48/* -DestinationPath NoSleep-${{ steps.version.outputs.version }}-net48.zip + + - name: Create Release Tag + id: tag + shell: bash + run: | + VERSION="${{ steps.version.outputs.version }}" + TAG="v${VERSION}-zh-CN" + echo "tag=$TAG" >> $GITHUB_OUTPUT + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.tag.outputs.tag }} + name: v${{ steps.version.outputs.version }} (zh-CN Localization) + body: | + ## NoSleep v${{ steps.version.outputs.version }} — 简体中文版 + + - 基于上游 v${{ steps.version.outputs.version }} 版本 + - 新增简体中文 (zh-CN) 本地化支持 + - 构建目标: .NET Framework 4.8 + files: NoSleep-${{ steps.version.outputs.version }}-net48.zip + draft: false + prerelease: false diff --git a/.trae/documents/chinese_localization_plan.md b/.trae/documents/chinese_localization_plan.md new file mode 100644 index 0000000..0315aea --- /dev/null +++ b/.trae/documents/chinese_localization_plan.md @@ -0,0 +1,200 @@ +# NoSleep 中文本地化(汉化)方案 + +## 概述 + +采用 .NET 标准 i18n 机制(.resx 卫星程序集)为 NoSleep 添加简体中文(`zh-CN`)本地化支持。英文作为默认/中性语言保留,运行时根据系统 `CurrentUICulture` 自动选择语言;同时翻译全部项目文档。 + +## 当前状态分析 + +### 国际化现状 +- **无任何 i18n 支持**:所有用户可见字符串均为英文硬编码在源码中 +- [Properties/Resources.resx](file:///Users/shen/Studio/Code/NoSleep/Sources/NoSleep/Properties/Resources.resx) 仅含 2 个图标(`TrayIcon`、`TrayIconInactive`),无字符串资源 +- [ConfigureAppsForm.resx](file:///Users/shen/Studio/Code/NoSleep/Sources/NoSleep/ConfigureAppsForm.resx) 为空(仅有 schema) +- [Properties/Resources.Designer.cs](file:///Users/shen/Studio/Code/NoSleep/Sources/NoSleep/Properties/Resources.Designer.cs) 已包含完整的 `ResourceManager` 基础设施,可直接扩展字符串属性 + +### 项目约束(来自 [CONTRIBUTING.md](file:///Users/shen/Studio/Code/NoSleep/CONTRIBUTING.md)) +- **避免外部依赖**:本方案不引入任何 NuGet 包,仅用 .NET 内置资源机制 +- **单一可执行文件**:卫星程序集在 .NET 8.0 单文件发布中会被打入 bundle,符合约束 +- **跨框架兼容**:卫星程序集机制同时支持 net48 和 net8.0-windows +- **保持精简**:资源查找开销可忽略不计 + +### 不应本地化的项 +| 项 | 原因 | +|---|---| +| `Settings.AppName`("NoSleep") | 用于 Mutex 名称、品牌名,保持不变 | +| `Settings.AppStartupName`("NoSleep") | 用于自启动快捷方式文件名 `.lnk`,本地化会导致切换语言后遗留孤立快捷方式 | +| `Settings.AppMutexGuid` | 技术标识符 | +| 托盘图标 `Text`(= AppName) | 品牌名,显示为 "NoSleep" | +| MessageBox 标题(使用 AppName) | 品牌名 | +| 代码注释 | 非用户可见,保持英文(遵循项目惯例) | + +## 提议变更 + +### 1. 资源文件 + +#### 1.1 [Properties/Resources.resx](file:///Users/shen/Studio/Code/NoSleep/Sources/NoSleep/Properties/Resources.resx) — 添加英文字符串(中性/默认) +在现有图标条目后,新增所有用户可见字符串的 `` 条目,值为英文原文。这些作为 fallback 默认值。 + +#### 1.2 新建 `Properties/Resources.zh-CN.resx` — 简体中文翻译 +与 `Resources.resx` 结构相同(schema + resheader),包含相同的 `` 条目但值为中文翻译。构建时自动生成 `zh-CN/NoSleep.resources.dll` 卫星程序集。 + +#### 1.3 [Properties/Resources.Designer.cs](file:///Users/shen/Studio/Code/NoSleep/Sources/NoSleep/Properties/Resources.Designer.cs) — 添加字符串属性 +在现有 `TrayIcon`/`TrayIconInactive` 图标属性之后,为每个字符串键添加 `internal static string` 属性,模式如下: +```csharp +internal static string TrayMenu_Close { + get { return ResourceManager.GetString("TrayMenu_Close", resourceCulture); } +} +``` + +### 2. 项目文件 [NoSleep.csproj](file:///Users/shen/Studio/Code/NoSleep/Sources/NoSleep/NoSleep.csproj) +在 `` 条目之后,添加: +```xml + + Resources.resx + +``` +SDK 风格项目会自动识别文件名中的 `zh-CN` 文化后缀并生成对应的卫星程序集。 + +### 3. 代码改动(用 `Properties.Resources.XXX` 替换硬编码字符串) + +#### 3.1 [TrayIcon.cs](file:///Users/shen/Studio/Code/NoSleep/Sources/NoSleep/TrayIcon.cs#L146-L183) — `InitializeComponent` 方法 +| 行 | 原文 | 替换为 | +|---|---|---| +| 146 | `"Close"` | `Resources.TrayMenu_Close` | +| 149 | `"Autostart at login"` | `Resources.TrayMenu_Autostart` | +| 152 | `"Should we start when you log in?"` | `Resources.Tooltip_Autostart` | +| 156 | `"Remember enabled state"` | `Resources.TrayMenu_RememberState` | +| 159 | `"Should we remember the enabled state between restarts?"` | `Resources.Tooltip_RememberState` | +| 163 | `"Configure apps to monitor"` | `Resources.TrayMenu_ConfigureApps` | +| 166 | `"Configure apps to keep the screen on when they are running."` | `Resources.Tooltip_ConfigureApps` | +| 170 | `"Enabled"` | `Resources.TrayMenu_Enabled` | +| 173 | `"Are we enabled right now?"` | `Resources.Tooltip_Enabled` | +| 177 | `"Keep screen on"` | `Resources.TrayMenu_KeepScreenOn` | +| 180 | `"If display should be kept always on in addition to keeping the system on."` | `Resources.Tooltip_KeepScreenOn` | + +#### 3.2 [Program.cs](file:///Users/shen/Studio/Code/NoSleep/Sources/NoSleep/Program.cs#L19) +```csharp +// 原: +$"{Properties.Settings.Default.AppName} instance is already running." +// 改为: +string.Format(Resources.Msg_InstanceRunning, Properties.Settings.Default.AppName) +``` +说明:使用 `string.Format` 以支持 `{0}` 占位符,保持 `AppName` 作为品牌名注入。 + +#### 3.3 [Tools.cs](file:///Users/shen/Studio/Code/NoSleep/Sources/NoSleep/Tools.cs#L49) +```csharp +// 原 (AutostartDisable): +$"Wasn't able to remove autostart shortcut from '{autostartPath}'. Error: {e.Message}" +// 改为: +string.Format(Resources.Msg_AutostartRemoveFailed, autostartPath, e.Message) + +// 原 (AutostartEnable): +$"Wasn't able to create autostart shortcut at '{autostartPath}'. Error: {e.Message}" +// 改为: +string.Format(Resources.Msg_AutostartCreateFailed, autostartPath, e.Message) +``` + +#### 3.4 [ConfigureAppsForm.cs](file:///Users/shen/Studio/Code/NoSleep/Sources/NoSleep/ConfigureAppsForm.cs) +| 行 | 原文 | 替换为 | +|---|---|---| +| 31 | `"Executable\|*.exe\|All files\|*.*"` | `Resources.FileDlg_FilterExecutable + "\|*.exe\|" + Resources.FileDlg_FilterAllFiles + "\|*.*"` | +| 32 | `"Select application executable"` | `Resources.FileDlg_Title` | +| 53 | `"Apps watching is enabled"` | `Resources.Btn_AppsWatchingEnabled` | +| 53 | `"Apps watching is DISABLED ❌"` | `Resources.Btn_AppsWatchingDisabled` | + +需要在文件顶部添加 `using NoSleep.Properties;`(若尚未引用)。 + +#### 3.5 [ConfigureAppsForm.Designer.cs](file:///Users/shen/Studio/Code/NoSleep/Sources/NoSleep/ConfigureAppsForm.Designer.cs) +将 `InitializeComponent` 中的硬编码字符串替换为资源引用: +| 行 | 原文 | 替换为 | +|---|---|---| +| 66 | `"Name"` | `Properties.Resources.Col_Name` | +| 75 | `"Exe Path"` | `Properties.Resources.Col_ExePath` | +| 87 | `"➕ Add"` | `Properties.Resources.Btn_Add` | +| 99 | `"➖ Remove"` | `Properties.Resources.Btn_Remove` | +| 112 | `"Enable"` | `Properties.Resources.Btn_Enable` | +| 125 | `"Close"` | `Properties.Resources.Btn_Close` | +| 144 | `"Configure Applications to monitor"` | `Properties.Resources.Form_Title` | + +注意:Designer.cs 中引用需使用完整命名空间 `Properties.Resources.XXX` 或在文件顶部添加 `using NoSleep.Properties;`。 + +### 4. 文档翻译 + +为三个文档创建中文版本,并在英文原文顶部添加语言切换链接: + +| 英文文档(保留) | 中文文档(新建) | +|---|---| +| [readme.md](file:///Users/shen/Studio/Code/NoSleep/readme.md) | `readme.zh-CN.md` | +| [BUILD.md](file:///Users/shen/Studio/Code/NoSleep/BUILD.md) | `BUILD.zh-CN.md` | +| [CONTRIBUTING.md](file:///Users/shen/Studio/Code/NoSleep/CONTRIBUTING.md) | `CONTRIBUTING.zh-CN.md` | + +每个英文文档顶部添加: +```markdown +> 🌐 Languages: [English](readme.md) | [简体中文](readme.zh-CN.md) +``` +每个中文文档顶部添加: +```markdown +> 🌐 语言:[English](readme.md) | [简体中文](readme.zh-CN.md) +``` + +## 字符串清单(完整) + +| 键 | 英文原文 | 中文翻译 | +|---|---|---| +| `TrayMenu_Close` | Close | 关闭 | +| `TrayMenu_Autostart` | Autostart at login | 登录时自动启动 | +| `TrayMenu_RememberState` | Remember enabled state | 记住启用状态 | +| `TrayMenu_ConfigureApps` | Configure apps to monitor | 配置监视的应用程序 | +| `TrayMenu_Enabled` | Enabled | 已启用 | +| `TrayMenu_KeepScreenOn` | Keep screen on | 保持屏幕常亮 | +| `Tooltip_Autostart` | Should we start when you log in? | 是否在登录时自动启动? | +| `Tooltip_RememberState` | Should we remember the enabled state between restarts? | 是否在重启之间记住启用状态? | +| `Tooltip_ConfigureApps` | Configure apps to keep the screen on when they are running. | 配置应用程序,当其运行时保持屏幕常亮。 | +| `Tooltip_Enabled` | Are we enabled right now? | 当前是否已启用? | +| `Tooltip_KeepScreenOn` | If display should be kept always on in addition to keeping the system on. | 在保持系统运行的基础上,是否同时保持显示器常亮。 | +| `Msg_InstanceRunning` | {0} instance is already running. | {0} 实例已在运行。 | +| `Msg_AutostartRemoveFailed` | Wasn't able to remove autostart shortcut from '{0}'. Error: {1} | 无法从 '{0}' 移除自启动快捷方式。错误:{1} | +| `Msg_AutostartCreateFailed` | Wasn't able to create autostart shortcut at '{0}'. Error: {1} | 无法在 '{0}' 创建自启动快捷方式。错误:{1} | +| `Btn_AppsWatchingEnabled` | Apps watching is enabled | 应用监视已启用 | +| `Btn_AppsWatchingDisabled` | Apps watching is DISABLED ❌ | 应用监视已禁用 ❌ | +| `FileDlg_FilterExecutable` | Executable | 可执行文件 | +| `FileDlg_FilterAllFiles` | All files | 所有文件 | +| `FileDlg_Title` | Select application executable | 选择应用程序可执行文件 | +| `Col_Name` | Name | 名称 | +| `Col_ExePath` | Exe Path | 可执行文件路径 | +| `Btn_Add` | ➕ Add | ➕ 添加 | +| `Btn_Remove` | ➖ Remove | ➖ 移除 | +| `Btn_Enable` | Enable | 启用 | +| `Btn_Close` | Close | 关闭 | +| `Form_Title` | Configure Applications to monitor | 配置监视的应用程序 | + +共 **26** 个字符串条目。 + +## 假设与决策 + +1. **语言选择策略**:依赖 .NET 默认的 `CurrentUICulture` 自动检测。中文系统 → 中文,其他系统 → 英文(默认)。不在代码中强制设置 culture,保持标准行为。 +2. **品牌名不本地化**:`NoSleep` 作为品牌名在所有语言中保持不变(Mutex、快捷方式文件名、托盘 tooltip、MessageBox 标题)。 +3. **文化代码**:使用 `zh-CN`(简体中文,中国大陆),覆盖最广。如需支持繁体中文可后续添加 `zh-TW`。 +4. **Designer.cs 手动编辑**:虽然 Designer.cs 是自动生成的,但项目已手动管理编译项(`EnableDefaultCompileItems=false`),且当前环境无 VS 设计器运行,手动编辑安全。在 VS 中重新生成时会保留字符串属性(只要 .resx 中有对应条目)。 +5. **单文件发布兼容性**:.NET 6+ 的 `PublishSingleFile` 默认将卫星程序集打入单文件 bundle,无需额外配置。net48 无单文件发布需求,卫星程序集正常输出到 `zh-CN\` 子目录。 +6. **文档策略**:保留英文原文 + 新增中文版本 + 顶部语言链接,符合 GitHub 国际化惯例。 + +## 验证步骤 + +1. **构建验证**(两个框架): + ```sh + cd Sources + dotnet build -f net8.0-windows -c Debug + # net48 需在 Windows 上用 msbuild + ``` +2. **卫星程序集生成验证**:确认 `bin/Debug/net8.0-windows/zh-CN/NoSleep.resources.dll` 存在。 +3. **单文件发布验证**: + ```sh + dotnet publish -f net8.0-windows -c Release + ``` + 确认发布产物为单文件,且中文资源已打入(运行时验证)。 +4. **运行时验证**: + - **中文环境**:设置系统 locale 为简体中文,或临时在 `Program.cs` 中添加 `Thread.CurrentThread.CurrentUICulture = new CultureInfo("zh-CN");` 测试,确认所有 UI 文本显示中文。 + - **英文环境**:保持默认 locale,确认所有 UI 文本显示英文(回退正常)。 +5. **UI 完整性检查**:逐项核对托盘菜单(6 项 + 6 tooltip)、单实例提示、自启错误提示、配置窗口(标题 + 表头 + 4 按钮 + 文件对话框)。 +6. **功能回归**:确认左键切换、右键菜单各功能、应用监视、自启动等行为不受影响。 diff --git a/BUILD.md b/BUILD.md index ed59268..a843267 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,3 +1,5 @@ +> 🌐 Languages: [English](BUILD.md) | [简体中文](BUILD.zh-CN.md) + # How to Build from Source Code This document describes how to build NoSleep from source code. These instructions are intended for developers who want to contribute or experiment with the codebase. diff --git a/BUILD.zh-CN.md b/BUILD.zh-CN.md new file mode 100644 index 0000000..6d85a61 --- /dev/null +++ b/BUILD.zh-CN.md @@ -0,0 +1,224 @@ +> 🌐 语言:[English](BUILD.md) | [简体中文](BUILD.zh-CN.md) + +# 如何从源代码构建 + +本文档描述了如何从源代码构建 NoSleep。这些说明面向希望为项目贡献代码或对代码库进行试验的开发者。 + +该解决方案使用 WinForms,并以两个框架为目标: + +- net48 – .NET Framework 4.8(旧版) +- net8.0-windows – .NET 8.0(Windows 专用) + +目标是构建一个体积较小的独立可执行文件(即我们可以发布单文件;可执行文件本身不包含 .NET 运行时),以便于简单分发。 + +NoSleep 最初使用 .NET 4.x 框架构建。由于 .NET 4.x 随 Windows 一起捆绑,通常终端用户无需进行额外操作。 + +在项目现代化过程中添加了 .NET 8.0,以便更轻松地进行未来的更新。 + +## 先决条件 + +- Windows(必需,因为该应用程序仅支持 Windows) +- 对于 .NET 8.0 构建: + - .NET SDK 8.0 或更高版本 +- 对于 .NET Framework 4.8(旧版): + - .NET SDK(任意较新版本) + - .NET Framework 4.8 SDK + - MSBuild + +您可以通过安装以下任意一项来获取 .NET Framework 4.8 所需的组件: + +- Visual Studio Build Tools – 选择 ".NET desktop build tools" 工作负载(这将包含所有必要的组件),或 +- 完整版 Visual Studio(Community/Professional/Enterprise)并选择 ".NET desktop development" 工作负载。 + +## 获取源代码 + +克隆仓库: + +```sh +git clone https://github.com/CHerSun/NoSleep.git +cd NoSleep +``` + +## 构建结果位置 + +- .NET Framework 4.8 - `Sources/NoSleep/bin/Debug/net48/` 或 `Sources/NoSleep/bin/Release/net48/` +- .NET 8.0 - `Sources/NoSleep/bin/Debug/net8.0-windows/` 或 `Sources/NoSleep/bin/Release/net8.0-windows/` +- 已发布(单可执行文件)的 .NET 8.0 - `Sources/NoSleep/bin/Release/net8.0-windows/win-x64/publish` + +## 使用 Visual Studio 构建 + +这是构建解决方案最简单的方式。只需打开 `Sources/NoSleep.sln` 并正常构建(`F6` 或 `Ctrl+Shift+B`)。 + +## 从命令行构建 + +### 构建 .NET 8.0 版本 + +使用 `dotnet build` 命令。要构建调试版本: + +```sh +cd Sources +dotnet build -f net8.0-windows -c Debug +``` + +构建发布版本: + +```sh +cd Sources +dotnet build -f net8.0-windows -c Release +``` + +注意:在 `net8.0-windows` 的发布构建完成后,会自动触发发布步骤(以创建单可执行文件)。 + +### 构建 .NET Framework 4.8 版本 + +我无法使用 dotnet build 生成单可执行文件,因此我们对该目标使用 `msbuild`。要构建调试版本: + +```sh +cd Sources +msbuild NoSleep/NoSleep.csproj /p:Configuration=Debug /p:TargetFramework=net48 /restore +``` + +构建发布版本: + +```sh +cd Sources +msbuild NoSleep/NoSleep.csproj /p:Configuration=Release /p:TargetFramework=net48 /restore +``` + +请确保 `msbuild` 位于您的 PATH 中(通常可从 Visual Studio Developer Command Prompt 中获取),或者提供 `msbuild.exe` 的完整路径。 + +## 使用 Visual Studio Code 构建 + +我个人更喜欢 VS Code。您可以在 `.vscode/tasks.json` 文件中定义构建任务。以下是一个示例 `tasks.json`: + +```json +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build-net48", + "command": "C:\\Program Files (x86)\\Microsoft Visual Studio\\18\\BuildTools\\MSBuild\\Current\\Bin\\MSBuild.exe", + "type": "process", + "group": "build", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/Sources/" + }, + "args": [ + "${workspaceFolder}/Sources/NoSleep/NoSleep.csproj", + "/p:Configuration=Debug", + "/p:TargetFramework=net48", + "/p:GenerateResourceUsePreserializedResources=false", // 可选,如有需要请保留 + "/restore" + ] + }, + { + "label": "build-net48-release", + "command": "C:\\Program Files (x86)\\Microsoft Visual Studio\\18\\BuildTools\\MSBuild\\Current\\Bin\\MSBuild.exe", + "type": "process", + "group": "build", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/Sources/" + }, + "args": [ + "${workspaceFolder}/Sources/NoSleep/NoSleep.csproj", + "/p:Configuration=Release", + "/p:TargetFramework=net48", + "/p:GenerateResourceUsePreserializedResources=false", // 可选,如有需要请保留 + "/restore" + ] + }, + { + "label": "build-net8.0", + "command": "dotnet", + "type": "process", + "group": "build", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/Sources/" + }, + "args": [ + "build", + "${workspaceFolder}/Sources/NoSleep/NoSleep.csproj", + "-f", + "net8.0-windows", + "-c", + "Debug" + ] + }, + { + "label": "publish-net8.0-release", + "command": "dotnet", + "type": "process", + "group": "build", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/Sources/" + }, + "args": [ + "publish", + "${workspaceFolder}/Sources/NoSleep/NoSleep.csproj", + "-f", + "net8.0-windows", + "-c", + "Release", + ] + }, + { + "label": "build-all", + "dependsOn": ["build-net48", "build-net8.0"], + "group": { + "kind": "build" + } + }, + { + "label": "build-all-release", + "dependsOn": ["build-net48-release", "publish-net8.0-release"], + "group": { + "kind": "build", + "isDefault": true + } + }, + ] +} +``` + +您可能需要根据自身环境调整 `MSBuild` 路径或重新定义任务。 + +## 调试 + +Visual Studio 提供即时调试支持。 + +对于 VS Code,您需要一个调试配置。以下是一个示例 `.vscode/launch.json`: + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug .NET 8.0 (Windows)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-net8.0", + "program": "${workspaceFolder}/Sources/NoSleep/bin/Debug/net8.0-windows/NoSleep.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "console": "internalConsole" + }, + { + "name": "Debug .NET Framework 4.8", + "type": "clr", + "request": "launch", + "preLaunchTask": "build-net48", + "program": "${workspaceFolder}/Sources/NoSleep/bin/Debug/net48/NoSleep.exe", + "args": [], + "cwd": "${workspaceFolder}", + "console": "internalConsole" + } + ] +} +``` + +请注意,`preLaunchTask` 的值必须与 `tasks.json` 中定义的任务标签完全一致。 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5ae23b0..a1c9826 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,5 @@ +> 🌐 Languages: [English](CONTRIBUTING.md) | [简体中文](CONTRIBUTING.zh-CN.md) + # Contributing to NoSleep Thank you for considering contributing to NoSleep! Contributions are welcomed from everyone. By participating in this project, you agree to abide by the following guidelines and to license your contributions under The Unlicense (see [LICENSE](LICENSE)). diff --git a/CONTRIBUTING.zh-CN.md b/CONTRIBUTING.zh-CN.md new file mode 100644 index 0000000..f21e060 --- /dev/null +++ b/CONTRIBUTING.zh-CN.md @@ -0,0 +1,72 @@ +> 🌐 语言:[English](CONTRIBUTING.md) | [简体中文](CONTRIBUTING.zh-CN.md) + +# 贡献 NoSleep + +感谢您考虑为 NoSleep 做出贡献!欢迎所有人参与本项目。参与本项目即表示您同意遵守以下指南,并根据 The Unlicense 协议授权您的贡献(参见 [LICENSE](LICENSE))。 + +构建说明请参见 [BUILD.md](BUILD.md)。 + +## 如何贡献 + +本项目采用标准的 GitHub fork + pull request 工作流。如果您对这一流程不熟悉,以下是简要概述: + +- 在 GitHub 上 fork 本仓库。 +- 将您的 fork 克隆到本地。 +- 为您的更改创建一个新 branch。 +- 进行更改(请遵循以下指南)。每个 branch 应只针对一项单独的改进。 +- 充分测试您的更改。 +- 将您的 branch 推送到您的 fork。 +- 向主仓库的 main(或 master)branch 提交 pull request。 + +## 开发指南 + +简而言之:保持精简。 + +为保持项目的一致性和可维护性,请遵守以下指南: + +### 1. 避免外部依赖 + +- 除非绝对必要,不要添加外部库或 NuGet 包。本项目旨在保持轻量和自包含。如果您认为某个依赖是必不可少的,请在 pull request 中说明原因。 + +### 2. 目标为单一可执行文件 + +- 构建过程必须生成一个独立的可执行文件(同时支持 .NET Framework 4.8 和 .NET 8.0 目标)。 +- 如果您的更改导致无法打包为单一可执行文件,请说明为何必须如此。 + +### 3. 保持精简 + +避免不必要的开销: + +- 除非绝对必要,不要调用外部进程(如果确实需要,请说明理由)。 +- 尽量减少内存分配、CPU 占用和异常抛出。 +- 编写高效的代码 —— 这是一个后台工具,应对系统资源的影响降到最低。 + +### 4. 保持跨框架兼容性 + +- NoSleep 同时支持 .NET Framework 4.8 和 .NET 8.0-windows。 +- 在添加新功能或进行更改时,请确保它们在两个框架上都能正常工作。 +- 在必要时使用预处理器指令(`#if NETFRAMEWORK` / `#if NET`),但优先选择无需条件编译即可在两个框架上通用的代码。 + +## 测试您的更改 + +- 在两个目标框架上测试您的更改(Debug 和 Release 配置)。 +- 验证生成的可执行文件能否按预期运行,并且没有引入新的问题。 +- 如果您要添加功能,请考虑是否需要相应的测试(尽管本项目目前没有正式的测试套件,但仍感谢您进行手动验证)。 + +## 提交 Pull Request + +当您提交 pull request 时,请: + +- 清晰地描述更改内容及其背后的动机。 +- 引用任何相关 issue(例如 "Fixes #123")。 +- 确保您的 branch 与最新的 main branch 保持同步。 + +请确保 pull request 聚焦于单一主题 —— 如果您有多个不相关的更改,请分别提交独立的 PR。 + +## 许可协议 + +通过为 NoSleep 做出贡献,您即同意您的贡献将根据 The Unlicense 协议授权。这意味着您的作品已贡献至公共领域,任何人都可以出于任何目的自由使用。如果您对此感到不适,请勿参与贡献。 + +## 需要帮助? + +如果您有疑问或需要指导,欢迎提交 issue 或发起讨论。 diff --git a/Sources/NoSleep/ConfigureAppsForm.Designer.cs b/Sources/NoSleep/ConfigureAppsForm.Designer.cs index 4cfb473..41b6c8f 100644 --- a/Sources/NoSleep/ConfigureAppsForm.Designer.cs +++ b/Sources/NoSleep/ConfigureAppsForm.Designer.cs @@ -63,7 +63,7 @@ private void InitializeComponent() // colName // this.colName.DataPropertyName = "Name"; - this.colName.HeaderText = "Name"; + this.colName.HeaderText = Properties.Resources.Col_Name; this.colName.MinimumWidth = 8; this.colName.Name = "colName"; this.colName.Width = 200; @@ -72,7 +72,7 @@ private void InitializeComponent() // this.colExePath.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; this.colExePath.DataPropertyName = "ExePath"; - this.colExePath.HeaderText = "Exe Path"; + this.colExePath.HeaderText = Properties.Resources.Col_ExePath; this.colExePath.MinimumWidth = 8; this.colExePath.Name = "colExePath"; // @@ -84,7 +84,7 @@ private void InitializeComponent() this.buttonAdd.Name = "buttonAdd"; this.buttonAdd.Size = new System.Drawing.Size(112, 42); this.buttonAdd.TabIndex = 1; - this.buttonAdd.Text = "➕ Add"; + this.buttonAdd.Text = Properties.Resources.Btn_Add; this.buttonAdd.UseVisualStyleBackColor = true; this.buttonAdd.Click += new System.EventHandler(this.ButtonAdd_Click); // @@ -96,7 +96,7 @@ private void InitializeComponent() this.buttonRemove.Name = "buttonRemove"; this.buttonRemove.Size = new System.Drawing.Size(112, 42); this.buttonRemove.TabIndex = 2; - this.buttonRemove.Text = "➖ Remove"; + this.buttonRemove.Text = Properties.Resources.Btn_Remove; this.buttonRemove.UseVisualStyleBackColor = true; this.buttonRemove.Click += new System.EventHandler(this.ButtonRemove_Click); // @@ -109,7 +109,7 @@ private void InitializeComponent() this.buttonEnable.Name = "buttonEnable"; this.buttonEnable.Size = new System.Drawing.Size(262, 42); this.buttonEnable.TabIndex = 3; - this.buttonEnable.Text = "Enable"; + this.buttonEnable.Text = Properties.Resources.Btn_Enable; this.buttonEnable.UseVisualStyleBackColor = true; this.buttonEnable.Click += new System.EventHandler(this.ButtonEnable_Click); // @@ -122,7 +122,7 @@ private void InitializeComponent() this.buttonClose.Name = "buttonClose"; this.buttonClose.Size = new System.Drawing.Size(112, 42); this.buttonClose.TabIndex = 4; - this.buttonClose.Text = "Close"; + this.buttonClose.Text = Properties.Resources.Btn_Close; this.buttonClose.UseVisualStyleBackColor = true; // // ConfigureAppsForm @@ -141,7 +141,7 @@ private void InitializeComponent() this.MinimizeBox = false; this.Name = "ConfigureAppsForm"; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; - this.Text = "Configure Applications to monitor"; + this.Text = Properties.Resources.Form_Title; ((System.ComponentModel.ISupportInitialize)(this.dataGridViewApps)).EndInit(); this.ResumeLayout(false); diff --git a/Sources/NoSleep/ConfigureAppsForm.cs b/Sources/NoSleep/ConfigureAppsForm.cs index 167d668..069f460 100644 --- a/Sources/NoSleep/ConfigureAppsForm.cs +++ b/Sources/NoSleep/ConfigureAppsForm.cs @@ -1,6 +1,7 @@ using System; using System.Drawing; using System.Windows.Forms; +using NoSleep.Properties; namespace NoSleep { @@ -28,8 +29,8 @@ private void ButtonAdd_Click(object sender, EventArgs e) { using (var dlg = new OpenFileDialog()) { - dlg.Filter = "Executable|*.exe|All files|*.*"; - dlg.Title = "Select application executable"; + dlg.Filter = Resources.FileDlg_FilterExecutable + "|*.exe|" + Resources.FileDlg_FilterAllFiles + "|*.*"; + dlg.Title = Resources.FileDlg_Title; if (dlg.ShowDialog(this) != DialogResult.OK) return; @@ -50,7 +51,7 @@ private void ButtonRemove_Click(object sender, EventArgs e) private void UpdateEnabledState(bool enabled) { - buttonEnable.Text = enabled ? "Apps watching is enabled" : "Apps watching is DISABLED ❌"; + buttonEnable.Text = enabled ? Resources.Btn_AppsWatchingEnabled : Resources.Btn_AppsWatchingDisabled; buttonEnable.BackColor = enabled ? Color.LightGreen : Color.Coral; mainForm.UserWatchingEnabled = enabled; } diff --git a/Sources/NoSleep/NoSleep.csproj b/Sources/NoSleep/NoSleep.csproj index dcb47ca..796cb93 100644 --- a/Sources/NoSleep/NoSleep.csproj +++ b/Sources/NoSleep/NoSleep.csproj @@ -1,4 +1,4 @@ - + NoSleep @@ -75,6 +75,9 @@ Resources.Designer.cs Designer + + Resources.resx + diff --git a/Sources/NoSleep/Program.cs b/Sources/NoSleep/Program.cs index 838c633..c47fc20 100644 --- a/Sources/NoSleep/Program.cs +++ b/Sources/NoSleep/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Windows.Forms; @@ -16,7 +16,7 @@ static void Main() { if (!mutex.WaitOne(0, false)) { - MessageBox.Show($"{Properties.Settings.Default.AppName} instance is already running.", Properties.Settings.Default.AppName, MessageBoxButtons.OK, MessageBoxIcon.Information); + MessageBox.Show(string.Format(Properties.Resources.Msg_InstanceRunning, Properties.Settings.Default.AppName), Properties.Settings.Default.AppName, MessageBoxButtons.OK, MessageBoxIcon.Information); return; } diff --git a/Sources/NoSleep/Properties/Resources.Designer.cs b/Sources/NoSleep/Properties/Resources.Designer.cs index 9aacd8e..73ab408 100644 --- a/Sources/NoSleep/Properties/Resources.Designer.cs +++ b/Sources/NoSleep/Properties/Resources.Designer.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 @@ -79,5 +79,135 @@ internal static System.Drawing.Icon TrayIconInactive { return ((System.Drawing.Icon)(obj)); } } + + /// Tray menu: Close + internal static string TrayMenu_Close { + get { return ResourceManager.GetString("TrayMenu_Close", resourceCulture); } + } + + /// Tray menu: Autostart at login + internal static string TrayMenu_Autostart { + get { return ResourceManager.GetString("TrayMenu_Autostart", resourceCulture); } + } + + /// Tray menu: Remember enabled state + internal static string TrayMenu_RememberState { + get { return ResourceManager.GetString("TrayMenu_RememberState", resourceCulture); } + } + + /// Tray menu: Configure apps to monitor + internal static string TrayMenu_ConfigureApps { + get { return ResourceManager.GetString("TrayMenu_ConfigureApps", resourceCulture); } + } + + /// Tray menu: Enabled + internal static string TrayMenu_Enabled { + get { return ResourceManager.GetString("TrayMenu_Enabled", resourceCulture); } + } + + /// Tray menu: Keep screen on + internal static string TrayMenu_KeepScreenOn { + get { return ResourceManager.GetString("TrayMenu_KeepScreenOn", resourceCulture); } + } + + /// Tooltip: Should we start when you log in? + internal static string Tooltip_Autostart { + get { return ResourceManager.GetString("Tooltip_Autostart", resourceCulture); } + } + + /// Tooltip: Should we remember the enabled state between restarts? + internal static string Tooltip_RememberState { + get { return ResourceManager.GetString("Tooltip_RememberState", resourceCulture); } + } + + /// Tooltip: Configure apps to keep the screen on when they are running. + internal static string Tooltip_ConfigureApps { + get { return ResourceManager.GetString("Tooltip_ConfigureApps", resourceCulture); } + } + + /// Tooltip: Are we enabled right now? + internal static string Tooltip_Enabled { + get { return ResourceManager.GetString("Tooltip_Enabled", resourceCulture); } + } + + /// Tooltip: If display should be kept always on in addition to keeping the system on. + internal static string Tooltip_KeepScreenOn { + get { return ResourceManager.GetString("Tooltip_KeepScreenOn", resourceCulture); } + } + + /// Message: {0} instance is already running. + internal static string Msg_InstanceRunning { + get { return ResourceManager.GetString("Msg_InstanceRunning", resourceCulture); } + } + + /// Message: Wasn't able to remove autostart shortcut from '{0}'. Error: {1} + internal static string Msg_AutostartRemoveFailed { + get { return ResourceManager.GetString("Msg_AutostartRemoveFailed", resourceCulture); } + } + + /// Message: Wasn't able to create autostart shortcut at '{0}'. Error: {1} + internal static string Msg_AutostartCreateFailed { + get { return ResourceManager.GetString("Msg_AutostartCreateFailed", resourceCulture); } + } + + /// Button: Apps watching is enabled + internal static string Btn_AppsWatchingEnabled { + get { return ResourceManager.GetString("Btn_AppsWatchingEnabled", resourceCulture); } + } + + /// Button: Apps watching is DISABLED + internal static string Btn_AppsWatchingDisabled { + get { return ResourceManager.GetString("Btn_AppsWatchingDisabled", resourceCulture); } + } + + /// File dialog filter label: Executable + internal static string FileDlg_FilterExecutable { + get { return ResourceManager.GetString("FileDlg_FilterExecutable", resourceCulture); } + } + + /// File dialog filter label: All files + internal static string FileDlg_FilterAllFiles { + get { return ResourceManager.GetString("FileDlg_FilterAllFiles", resourceCulture); } + } + + /// File dialog title: Select application executable + internal static string FileDlg_Title { + get { return ResourceManager.GetString("FileDlg_Title", resourceCulture); } + } + + /// Column header: Name + internal static string Col_Name { + get { return ResourceManager.GetString("Col_Name", resourceCulture); } + } + + /// Column header: Exe Path + internal static string Col_ExePath { + get { return ResourceManager.GetString("Col_ExePath", resourceCulture); } + } + + /// Button: Add + internal static string Btn_Add { + get { return ResourceManager.GetString("Btn_Add", resourceCulture); } + } + + /// Button: Remove + internal static string Btn_Remove { + get { return ResourceManager.GetString("Btn_Remove", resourceCulture); } + } + + /// Button: Enable + internal static string Btn_Enable { + get { return ResourceManager.GetString("Btn_Enable", resourceCulture); } + } + + /// Button: Close + internal static string Btn_Close { + get { return ResourceManager.GetString("Btn_Close", resourceCulture); } + } + + /// Form title: Configure Applications to monitor + internal static string Form_Title { + get { return ResourceManager.GetString("Form_Title", resourceCulture); } + } } } diff --git a/Sources/NoSleep/Properties/Resources.resx b/Sources/NoSleep/Properties/Resources.resx index 888a4ec..d6d81cd 100644 --- a/Sources/NoSleep/Properties/Resources.resx +++ b/Sources/NoSleep/Properties/Resources.resx @@ -1,4 +1,4 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 关闭 + + + 登录时自动启动 + + + 记住启用状态 + + + 配置监视的应用程序 + + + 已启用 + + + 保持屏幕常亮 + + + 是否在登录时自动启动? + + + 是否在重启之间记住启用状态? + + + 配置应用程序,当其运行时保持屏幕常亮。 + + + 当前是否已启用? + + + 在保持系统运行的基础上,是否同时保持显示器常亮。 + + + {0} 实例已在运行。 + + + 无法从 '{0}' 移除自启动快捷方式。错误:{1} + + + 无法在 '{0}' 创建自启动快捷方式。错误:{1} + + + 应用监视已启用 + + + 应用监视已禁用 ❌ + + + 可执行文件 + + + 所有文件 + + + 选择应用程序可执行文件 + + + 名称 + + + 可执行文件路径 + + + ➕ 添加 + + + ➖ 移除 + + + 启用 + + + 关闭 + + + 配置监视的应用程序 + + diff --git a/Sources/NoSleep/Tools.cs b/Sources/NoSleep/Tools.cs index 9c3f33c..8740d66 100644 --- a/Sources/NoSleep/Tools.cs +++ b/Sources/NoSleep/Tools.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Windows.Forms; @@ -46,7 +46,7 @@ internal static bool AutostartDisable() } catch (Exception e) { - MessageBox.Show($"Wasn't able to remove autostart shortcut from '{autostartPath}'. Error: {e.Message}", + MessageBox.Show(string.Format(Properties.Resources.Msg_AutostartRemoveFailed, autostartPath, e.Message), caption: Properties.Settings.Default.AppName, buttons: MessageBoxButtons.OK, icon: MessageBoxIcon.Error); return false; } @@ -64,7 +64,7 @@ internal static bool AutostartEnable() } catch (Exception e) { - MessageBox.Show($"Wasn't able to create autostart shortcut at '{autostartPath}'. Error: {e.Message}", + MessageBox.Show(string.Format(Properties.Resources.Msg_AutostartCreateFailed, autostartPath, e.Message), caption: Properties.Settings.Default.AppName, buttons: MessageBoxButtons.OK, icon: MessageBoxIcon.Error); return false; } diff --git a/Sources/NoSleep/TrayIcon.cs b/Sources/NoSleep/TrayIcon.cs index af6a35d..cc1182b 100644 --- a/Sources/NoSleep/TrayIcon.cs +++ b/Sources/NoSleep/TrayIcon.cs @@ -1,4 +1,4 @@ -using NoSleep.Properties; +using NoSleep.Properties; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -143,41 +143,41 @@ private void InitializeComponent() _trayIcon.Click += Click_TrayIcon; // Create tray menu items - var _MenuItem_Close = new ToolStripMenuItem("Close"); + var _MenuItem_Close = new ToolStripMenuItem(Resources.TrayMenu_Close); _MenuItem_Close.Click += (e, s) => Application.Exit(); - _menuItem_AutoStart = new ToolStripMenuItem("Autostart at login") + _menuItem_AutoStart = new ToolStripMenuItem(Resources.TrayMenu_Autostart) { Checked = UserAutoStart, - ToolTipText = "Should we start when you log in?" + ToolTipText = Resources.Tooltip_Autostart }; _menuItem_AutoStart.Click += Click_AutoStart; - _menuItem_RememberEnabledState = new ToolStripMenuItem("Remember enabled state") + _menuItem_RememberEnabledState = new ToolStripMenuItem(Resources.TrayMenu_RememberState) { Checked = UserPersistEnabled, - ToolTipText = "Should we remember the enabled state between restarts?" + ToolTipText = Resources.Tooltip_RememberState }; _menuItem_RememberEnabledState.Click += Click_SaveEnabledState; - _menuItem_ConfigureApps = new ToolStripMenuItem("Configure apps to monitor") + _menuItem_ConfigureApps = new ToolStripMenuItem(Resources.TrayMenu_ConfigureApps) { Checked = IsWatchingFeatureActive, - ToolTipText = "Configure apps to keep the screen on when they are running." + ToolTipText = Resources.Tooltip_ConfigureApps }; _menuItem_ConfigureApps.Click += Click_ConfigureApps; - _menuItem_Enabled = new ToolStripMenuItem("Enabled") + _menuItem_Enabled = new ToolStripMenuItem(Resources.TrayMenu_Enabled) { Checked = UserEnabled, - ToolTipText = "Are we enabled right now?" + ToolTipText = Resources.Tooltip_Enabled }; _menuItem_Enabled.Click += Click_Enabled; - _menuItem_DisplayRequired = new ToolStripMenuItem("Keep screen on") + _menuItem_DisplayRequired = new ToolStripMenuItem(Resources.TrayMenu_KeepScreenOn) { Checked = !Settings.Default.DisplayRequired, - ToolTipText = "If display should be kept always on in addition to keeping the system on." + ToolTipText = Resources.Tooltip_KeepScreenOn }; _menuItem_DisplayRequired.Click += Click_DisplayRequired; Click_DisplayRequired(null, null); diff --git a/readme.md b/readme.md index 2ed4852..ac89115 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,5 @@ +> 🌐 Languages: [English](readme.md) | [简体中文](readme.zh-CN.md) + # NoSleep NoSleep is a lightweight tool that prevents Windows from automatically activating the screen saver, sleep mode, or lock screen. It's designed for situations where you can't change these settings yourself—for example, due to corporate-enforced policies. Inspired by Linux Caffeine. diff --git a/readme.zh-CN.md b/readme.zh-CN.md new file mode 100644 index 0000000..3d7ac0f --- /dev/null +++ b/readme.zh-CN.md @@ -0,0 +1,57 @@ +> 🌐 语言:[English](readme.md) | [简体中文](readme.zh-CN.md) + +# NoSleep + +NoSleep 是一款轻量级工具,用于防止 Windows 自动激活屏幕保护程序、睡眠模式或锁屏。它专为那些你无法自行更改这些设置的场景而设计——例如,因公司强制策略限制而无法修改时。灵感来自 Linux 的 Caffeine。 + +> **注意:** Windows PowerToys 包含一款名为 Awake 的工具,功能相同,但 PowerToys 可能需要管理员权限,且体积较大。NoSleep 致力于尽可能精简,运行时无需任何额外权限。 + +## 安装 + +NoSleep 可通过 [Scoop](https://scoop.sh/) 包管理器安装: + +```sh +scoop bucket add extras +scoop install extras/nosleep +``` + +你也可以从[发布页面](https://github.com/CHerSun/NoSleep/releases/latest)手动下载最新版本。 + +## 使用方法 + +NoSleep 采用"设置后即忘"的设计。一旦运行,它会驻留在 Windows 系统托盘中,防止你的电脑进入睡眠状态。 + +- **左键单击**托盘图标可切换启用或禁用状态。图标会随之变化以反映当前状态。 +- **右键单击**托盘图标可打开菜单,包含以下附加选项: + + - **登录时自动启动** – 在你登录时自动启动 NoSleep。 + - **保持屏幕常亮** – 在 NoSleep 启用时,防止显示器关闭。 + - **记住启用状态** – 在应用程序重启后保存启用状态。 + - **配置要监视的应用程序** – 定义一个应用程序列表。如果这些应用都没有运行,NoSleep 会自动禁用自身;如果任意一个被监视的应用正在运行,NoSleep 会启用自身并阻止睡眠。此动态行为仅在用户启用 NoSleep 时生效。(自 v1.4.0 起可用) + +要完全停止 NoSleep,请右键单击托盘图标并选择 **关闭**。 + +### 行为矩阵 + +| 启用 | 保持屏幕常亮 | 系统行为 | 显示器行为 | +| ------- | -------------- | ---------------------------------------- | ---------------- | +| ✅ 开 | ✅ 开 | 阻止睡眠 | 始终常亮 | +| ✅ 开 | ⬜ 关 | 阻止睡眠 | 可关闭 | +| ⬜ 关 | 任意 | 正常系统行为(可能进入睡眠) | 可关闭 | + +## 系统要求 + +- .NET Framework 4.8 或更高版本(4.x 分支)。通常在 Windows 10 及更高版本中已预装。如需安装,可从 [Microsoft](https://dotnet.microsoft.com/en-us/download/dotnet-framework) 下载。 + +## 工作原理 + +NoSleep 每 10 秒调用一次 `SetThreadExecutionState` 函数,重置 Windows 的显示器和空闲计时器。此过程 CPU 占用极低,仅消耗少量内存。编译后的二进制文件包含图标(约 180 KB)和核心代码。 + +## 图标归属 + +| 用途 | 图标及来源 | 许可证 | 作者 | +| -------------- | ---------------------------------------------------------------------------------------------- | ------------------------ | --------------------------------------------------------------------------- | +| 启用状态 | [Coffee icon](https://www.iconarchive.com/show/food-icons-by-martin-berube/coffee-icon.html) | 免费软件 | [Martin Berube](https://www.iconarchive.com/artist/martin-berube.html) | +| 禁用状态 | [Sleep icon](https://www.iconarchive.com/show/material-icons-by-pictogrammers/sleep-icon.html) | Apache 2.0(开源) | [Pictogrammers Team](https://www.iconarchive.com/artist/pictogrammers.html) | + +衷心感谢这些图标的作者!