diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2e973aa..d60d726 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -161,18 +161,13 @@ jobs: schtasks /Delete /TN "ThreadPilot_Startup" /F >nul 2>&1 reg delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "ThreadPilot" /f >nul 2>&1 - echo [3/4] Optional user data cleanup... - set "REMOVE_DATA=N" - set /p REMOVE_DATA=Do you want to remove user settings at "%APPDATA%\ThreadPilot"? [y/N]: - if /I "%REMOVE_DATA%"=="Y" ( - if exist "%APPDATA%\ThreadPilot" ( - rd /s /q "%APPDATA%\ThreadPilot" - echo User settings removed. - ) else ( - echo No user settings folder found. - ) + echo [3/4] Removing ThreadPilot user data for this Windows account... + rem Full uninstall removes only ThreadPilot-owned per-user AppData. Normal install/update paths never run this script. + if exist "%APPDATA%\ThreadPilot" ( + rd /s /q "%APPDATA%\ThreadPilot" + echo ThreadPilot user data removed. ) else ( - echo User settings were kept. + echo No ThreadPilot user data folder found. ) echo [4/4] Scheduling app folder removal... diff --git a/App.xaml.cs b/App.xaml.cs index 4916d2a..06dc022 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -63,50 +63,22 @@ public App() protected override void OnStartup(StartupEventArgs e) { // Parse command line arguments early so special startup modes can short-circuit normal flow. - bool startMinimized = false; - bool isAutostart = false; - bool isSmokeTest = false; - bool registerLaunchTask = false; - bool launchedViaTask = false; -#if DEBUG - bool isTestMode = false; -#endif + var startupMode = StartupMode.Parse(e.Args); bool effectiveStartMinimized = false; ApplicationSettingsModel? loadedSettings = null; - foreach (var arg in e.Args) + effectiveStartMinimized = startupMode.StartMinimized; + + if (startupMode.IsSmokeTest) { - switch (arg.ToLowerInvariant()) - { -#if DEBUG - case "--test": - isTestMode = true; - break; -#endif - case "--smoke-test": - isSmokeTest = true; - break; - case "--start-minimized": - startMinimized = true; - break; - case "--autostart": - isAutostart = true; - break; - case "--startup": // Alternative startup argument - isAutostart = true; - startMinimized = true; - break; - case RegisterLaunchTaskArgument: - registerLaunchTask = true; - break; - case LaunchedViaTaskArgument: - launchedViaTask = true; - break; - } + var smokeLogger = this.ServiceProvider.GetRequiredService>(); + var smokeTestResult = this.RunSmokeTestWithTimeout(smokeLogger, TimeSpan.FromSeconds(10)); + Environment.ExitCode = smokeTestResult; + this.Shutdown(smokeTestResult); + Environment.Exit(smokeTestResult); + return; } - effectiveStartMinimized = startMinimized; - // Set up global exception handlers first AppDomain.CurrentDomain.UnhandledException += this.OnUnhandledException; this.DispatcherUnhandledException += this.OnDispatcherUnhandledException; @@ -130,14 +102,14 @@ protected override void OnStartup(StartupEventArgs e) } else { - if (launchedViaTask) + if (startupMode.LaunchedViaTask) { logger.LogError("Application was launched via managed task marker but is still not elevated."); } #if DEBUG - else if (!isSmokeTest && !isTestMode) + else if (!startupMode.IsTestMode) #else - else if (!isSmokeTest) + else #endif { var launchedElevatedInstance = Task.Run(async () => await elevatedTaskService.TryRunLaunchTaskAsync()).GetAwaiter().GetResult(); @@ -148,7 +120,7 @@ protected override void OnStartup(StartupEventArgs e) return; } - if (!registerLaunchTask) + if (!startupMode.RegisterLaunchTask) { logger.LogInformation("Managed elevated launch task is unavailable. Requesting one-time elevation to bootstrap persistent launch."); var restartInitiated = Task.Run(async () => await elevationService.RestartWithElevation(new[] { RegisterLaunchTaskArgument })).GetAwaiter().GetResult(); @@ -160,9 +132,9 @@ protected override void OnStartup(StartupEventArgs e) } #if DEBUG - if (!isSmokeTest && !isTestMode) + if (!startupMode.IsTestMode) #else - if (!isSmokeTest) + if (true) #endif { logger.LogError("ThreadPilot requires administrator privileges and cannot continue without elevation."); @@ -170,33 +142,28 @@ protected override void OnStartup(StartupEventArgs e) this.Shutdown(1); return; } - - logger.LogWarning("Application is running without administrator privileges in smoke test mode."); } // Enforce single-instance after elevation bootstrap logic to avoid mutex races during handoff. - if (!isSmokeTest) + bool createdNew; + this.singleInstanceMutex = new Mutex(initiallyOwned: true, name: "Global\\ThreadPilot_SingleInstance", createdNew: out createdNew); + if (!createdNew) { - bool createdNew; - this.singleInstanceMutex = new Mutex(initiallyOwned: true, name: "Global\\ThreadPilot_SingleInstance", createdNew: out createdNew); - if (!createdNew) - { - System.Windows.MessageBox.Show( - "ThreadPilot is already running.", - "Instance already open", - MessageBoxButton.OK, - MessageBoxImage.Information); + System.Windows.MessageBox.Show( + "ThreadPilot is already running.", + "Instance already open", + MessageBoxButton.OK, + MessageBoxImage.Information); - this.Shutdown(); - return; - } + this.Shutdown(); + return; } base.OnStartup(e); // Check for test mode #if DEBUG - if (isTestMode) + if (startupMode.IsTestMode) { // Run in console test mode AllocConsole(); @@ -209,13 +176,6 @@ protected override void OnStartup(StartupEventArgs e) } #endif - if (isSmokeTest) - { - var smokeTestResult = Task.Run(async () => await this.RunSmokeTestAsync(logger)).GetAwaiter().GetResult(); - this.Shutdown(smokeTestResult); - return; - } - try { var settingsService = this.ServiceProvider.GetRequiredService(); @@ -226,7 +186,7 @@ protected override void OnStartup(StartupEventArgs e) var settings = settingsService.Settings; loadedSettings = settings; localizationService.ApplyLanguage(settings.Language); - effectiveStartMinimized = startMinimized || settings.StartMinimized; + effectiveStartMinimized = startupMode.StartMinimized || settings.StartMinimized; var useDarkTheme = settings.HasUserThemePreference ? settings.UseDarkTheme : themeService.GetSystemUsesDarkTheme(); @@ -258,7 +218,7 @@ protected override void OnStartup(StartupEventArgs e) throw new InvalidOperationException("MainWindow could not be created"); } - var startupWindowBehavior = StartupWindowBehavior.Resolve(isAutostart, effectiveStartMinimized); + var startupWindowBehavior = StartupWindowBehavior.Resolve(startupMode.IsAutostart, effectiveStartMinimized); var showStartupSuggestion = loadedSettings != null && StartupMinimizedSuggestionPolicy.ShouldShow(loadedSettings, startupWindowBehavior); mainWindow.ConfigureStartupMode( @@ -301,16 +261,33 @@ protected override void OnStartup(StartupEventArgs e) } } - private async Task RunSmokeTestAsync(ILogger logger) + private int RunSmokeTestWithTimeout(ILogger logger, TimeSpan timeout) + { + var smokeTestTask = Task.Run(() => this.RunSmokeTest(logger)); + if (smokeTestTask.Wait(timeout)) + { + return smokeTestTask.GetAwaiter().GetResult(); + } + + logger.LogError("ThreadPilot smoke test timed out after {TimeoutSeconds} seconds", timeout.TotalSeconds); + return 2; + } + + private int RunSmokeTest(ILogger logger) { try { logger.LogInformation("Starting ThreadPilot smoke test"); - var settingsService = this.ServiceProvider.GetRequiredService(); - await settingsService.LoadSettingsAsync().ConfigureAwait(false); - _ = this.ServiceProvider.GetRequiredService(); - _ = this.ServiceProvider.GetRequiredService(); + _ = this.ServiceProvider.GetRequiredService(); + _ = this.ServiceProvider.GetRequiredService(); + _ = this.ServiceProvider.GetRequiredService(); + _ = this.ServiceProvider.GetRequiredService(); + + if (!System.IO.Directory.Exists(AppContext.BaseDirectory)) + { + throw new InvalidOperationException("Application base directory was not found."); + } logger.LogInformation("ThreadPilot smoke test completed successfully"); return 0; @@ -322,6 +299,55 @@ private async Task RunSmokeTestAsync(ILogger logger) } } + private readonly struct StartupMode + { + public bool StartMinimized { get; init; } + + public bool IsAutostart { get; init; } + + public bool IsSmokeTest { get; init; } + + public bool RegisterLaunchTask { get; init; } + + public bool LaunchedViaTask { get; init; } + + public bool IsTestMode { get; init; } + + public static StartupMode Parse(IEnumerable args) + { + var mode = default(StartupMode); + foreach (var arg in args) + { + switch (arg.ToLowerInvariant()) + { + case "--test": + mode = mode with { IsTestMode = true }; + break; + case "--smoke-test": + mode = mode with { IsSmokeTest = true }; + break; + case "--start-minimized": + mode = mode with { StartMinimized = true }; + break; + case "--autostart": + mode = mode with { IsAutostart = true }; + break; + case "--startup": + mode = mode with { IsAutostart = true, StartMinimized = true }; + break; + case RegisterLaunchTaskArgument: + mode = mode with { RegisterLaunchTask = true }; + break; + case LaunchedViaTaskArgument: + mode = mode with { LaunchedViaTask = true }; + break; + } + } + + return mode; + } + } + protected override void OnExit(ExitEventArgs e) { AppDomain.CurrentDomain.UnhandledException -= this.OnUnhandledException; diff --git a/Installer/Installer.iss b/Installer/Installer.iss index 622de70..a17973c 100644 --- a/Installer/Installer.iss +++ b/Installer/Installer.iss @@ -5,7 +5,7 @@ #define MyAppPublisher "ThreadPilot" #define MyAppURL "https://github.com/" #define MyAppExeName "ThreadPilot.exe" -#define MyAppVersion "1.3.0" +#define MyAppVersion "1.4.0" #ifndef MyWizardStyle #define MyWizardStyle "modern dynamic windows11" @@ -20,7 +20,7 @@ AppId={{A2A4C8B5-4A9A-4B1B-93F4-5F8B1C7E8C2A} AppName={#MyAppName} AppVersion={#MyAppVersion} -AppVerName={#MyAppName} {#MyAppVersion} +AppVerName={#MyAppName} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL} @@ -54,7 +54,15 @@ Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: de Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent [UninstallRun] -Filename: taskkill.exe; Parameters: "/IM '{#MyAppExeName}' /F"; Flags: runhidden waituntilterminated; RunOnceId: UninstallKill +Filename: "taskkill.exe"; Parameters: "/IM ""{#MyAppExeName}"" /F"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallKill" +Filename: "schtasks.exe"; Parameters: "/Delete /TN ""ThreadPilot_Startup"" /F"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallRemoveThreadPilotStartupTask" +Filename: "reg.exe"; Parameters: "delete ""HKCU\Software\Microsoft\Windows\CurrentVersion\Run"" /v ""ThreadPilot"" /f"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallRemoveThreadPilotRunEntry" + +; ThreadPilot user data is preserved during install/update and removed only when +; the generated uninstaller runs. Per-user AppData cleanup is limited to the +; account context used by uninstall. +[UninstallDelete] +Type: filesandordirs; Name: "{userappdata}\ThreadPilot" [Code] diff --git a/Installer/ThreadPilot.wxs b/Installer/ThreadPilot.wxs index b72c9fe..8bcf6f1 100644 --- a/Installer/ThreadPilot.wxs +++ b/Installer/ThreadPilot.wxs @@ -7,7 +7,7 @@ diff --git a/Installer/setup.iss b/Installer/setup.iss index 25a2870..7fbed61 100644 --- a/Installer/setup.iss +++ b/Installer/setup.iss @@ -11,7 +11,7 @@ #endif #ifndef MyAppVersion - #define MyAppVersion "1.3.0" + #define MyAppVersion "1.4.0" #endif #ifndef MyAppSourceDir @@ -22,7 +22,7 @@ AppId={{E8F7A3B2-5C4D-4E6F-8A9B-1C2D3E4F5A6B} AppName={#MyAppName} AppVersion={#MyAppVersion} -AppVerName={#MyAppName} {#MyAppVersion} +AppVerName={#MyAppName} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL}/issues @@ -62,3 +62,49 @@ Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon ; Intentionally do not auto-launch after setup to keep package-manager installs unattended. +; ThreadPilot user data is preserved during install/update. Inno removes installed +; files and shortcuts automatically only when the generated uninstaller runs. +; Per-user AppData cleanup is limited to the account context used by uninstall. +[UninstallRun] +Filename: "taskkill.exe"; Parameters: "/IM ""{#MyAppExeName}"" /F"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallKillThreadPilot" +Filename: "schtasks.exe"; Parameters: "/Delete /TN ""ThreadPilot_Startup"" /F"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallRemoveThreadPilotStartupTask" +Filename: "reg.exe"; Parameters: "delete ""HKCU\Software\Microsoft\Windows\CurrentVersion\Run"" /v ""ThreadPilot"" /f"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallRemoveThreadPilotRunEntry" + +[UninstallDelete] +Type: filesandordirs; Name: "{userappdata}\ThreadPilot" + +[Code] +const + LegacyBetaUninstallKey = 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{A2A4C8B5-4A9A-4B1B-93F4-5F8B1C7E8C2A}_is1'; + LegacyBetaDisplayName = 'ThreadPilot 0.1.0-beta'; + +function IsLegacyThreadPilotInstallPath(InstallLocation: string): Boolean; +var + NormalizedLocation: string; + ExpectedInstallRoot: string; +begin + NormalizedLocation := Lowercase(RemoveBackslashUnlessRoot(RemoveQuotes(InstallLocation))); + ExpectedInstallRoot := Lowercase(RemoveBackslashUnlessRoot(ExpandConstant('{autopf}\ThreadPilot'))); + Result := (NormalizedLocation = ExpectedInstallRoot); +end; + +procedure DeleteLegacyBetaUninstallEntry(RootKey: Integer); +var + DisplayName: string; + InstallLocation: string; +begin + if RegQueryStringValue(RootKey, LegacyBetaUninstallKey, 'DisplayName', DisplayName) and + RegQueryStringValue(RootKey, LegacyBetaUninstallKey, 'InstallLocation', InstallLocation) and + (DisplayName = LegacyBetaDisplayName) and + IsLegacyThreadPilotInstallPath(InstallLocation) then + begin + RegDeleteKeyIncludingSubkeys(RootKey, LegacyBetaUninstallKey); + end; +end; + +function InitializeSetup(): Boolean; +begin + DeleteLegacyBetaUninstallEntry(HKLM); + DeleteLegacyBetaUninstallEntry(HKCU); + Result := True; +end; diff --git a/Locales/en-US.xaml b/Locales/en-US.xaml index 41967ab..e2bc6fd 100644 --- a/Locales/en-US.xaml +++ b/Locales/en-US.xaml @@ -30,6 +30,12 @@ Recommended Don't show again Open Settings + Primary navigation + Elevation warning overlay + Performance introduction overlay + Startup minimized suggestion + Unsaved settings dialog + Application startup loading overlay ThreadPilot Activity @@ -94,10 +100,13 @@ Delete selected mask CPUs CPU + Mask Options Optional Diagnostics Diagnostics are optional and intended for troubleshooting. For in-game overlays and detailed performance graphs, use dedicated tools. + Diagnostics are optional and intended for troubleshooting. + For in-game overlays and detailed performance graphs, use dedicated tools. Quick tips: 1. Open diagnostics only when you need a focused troubleshooting snapshot. 2. Review hotspots only as a hint before creating automation rules. @@ -128,6 +137,12 @@ Refresh the current dashboard snapshot Global Power Plan + Process Hotspots + Filter + Memory + Name + Priority + Window Memory Used CPU % Mem % @@ -180,6 +195,7 @@ Rule CPU Mask: Optional: Select a CPU mask to apply when this process starts Rule Priority + Update Optional: Select the process priority to apply when this process starts Association Priority: Higher priority associations take precedence when multiple match @@ -206,6 +222,7 @@ Save Configuration No automation rules yet Status: + Automation Monitoring Start Automation Monitoring Stop Automation Monitoring @@ -226,6 +243,7 @@ Process Management Search, filter, and control active process configurations Search processes by name + Process search Hide Windows system processes Hide System Processes Hide processes with very low CPU usage @@ -276,7 +294,11 @@ Refresh the process list Load More Load more processes - + Refresh processes + Load more processes + Running process list + Virtualized process table with sorting and selection + Process Name Window Title CPU Usage @@ -294,6 +316,32 @@ Apply Pending Settings Apply the pending affinity and selected power plan to the selected process Rules and changes are applied by ThreadPilot only when configured. + Current process status + Advanced affinity picker + Select the pending mask for row context-menu affinity actions + Selected power plan + Save as Rule + No visible window title + Batch + total) + Priority + CPU: unavailable + Memory: unavailable + CPU priority: unavailable + Memory priority unavailable + Affinity: unavailable + No saved rule + No recent ThreadPilot action + Selected process: {0} (PID {1}) + Current process status: protected or access denied + Current process status: selected + CPU: {0:N1}% + Memory: {0} + CPU priority: {0} + Affinity: legacy mask 0x{0:X} + Memory priority: {0} + saved rule + Saved rule exists: {0} Above Normal Below Normal @@ -388,6 +436,12 @@ Translator: Ylimhs License: AGPLv3 Check for updates + Download and install update + Check automatically on startup + Check interval (days): + Include prerelease updates + Latest version: + Last checked: Checks on demand using the official GitHub Releases. Reset to Defaults @@ -395,6 +449,55 @@ Import Configuration Save Settings ThreadPilot + Theme changed to {0}. + Failed to change theme to {0}. + Language changed to {0}. + Failed to change language. + Saving settings... + Settings saved with warnings: {0} + Settings saved and applied successfully. + Error saving settings: {0} + Resetting to defaults... + Settings reset to defaults (not saved yet) + Error resetting settings: {0} + Exporting configuration bundle... + Export canceled + Configuration exported to: {0} + Error exporting settings: {0} + Importing configuration... + Import canceled + Configuration bundle imported and applied + Legacy settings imported (rules unchanged) + Error importing settings: {0} + Test notification sent + Error sending test notification: {0} + Loading settings... + Settings loaded + Error loading settings: {0} + Settings synchronized + Checking for updates... + Unable to determine the latest version. + New version available: {0} + Application is up to date. Installed version: {0} + Error while checking updates: {0} + Unknown + Not checked + Never + Install ThreadPilot update + ThreadPilot will download and verify version {0}, then ask Windows for permission to run the installer. Continue? + Update canceled. + Downloading and verifying update... + Update installer started. + Update install failed: {0} + Settings have been modified + Settings match the saved configuration + Simplified Chinese + English + Dark + Light + Export ThreadPilot Configuration + Import ThreadPilot Configuration + Failed to update Windows autostart. Keeping previous autostart state. ThreadPilot Settings @@ -509,7 +612,30 @@ Game Boost mode deactivated after {0} Process Monitor Error Affinity blocked + Affinity applied + Affinity adjusted + Affinity failed + Affinity error Priority blocked + Priority warning + Priority applied + Priority adjusted + Priority error + Keyboard Shortcut + Toggle monitoring shortcut activated + High Performance power plan shortcut activated + Refresh process list shortcut activated + ThreadPilot Started + Process monitoring and power plan management is now active + Startup Error + Failed to start process monitoring manager + Automation Monitoring Error + Settings Saved + Application settings have been saved successfully + Settings Saved with Warnings + Settings Error + Failed to save settings + This is a test notification to verify your settings are working correctly. ThreadPilot requires administrator privileges to manage process affinity and power plans. Would you like to restart the application with administrator privileges? diff --git a/Locales/zh-CN.xaml b/Locales/zh-CN.xaml index 3a9ce20..d3cc1fa 100644 --- a/Locales/zh-CN.xaml +++ b/Locales/zh-CN.xaml @@ -30,6 +30,12 @@ 推荐 不再显示 打开设置 + 主导航 + 权限提升警告覆盖层 + 性能诊断介绍覆盖层 + 启动时最小化建议 + 未保存设置对话框 + 应用启动加载覆盖层 ThreadPilot 活动日志 @@ -94,10 +100,13 @@ 删除选定的掩码 CPU 核心 CPU + 掩码选项 可选性能诊断 诊断是可选的,仅用于故障排除。对于游戏内覆盖层和详细的性能图表,请使用专用工具。 + 诊断是可选的,仅用于故障排除。 + 对于游戏内覆盖层和详细的性能图表,请使用专用工具。 快速提示: 1. 仅在需要针对性的故障排除快照时才打开诊断。 2. 在创建自动化规则之前,仅将热点查看作为提示。 @@ -128,6 +137,12 @@ 刷新当前的仪表板快照 全局电源计划 + 进程热点 + 筛选 + 内存 + 名称 + 优先级 + 窗口 已用内存 CPU % 内存 % @@ -180,6 +195,7 @@ 规则 CPU 掩码: 可选: 选择在该进程启动时要应用的 CPU 掩码 规则优先级 + 更新 可选: 选择在该进程启动时要应用的进程优先级 关联优先级: 当有多个匹配时,优先级较高的关联优先使用 @@ -206,6 +222,7 @@ 保存配置 尚无自动化规则 状态: + 自动化监控 启动自动化监控 停止自动化监控 @@ -226,6 +243,7 @@ 系统进程管理 搜索、过滤和控制活动的进程配置 按名称搜索进程 + 进程搜索 隐藏 Windows 系统进程 隐藏系统进程 隐藏 CPU 使用率极低的进程 @@ -276,6 +294,10 @@ 刷新进程列表 加载更多 加载更多进程 + 刷新进程 + 加载更多进程 + 运行中进程列表 + 支持排序和选择的虚拟化进程表 进程名称 窗口标题 @@ -294,6 +316,32 @@ 应用待处理设置 将待处理的关联性和选定的电源计划应用于选定的进程 ThreadPilot 仅在配置后才应用规则和更改。 + 当前进程状态 + 高级关联性选择器 + 为行上下文菜单关联性操作选择待处理掩码 + 所选电源计划 + 保存为规则 + 无可见窗口标题 + 批次 + 总计) + 优先级 + CPU: 不可用 + 内存: 不可用 + CPU 优先级: 不可用 + 内存优先级不可用 + 关联性: 不可用 + 无已保存规则 + 暂无最近的 ThreadPilot 操作 + 选定进程: {0} (PID {1}) + 当前进程状态: 受保护或访问被拒绝 + 当前进程状态: 已选择 + CPU: {0:N1}% + 内存: {0} + CPU 优先级: {0} + 关联性: 旧版掩码 0x{0:X} + 内存优先级: {0} + 已保存规则 + 存在已保存规则: {0} 高于正常 低于正常 @@ -388,6 +436,12 @@ 翻译人员:Ylimhs 软件授权: AGPLv3 检查新版本 + 下载并安装更新 + 启动时自动检查更新 + 检查间隔(天): + 包含预发布版本 + 最新版本: + 上次检查: 按需通过官方 GitHub Releases API 检索最新稳定版本。 重置为默认值 @@ -395,6 +449,55 @@ 导入配置包 保存并应用设置 ThreadPilot + 主题已切换为{0}。 + 无法切换主题为{0}。 + 语言已切换为{0}。 + 无法切换语言。 + 正在保存设置... + 设置已保存但有警告: {0} + 设置已成功保存并应用。 + 保存设置时出错: {0} + 正在重置为默认值... + 设置已重置为默认值(尚未保存) + 重置设置时出错: {0} + 正在导出配置包... + 已取消导出 + 配置已导出到: {0} + 导出设置时出错: {0} + 正在导入配置... + 已取消导入 + 配置包已导入并应用 + 旧版设置已导入(规则未更改) + 导入设置时出错: {0} + 测试通知已发送 + 发送测试通知时出错: {0} + 正在加载设置... + 设置已加载 + 加载设置时出错: {0} + 设置已同步 + 正在检查更新... + 无法确定最新版本。 + 发现新版本: {0} + 应用已是最新版本。已安装版本: {0} + 检查更新时出错: {0} + 未知 + 尚未检查 + 从未检查 + 安装 ThreadPilot 更新 + ThreadPilot 将下载并验证版本 {0},然后请求 Windows 权限运行安装程序。是否继续? + 更新已取消。 + 正在下载并验证更新... + 更新安装程序已启动。 + 更新安装失败: {0} + 设置已被修改 + 设置与已保存的配置一致 + 简体中文 + 英文 + 深色 + 浅色 + 导出 ThreadPilot 配置 + 导入 ThreadPilot 配置 + 无法更新 Windows 自启动。将保留之前的自启动状态。 ThreadPilot 设置 @@ -509,7 +612,30 @@ 游戏加速模式已关闭,持续时间: {0} 进程监控器错误 关联性应用被阻止 + 关联性已应用 + 关联性已调整 + 关联性应用失败 + 关联性错误 优先级应用被阻止 + 优先级警告 + 优先级已应用 + 优先级已调整 + 优先级错误 + 键盘快捷键 + 切换监控快捷键已触发 + 高性能电源计划快捷键已触发 + 刷新进程列表快捷键已触发 + ThreadPilot 已启动 + 进程监控和电源计划管理现已激活 + 启动错误 + 无法启动进程监控管理器 + 自动化监控错误 + 设置已保存 + 应用设置已成功保存 + 设置已保存但有警告 + 设置错误 + 无法保存设置 + 这是一条用于验证您的设置是否正常工作的测试通知。 ThreadPilot 需要管理员权限来管理进程关联性和电源计划。 您想以管理员权限重新启动应用程序吗? diff --git a/MainWindow.Behaviors.partial.cs b/MainWindow.Behaviors.partial.cs index a4da578..1c857ee 100644 --- a/MainWindow.Behaviors.partial.cs +++ b/MainWindow.Behaviors.partial.cs @@ -157,27 +157,26 @@ private async Task CheckForUpdatesAtStartupAsync() try { this.LogDebug("Startup update check started"); - var checker = this.serviceProvider.GetRequiredService(); - var currentVersion = GetCurrentApplicationVersion(); - var (latest, _) = await checker.GetLatestVersionAsync("PrimeBuild-pc", "ThreadPilot"); + var updateService = this.serviceProvider.GetRequiredService(); + var result = await updateService.CheckForUpdatesAsync(new UpdateCheckRequest(UpdateCheckTrigger.Startup)); - if (latest == null) + if (result.Status == UpdateCheckStatus.Skipped) { - this.LogDebug("Startup update check completed without release information"); + this.LogDebug($"Startup update check skipped: {result.Message}"); return; } - if (latest <= currentVersion) + if (!result.IsUpdateAvailable || result.Release == null) { - this.LogDebug($"Startup update check complete: installed {currentVersion}, latest {latest}"); + this.LogDebug($"Startup update check complete: {result.Message}"); return; } await this.notificationService.ShowNotificationAsync( "Update available", - $"ThreadPilot {latest} is available from GitHub releases.", + $"ThreadPilot {result.Release.Version} is available. Open Settings to download and install it.", NotificationType.Information); - this.LogDebug($"Startup update check found update: installed {currentVersion}, latest {latest}"); + this.LogDebug($"Startup update check found update: installed {result.CurrentVersion}, latest {result.Release.Version}"); } catch (Exception ex) { diff --git a/MainWindow.xaml b/MainWindow.xaml index 7a203ed..c991768 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -10,7 +10,7 @@ mc:Ignorable="d" ExtendsContentIntoTitleBar="True" WindowBackdropType="Mica" - Title="ThreadPilot - Process & Power Plan Manager" + Title="{DynamicResource MainWindow_Title}" Height="864" Width="1280" WindowStartupLocation="CenterScreen" @@ -86,7 +86,7 @@ IsPaneOpen="False" OpenPaneLength="180" CompactPaneLength="52" - AutomationProperties.Name="Primary navigation"> + AutomationProperties.Name="{DynamicResource MainWindow_PrimaryNavigation}"> - - - + @@ -290,22 +290,22 @@ - + - + - + - + - - + - - + - + @@ -168,9 +168,9 @@ - + - - + @@ -219,20 +219,20 @@ - + - @@ -583,7 +583,7 @@ CornerRadius="8"> - @@ -595,7 +595,7 @@ Background="{DynamicResource QuietRowBackgroundBrush}" Margin="0,0,0,12"> - @@ -610,7 +610,7 @@ - - + - + + ToolTip="{DynamicResource ProcessView_AffinityCurrentTooltip}"/> + ToolTip="{DynamicResource ProcessView_AffinityStagedTooltip}"/> - + + ToolTip="{DynamicResource ProcessView_StageMaskTooltip}"> @@ -700,7 +700,7 @@ FontSize="{StaticResource ProcessFontSmall}" Margin="0,2,0,6" Foreground="{Binding IsHyperThreadingActive, Converter={StaticResource BoolToColorConverter}}" - ToolTip="Shows whether Hyper-Threading (Intel) or SMT (AMD) is present and active on this system"/> + ToolTip="{DynamicResource ProcessView_HyperThreadingTooltip}"/> + ToolTip="{DynamicResource ProcessView_CpuSelectionPreviewTooltip}"/> @@ -736,59 +736,59 @@ - + - - - - - - + + + + + +