diff --git a/.gitignore b/.gitignore index c701bc1..44b901e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,9 @@ obj/ *.user *.suo dist/ +dist-svc/ +dist-installer/ *.log +wix/obj/ +wix/Harvested.wxs +wix/Harvested-Service.wxs diff --git a/ChromeboxFanControl.sln b/ChromeboxFanControl.sln index 3666aad..92d589f 100644 --- a/ChromeboxFanControl.sln +++ b/ChromeboxFanControl.sln @@ -3,6 +3,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChromeboxFanControl", "ChromeboxFanControl\ChromeboxFanControl.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChromeboxFanControlService", "ChromeboxFanControlService\ChromeboxFanControlService.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -13,5 +15,9 @@ Global {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/ChromeboxFanControl/AppConfig.cs b/ChromeboxFanControl/AppConfig.cs index abe51a1..25221dc 100644 --- a/ChromeboxFanControl/AppConfig.cs +++ b/ChromeboxFanControl/AppConfig.cs @@ -5,6 +5,8 @@ namespace ChromeboxFanControl; public sealed class AppConfig { + /// UI language: "auto" (system), "en", "zh-Hans", "zh-Hant". Restart to apply. + public string Language { get; set; } = "auto"; public string EctoolPath { get; set; } = @"C:\Program Files\crosec\ectool.exe"; public int PollIntervalMs { get; set; } = 1500; /// Call ectool for RPM every N control cycles (1 = every cycle). @@ -33,7 +35,9 @@ public sealed class AppConfig public static string UserConfigPath => Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.UserInteractive + ? Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + : Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "ChromeboxFanControl", "config.json"); @@ -66,6 +70,8 @@ public sealed class AppConfig if (src == null) return; + if (!string.IsNullOrEmpty(src.Language)) + dst.Language = src.Language; dst.EctoolPath = src.EctoolPath; dst.PollIntervalMs = Math.Clamp(src.PollIntervalMs, 250, 60_000); dst.FanRpmPollEveryNCycles = Math.Max(1, src.FanRpmPollEveryNCycles); diff --git a/ChromeboxFanControl/ChromeboxFanControl.csproj b/ChromeboxFanControl/ChromeboxFanControl.csproj index 8df6c90..b3ae4d5 100644 --- a/ChromeboxFanControl/ChromeboxFanControl.csproj +++ b/ChromeboxFanControl/ChromeboxFanControl.csproj @@ -1,6 +1,7 @@ + $(NoWarn);NU1701 WinExe net8.0-windows enable @@ -24,4 +25,8 @@ + + en;zh-Hans;zh-Hant + + diff --git a/ChromeboxFanControl/MainForm.cs b/ChromeboxFanControl/MainForm.cs index 168b935..a70b4b0 100644 --- a/ChromeboxFanControl/MainForm.cs +++ b/ChromeboxFanControl/MainForm.cs @@ -1,3 +1,4 @@ +using ChromeboxFanControl.Properties; using ScottPlot; using ScottPlot.WinForms; @@ -18,6 +19,7 @@ public sealed class MainForm : Form private readonly TextBox _txtFanDutyArgs = new(); private readonly TextBox _txtAutoFanArgs = new(); private readonly ComboBox _cmbTempSource = new(); + private readonly ComboBox _cmbLanguage = new(); private readonly NumericUpDown _nudFailCount = new(); private readonly NumericUpDown _nudFailPct = new(); private readonly CheckBox _chkFailAuto = new(); @@ -25,7 +27,7 @@ public sealed class MainForm : Form private readonly NumericUpDown _nudChartPoints = new(); private readonly NotifyIcon _tray = new(); private readonly ContextMenuStrip _trayMenu = new(); - private readonly ToolStripMenuItem _miPause = new("暂停控制"); + private readonly ToolStripMenuItem _miPause; private readonly object _seriesLock = new(); private readonly List _tSec = new(); private readonly List _temps = new(); @@ -37,7 +39,8 @@ public sealed class MainForm : Form public MainForm() { - Text = "Chromebox 风扇温控"; + _miPause = new ToolStripMenuItem(Resources.TrayPause); + Text = Resources.AppTitle; Size = new Size(960, 700); StartPosition = FormStartPosition.CenterScreen; MinimumSize = new Size(800, 500); @@ -62,9 +65,9 @@ public sealed class MainForm : Form { var tabs = new TabControl { Dock = DockStyle.Fill }; - var tabMonitor = new TabPage("监控"); - var tabCurve = new TabPage("曲线"); - var tabAdv = new TabPage("高级"); + var tabMonitor = new TabPage(Resources.TabMonitor); + var tabCurve = new TabPage(Resources.TabCurve); + var tabAdv = new TabPage(Resources.TabAdvanced); tabs.TabPages.Add(tabMonitor); tabs.TabPages.Add(tabCurve); @@ -75,7 +78,7 @@ public sealed class MainForm : Form _lblStatus.Dock = DockStyle.Bottom; _lblStatus.Padding = new Padding(8, 4, 8, 4); _lblStatus.AutoEllipsis = true; - _lblStatus.Text = "启动中…"; + _lblStatus.Text = Resources.StatusStarting; var panelChart = new Panel { Dock = DockStyle.Fill }; panelChart.Controls.Add(_chart); @@ -91,14 +94,14 @@ public sealed class MainForm : Form _gridCurve.Columns.Add(new DataGridViewTextBoxColumn { Name = "colTemp", - HeaderText = "温度 (°C)", + HeaderText = Resources.GridColTemp, ReadOnly = true, Width = 80 }); _gridCurve.Columns.Add(new DataGridViewTextBoxColumn { Name = "colDuty", - HeaderText = "占空比 0–100", + HeaderText = Resources.GridColDuty, Width = 100 }); for (var i = 0; i < 14; i++) @@ -112,9 +115,9 @@ public sealed class MainForm : Form FlowDirection = FlowDirection.LeftToRight, Padding = new Padding(8) }; - var btnSave = new Button { Text = "保存并应用", AutoSize = true }; + var btnSave = new Button { Text = Resources.BtnSaveApply, AutoSize = true }; btnSave.Click += (_, _) => SaveFromUi(); - var btnDefault = new Button { Text = "恢复默认曲线", AutoSize = true }; + var btnDefault = new Button { Text = Resources.BtnRestoreDefault, AutoSize = true }; btnDefault.Click += (_, _) => { var def = new AppConfig(); @@ -138,41 +141,45 @@ public sealed class MainForm : Form { Dock = DockStyle.Fill, ColumnCount = 2, - RowCount = 12, + RowCount = 13, Padding = new Padding(12) }; flp.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 35)); flp.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 65)); var row = 0; - AddRow(flp, row++, "ectool 路径", _txtEctool); - AddRow(flp, row++, "轮询间隔 (ms)", _nudPoll); + AddRow(flp, row++, Resources.LblLanguage, _cmbLanguage); + _cmbLanguage.DropDownStyle = ComboBoxStyle.DropDownList; + _cmbLanguage.Items.AddRange([Resources.LangAuto, Resources.LangEn, Resources.LangZhHans, Resources.LangZhHant]); + + AddRow(flp, row++, Resources.LblEctoolPath, _txtEctool); + AddRow(flp, row++, Resources.LblPollInterval, _nudPoll); _nudPoll.Minimum = 250; _nudPoll.Maximum = 60_000; _nudPoll.Increment = 250; - AddRow(flp, row++, "每 N 轮读 RPM", _nudRpmEvery); + AddRow(flp, row++, Resources.LblRpmEvery, _nudRpmEvery); _nudRpmEvery.Minimum = 1; _nudRpmEvery.Maximum = 100; - AddRow(flp, row++, "读转速参数 (空格分隔)", _txtFanRpmArgs); - AddRow(flp, row++, "读占空比参数 (空=跳过)", _txtFanDutyArgs); + AddRow(flp, row++, Resources.LblFanRpmArgs, _txtFanRpmArgs); + AddRow(flp, row++, Resources.LblFanDutyArgs, _txtFanDutyArgs); - AddRow(flp, row++, "恢复自动风扇参数", _txtAutoFanArgs); + AddRow(flp, row++, Resources.LblAutoFanArgs, _txtAutoFanArgs); - AddRow(flp, row++, "温度来源", _cmbTempSource); + AddRow(flp, row++, Resources.LblTempSource, _cmbTempSource); _cmbTempSource.DropDownStyle = ComboBoxStyle.DropDownList; _cmbTempSource.Items.AddRange(["AverageCore", "MaxCore"]); - AddRow(flp, row++, "连续失败进入安全模式 (次)", _nudFailCount); + AddRow(flp, row++, Resources.LblFailCount, _nudFailCount); _nudFailCount.Minimum = 1; _nudFailCount.Maximum = 100; - AddRow(flp, row++, "安全模式固定占空比 (%)", _nudFailPct); + AddRow(flp, row++, Resources.LblFailPct, _nudFailPct); _nudFailPct.Minimum = 0; _nudFailPct.Maximum = 100; - _chkFailAuto.Text = "安全模式使用 autofanctrl(不用固定占空比)"; + _chkFailAuto.Text = Resources.ChkFailAuto; _chkFailAuto.AutoSize = true; var failPanel = new FlowLayoutPanel { @@ -182,19 +189,19 @@ public sealed class MainForm : Form WrapContents = false }; failPanel.Controls.Add(_chkFailAuto); - flp.Controls.Add(new System.Windows.Forms.Label { Text = "安全模式策略", AutoSize = true, Anchor = AnchorStyles.Left }, 0, row); + flp.Controls.Add(new System.Windows.Forms.Label { Text = Resources.LblFailStrategy, AutoSize = true, Anchor = AnchorStyles.Left }, 0, row); flp.Controls.Add(failPanel, 1, row++); - AddRow(flp, row++, "图表保留时长 (分钟)", _nudChartMin); + AddRow(flp, row++, Resources.LblChartMin, _nudChartMin); _nudChartMin.Minimum = 1; _nudChartMin.Maximum = 120; - AddRow(flp, row++, "图表最大点数", _nudChartPoints); + AddRow(flp, row++, Resources.LblChartPoints, _nudChartPoints); _nudChartPoints.Minimum = 100; _nudChartPoints.Maximum = 10_000; _nudChartPoints.Increment = 100; - var btnSaveAdv = new Button { Text = "保存高级设置", AutoSize = true, Dock = DockStyle.Bottom }; + var btnSaveAdv = new Button { Text = Resources.BtnSaveAdvanced, AutoSize = true, Dock = DockStyle.Bottom }; btnSaveAdv.Click += (_, _) => SaveFromUi(); tabAdv.Controls.Add(btnSaveAdv); tabAdv.Controls.Add(flp); @@ -211,7 +218,7 @@ public sealed class MainForm : Form private void WireTray() { - _tray.Text = "Chromebox 风扇温控"; + _tray.Text = Resources.AppTitle; _tray.Visible = true; _tray.Icon = SystemIcons.Application; _tray.DoubleClick += (_, _) => ShowFromTray(); @@ -219,13 +226,11 @@ public sealed class MainForm : Form { TogglePause(); }; - _trayMenu.Items.Add("打开主窗口", null, (_, _) => ShowFromTray()); + _trayMenu.Items.Add(Resources.TrayOpen, null, (_, _) => ShowFromTray()); _trayMenu.Items.Add(_miPause); - _trayMenu.Items.Add("关于", null, (_, _) => - MessageBox.Show(this, - "Chromebox 风扇温控\n\n使用 LibreHardwareMonitor 读取 CPU 温度,通过 crosec ectool 控制风扇。\n\n请以管理员身份运行。", - Text, MessageBoxButtons.OK, MessageBoxIcon.Information)); - _trayMenu.Items.Add("退出", null, (_, _) => Close()); + _trayMenu.Items.Add(Resources.TrayAbout, null, (_, _) => + MessageBox.Show(this, Resources.AboutMessage, Text, MessageBoxButtons.OK, MessageBoxIcon.Information)); + _trayMenu.Items.Add(Resources.TrayExit, null, (_, _) => Close()); _tray.ContextMenuStrip = _trayMenu; } @@ -242,11 +247,16 @@ public sealed class MainForm : Form if (_fan == null) return; _fan.Paused = !_fan.Paused; - _miPause.Text = _fan.Paused ? "恢复控制" : "暂停控制"; + _miPause.Text = _fan.Paused ? Resources.TrayResume : Resources.TrayPause; } + private static readonly string[] LanguageValues = ["auto", "en", "zh-Hans", "zh-Hant"]; + private void ApplyConfigToUi() { + var langIdx = Array.IndexOf(LanguageValues, _config.Language); + _cmbLanguage.SelectedIndex = langIdx >= 0 ? langIdx : 0; + _txtEctool.Text = _config.EctoolPath; _nudPoll.Value = _config.PollIntervalMs; _nudRpmEvery.Value = _config.FanRpmPollEveryNCycles; @@ -268,8 +278,12 @@ public sealed class MainForm : Form private AppConfig CloneConfigFromUi() { + var langIdx = _cmbLanguage.SelectedIndex; + var lang = langIdx >= 0 && langIdx < LanguageValues.Length ? LanguageValues[langIdx] : "auto"; + var c = new AppConfig { + Language = lang, EctoolPath = _txtEctool.Text.Trim(), PollIntervalMs = (int)_nudPoll.Value, FanRpmPollEveryNCycles = (int)_nudRpmEvery.Value, @@ -313,8 +327,8 @@ public sealed class MainForm : Form var plt = _curveChart.Plot; plt.Clear(); - plt.Axes.Left.Label.Text = "占空比 (%)"; - plt.Axes.Bottom.Label.Text = "温度 (°C)"; + plt.Axes.Left.Label.Text = Resources.CurveChartDuty; + plt.Axes.Bottom.Label.Text = Resources.CurveChartTemp; plt.Axes.SetLimitsX(0, 100); plt.Axes.SetLimitsY(0, 100); @@ -326,6 +340,7 @@ public sealed class MainForm : Form scatter.MarkerSize = 6; } + plt.Font.Automatic(); _curveChart.Refresh(); } @@ -336,21 +351,25 @@ public sealed class MainForm : Form var rpmArgs = SplitArgs(_txtFanRpmArgs.Text); if (rpmArgs.Length == 0) { - MessageBox.Show(this, "读转速参数不能为空。", Text, MessageBoxButtons.OK, MessageBoxIcon.Warning); + MessageBox.Show(this, Resources.ErrRpmArgsEmpty, Text, MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } var autoArgs = SplitArgs(_txtAutoFanArgs.Text); if (autoArgs.Length == 0) { - MessageBox.Show(this, "恢复自动风扇参数不能为空。", Text, MessageBoxButtons.OK, MessageBoxIcon.Warning); + MessageBox.Show(this, Resources.ErrAutoFanArgsEmpty, Text, MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } + var langBefore = _config.Language; _config = CloneConfigFromUi(); _config.SaveUser(); _fan?.UpdateConfig(_config); - MessageBox.Show(this, "已保存。", Text, MessageBoxButtons.OK, MessageBoxIcon.Information); + var msg = Resources.MsgSaved; + if (langBefore != _config.Language) + msg = Resources.MsgRestartToApplyLanguage; + MessageBox.Show(this, msg, Text, MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { @@ -388,11 +407,11 @@ public sealed class MainForm : Form var dVal = e.ActualDutyPercent ?? e.TargetDutyPercent; var dStr = dVal.HasValue ? $"{dVal} %" : "—"; var rStr = e.Rpm.HasValue ? $"{e.Rpm} RPM" : "—"; - var state = e.Paused ? "已暂停" : e.FailSafe ? "安全模式" : "运行中"; + var state = e.Paused ? Resources.StatusPaused : e.FailSafe ? Resources.StatusFailSafe : Resources.StatusRunning; _lblStatus.Text = - $"{state} 温度: {tStr} 目标占空比: {dStr} 转速: {rStr} ectool: {e.LastEctoolMessage ?? "—"}"; + $"{state} {Resources.LabelTemp} {tStr} {Resources.LabelTargetDuty} {dStr} {Resources.LabelRpm} {rStr} ectool: {e.LastEctoolMessage ?? "—"}"; - _tray.Text = $"Chromebox {tStr} {dStr}"; + _tray.Text = $"{Resources.AppTitle} {tStr} {dStr}"; RebuildChart(); } @@ -435,14 +454,14 @@ public sealed class MainForm : Form var plt = _chart.Plot; plt.Clear(); - plt.Axes.Left.Label.Text = "Temperature (°C)"; - plt.Axes.Bottom.Label.Text = "Time (s)"; + plt.Axes.Left.Label.Text = Resources.ChartTemp; + plt.Axes.Bottom.Label.Text = Resources.ChartTime; plt.Axes.Right.IsVisible = true; - plt.Axes.Right.Label.Text = "Duty (%)"; + plt.Axes.Right.Label.Text = Resources.ChartDuty; if (_yRpmAxis == null) _yRpmAxis = plt.Axes.AddRightAxis(); - _yRpmAxis.Label.Text = "RPM"; + _yRpmAxis.Label.Text = Resources.ChartRpm; if (x.Length > 0) { @@ -509,12 +528,13 @@ public sealed class MainForm : Form LegendItem[] leg = [ - new() { LineColor = Colors.Blue, LineWidth = 2, LabelText = "°C" }, - new() { LineColor = Colors.Orange, LineWidth = 2, LabelText = "Duty %" }, - new() { LineColor = Colors.Green, LineWidth = 2, LabelText = "RPM" } + new() { LineColor = Colors.Blue, LineWidth = 2, LabelText = Resources.LegendCelsius }, + new() { LineColor = Colors.Orange, LineWidth = 2, LabelText = Resources.LegendDuty }, + new() { LineColor = Colors.Green, LineWidth = 2, LabelText = Resources.ChartRpm } ]; plt.ShowLegend(leg, Alignment.UpperRight); plt.Legend.Orientation = ScottPlot.Orientation.Horizontal; + plt.Font.Automatic(); _chart.Refresh(); } diff --git a/ChromeboxFanControl/Program.cs b/ChromeboxFanControl/Program.cs index 81aa215..296d9c8 100644 --- a/ChromeboxFanControl/Program.cs +++ b/ChromeboxFanControl/Program.cs @@ -6,6 +6,22 @@ internal static class Program private static void Main() { ApplicationConfiguration.Initialize(); + + var config = AppConfig.LoadMerged(); + var lang = config.Language; + if (string.IsNullOrEmpty(lang) || lang.Equals("auto", StringComparison.OrdinalIgnoreCase)) + { + var culture = System.Globalization.CultureInfo.CurrentUICulture; + if (culture.Name is "zh-TW" or "zh-HK" or "zh-MO" or "zh-Hant") + lang = "zh-Hant"; + else if (culture.Name.StartsWith("zh", StringComparison.OrdinalIgnoreCase)) + lang = "zh-Hans"; + else + lang = "en"; + } + + System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo(lang); + Application.Run(new MainForm()); } } diff --git a/ChromeboxFanControl/Properties/Resources.Designer.cs b/ChromeboxFanControl/Properties/Resources.Designer.cs new file mode 100644 index 0000000..f3901a7 --- /dev/null +++ b/ChromeboxFanControl/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +using System.Resources; + +namespace ChromeboxFanControl.Properties; + +internal static class Resources +{ + private static readonly ResourceManager _rm = new( + "ChromeboxFanControl.Properties.Resources", + typeof(Resources).Assembly); + + public static string AppTitle => _rm.GetString("AppTitle") ?? "Chromebox Fan Control"; + public static string TabMonitor => _rm.GetString("TabMonitor") ?? "Monitoring"; + public static string TabCurve => _rm.GetString("TabCurve") ?? "Curve"; + public static string TabAdvanced => _rm.GetString("TabAdvanced") ?? "Advanced"; + public static string StatusStarting => _rm.GetString("StatusStarting") ?? "Starting…"; + public static string StatusRunning => _rm.GetString("StatusRunning") ?? "Running"; + public static string StatusPaused => _rm.GetString("StatusPaused") ?? "Paused"; + public static string StatusFailSafe => _rm.GetString("StatusFailSafe") ?? "Fail-safe mode"; + public static string GridColTemp => _rm.GetString("GridColTemp") ?? "Temp (°C)"; + public static string GridColDuty => _rm.GetString("GridColDuty") ?? "Duty 0–100"; + public static string BtnSaveApply => _rm.GetString("BtnSaveApply") ?? "Save & Apply"; + public static string BtnRestoreDefault => _rm.GetString("BtnRestoreDefault") ?? "Restore Default"; + public static string LblEctoolPath => _rm.GetString("LblEctoolPath") ?? "ectool path"; + public static string LblPollInterval => _rm.GetString("LblPollInterval") ?? "Poll interval (ms)"; + public static string LblRpmEvery => _rm.GetString("LblRpmEvery") ?? "Read RPM every N"; + public static string LblFanRpmArgs => _rm.GetString("LblFanRpmArgs") ?? "RPM args"; + public static string LblFanDutyArgs => _rm.GetString("LblFanDutyArgs") ?? "Duty args"; + public static string LblAutoFanArgs => _rm.GetString("LblAutoFanArgs") ?? "Auto fan args"; + public static string LblTempSource => _rm.GetString("LblTempSource") ?? "Temp source"; + public static string LblFailCount => _rm.GetString("LblFailCount") ?? "Fail count"; + public static string LblFailPct => _rm.GetString("LblFailPct") ?? "Fail-safe %"; + public static string ChkFailAuto => _rm.GetString("ChkFailAuto") ?? "Use autofanctrl"; + public static string LblFailStrategy => _rm.GetString("LblFailStrategy") ?? "Fail-safe strategy"; + public static string LblChartMin => _rm.GetString("LblChartMin") ?? "Chart minutes"; + public static string LblChartPoints => _rm.GetString("LblChartPoints") ?? "Chart points"; + public static string BtnSaveAdvanced => _rm.GetString("BtnSaveAdvanced") ?? "Save Advanced"; + public static string TrayOpen => _rm.GetString("TrayOpen") ?? "Show window"; + public static string TrayPause => _rm.GetString("TrayPause") ?? "Pause"; + public static string TrayResume => _rm.GetString("TrayResume") ?? "Resume"; + public static string TrayAbout => _rm.GetString("TrayAbout") ?? "About"; + public static string TrayExit => _rm.GetString("TrayExit") ?? "Exit"; + public static string AboutMessage => _rm.GetString("AboutMessage") ?? ""; + public static string ErrRpmArgsEmpty => _rm.GetString("ErrRpmArgsEmpty") ?? "RPM args required."; + public static string ErrAutoFanArgsEmpty => _rm.GetString("ErrAutoFanArgsEmpty") ?? "Auto fan args required."; + public static string MsgSaved => _rm.GetString("MsgSaved") ?? "Saved."; + public static string LabelTemp => _rm.GetString("LabelTemp") ?? "Temp:"; + public static string LabelTargetDuty => _rm.GetString("LabelTargetDuty") ?? "Target duty:"; + public static string LabelRpm => _rm.GetString("LabelRpm") ?? "RPM:"; + public static string ChartTemp => _rm.GetString("ChartTemp") ?? "Temperature (°C)"; + public static string ChartTime => _rm.GetString("ChartTime") ?? "Time (s)"; + public static string ChartDuty => _rm.GetString("ChartDuty") ?? "Duty (%)"; + public static string ChartRpm => _rm.GetString("ChartRpm") ?? "RPM"; + public static string LegendCelsius => _rm.GetString("LegendCelsius") ?? "°C"; + public static string LegendDuty => _rm.GetString("LegendDuty") ?? "Duty %"; + public static string CurveChartDuty => _rm.GetString("CurveChartDuty") ?? "Duty (%)"; + public static string CurveChartTemp => _rm.GetString("CurveChartTemp") ?? "Temp (°C)"; + public static string LblLanguage => _rm.GetString("LblLanguage") ?? "Language"; + public static string LangAuto => _rm.GetString("LangAuto") ?? "Auto"; + public static string LangEn => _rm.GetString("LangEn") ?? "English"; + public static string LangZhHans => _rm.GetString("LangZhHans") ?? "Simplified Chinese"; + public static string LangZhHant => _rm.GetString("LangZhHant") ?? "Traditional Chinese"; + public static string MsgRestartToApplyLanguage => _rm.GetString("MsgRestartToApplyLanguage") ?? "Restart to apply."; +} diff --git a/ChromeboxFanControl/Properties/Resources.resx b/ChromeboxFanControl/Properties/Resources.resx new file mode 100644 index 0000000..45d11df --- /dev/null +++ b/ChromeboxFanControl/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + Chromebox Fan Control + Monitoring + Curve + Advanced + Starting… + Running + Paused + Fail-safe mode + Temp (°C) + Duty 0–100 + Save & Apply + Restore Default Curve + ectool path + Poll interval (ms) + Read RPM every N cycles + RPM args (space-separated) + Duty args (empty=skip) + Restore auto fan args + Temp source + Consecutive errors before fail-safe + Fail-safe fixed duty (%) + Use autofanctrl in fail-safe (not fixed duty) + Fail-safe strategy + Chart history (minutes) + Chart max points + Save Advanced Settings + Show window + Pause control + Resume control + About + Exit + Chromebox Fan Control + +Uses LibreHardwareMonitor to read CPU temperature, controls fan via crosec ectool. + +Run as Administrator. + RPM args cannot be empty. + Restore auto fan args cannot be empty. + Saved. + Temp: + Target duty: + RPM: + Temperature (°C) + Time (s) + Duty (%) + RPM + °C + Duty % + Duty (%) + Temp (°C) + Language + Auto (follow system) + English + Simplified Chinese + Traditional Chinese + Language changed. Restart the application to apply. + diff --git a/ChromeboxFanControl/Properties/Resources.zh-Hans.resx b/ChromeboxFanControl/Properties/Resources.zh-Hans.resx new file mode 100644 index 0000000..e4e7edf --- /dev/null +++ b/ChromeboxFanControl/Properties/Resources.zh-Hans.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + Chromebox 风扇温控 + 监控 + 曲线 + 高级 + 启动中… + 运行中 + 已暂停 + 安全模式 + 温度 (°C) + 占空比 0–100 + 保存并应用 + 恢复默认曲线 + ectool 路径 + 轮询间隔 (ms) + 每 N 轮读 RPM + 读转速参数 (空格分隔) + 读占空比参数 (空=跳过) + 恢复自动风扇参数 + 温度来源 + 连续失败进入安全模式 (次) + 安全模式固定占空比 (%) + 安全模式使用 autofanctrl(不用固定占空比) + 安全模式策略 + 图表保留时长 (分钟) + 图表最大点数 + 保存高级设置 + 打开主窗口 + 暂停控制 + 恢复控制 + 关于 + 退出 + Chromebox 风扇温控 + +使用 LibreHardwareMonitor 读取 CPU 温度,通过 crosec ectool 控制风扇。 + +请以管理员身份运行。 + 读转速参数不能为空。 + 恢复自动风扇参数不能为空。 + 已保存。 + 温度: + 目标占空比: + 转速: + 温度 (°C) + 时间 (s) + 占空比 (%) + 转速 + °C + 占空比 % + 占空比 (%) + 温度 (°C) + 语言 + 跟随系统 + English + 简体中文 + 繁体中文 + 语言已更改,请重启应用以生效。 + diff --git a/ChromeboxFanControl/Properties/Resources.zh-Hant.resx b/ChromeboxFanControl/Properties/Resources.zh-Hant.resx new file mode 100644 index 0000000..d62e940 --- /dev/null +++ b/ChromeboxFanControl/Properties/Resources.zh-Hant.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + Chromebox 風扇溫控 + 監控 + 曲線 + 進階 + 啟動中… + 運行中 + 已暫停 + 安全模式 + 溫度 (°C) + 占空比 0–100 + 儲存並套用 + 恢復預設曲線 + ectool 路徑 + 輪詢間隔 (ms) + 每 N 輪讀 RPM + 讀轉速參數 (空格分隔) + 讀占空比參數 (空=略過) + 恢復自動風扇參數 + 溫度來源 + 連續失敗進入安全模式 (次) + 安全模式固定占空比 (%) + 安全模式使用 autofanctrl(不用固定占空比) + 安全模式策略 + 圖表保留時長 (分鐘) + 圖表最大點數 + 儲存進階設定 + 開啟主視窗 + 暫停控制 + 恢復控制 + 關於 + 結束 + Chromebox 風扇溫控 + +使用 LibreHardwareMonitor 讀取 CPU 溫度,透過 crosec ectool 控制風扇。 + +請以系統管理員身分執行。 + 讀轉速參數不能為空。 + 恢復自動風扇參數不能為空。 + 已儲存。 + 溫度: + 目標占空比: + 轉速: + 溫度 (°C) + 時間 (s) + 占空比 (%) + 轉速 + °C + 占空比 % + 占空比 (%) + 溫度 (°C) + 語言 + 跟隨系統 + English + 簡體中文 + 繁體中文 + 語言已變更,請重新啟動應用程式以生效。 + diff --git a/ChromeboxFanControlService/ChromeboxFanControlService.csproj b/ChromeboxFanControlService/ChromeboxFanControlService.csproj new file mode 100644 index 0000000..d56dcac --- /dev/null +++ b/ChromeboxFanControlService/ChromeboxFanControlService.csproj @@ -0,0 +1,27 @@ + + + + $(NoWarn);NU1701 + net8.0-windows + enable + enable + ChromeboxFanControlService + ChromeboxFanControlService + Exe + + + + + + + + + + + + + + + + + diff --git a/ChromeboxFanControlService/FanControlWorker.cs b/ChromeboxFanControlService/FanControlWorker.cs new file mode 100644 index 0000000..b9f9619 --- /dev/null +++ b/ChromeboxFanControlService/FanControlWorker.cs @@ -0,0 +1,43 @@ +using ChromeboxFanControl; + +namespace ChromeboxFanControlService; + +public sealed class FanControlWorker : BackgroundService +{ + private FanController? _fan; + private AppConfig? _config; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _config = AppConfig.LoadMerged(); + _fan = new FanController(_config); + _fan.Start(); + + try + { + await Task.Delay(Timeout.Infinite, stoppingToken); + } + catch (OperationCanceledException) + { + /* shutdown */ + } + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + _fan?.Stop(); + _fan?.Dispose(); + if (_config != null) + { + try + { + EctoolRunner.RunSync(_config.EctoolPath, _config.AutoFanCtrlArgs); + } + catch + { + /* best effort restore autofan on shutdown */ + } + } + await base.StopAsync(cancellationToken); + } +} diff --git a/ChromeboxFanControlService/Program.cs b/ChromeboxFanControlService/Program.cs new file mode 100644 index 0000000..ce209b5 --- /dev/null +++ b/ChromeboxFanControlService/Program.cs @@ -0,0 +1,16 @@ +using ChromeboxFanControlService; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var host = Host.CreateDefaultBuilder(args) + .UseWindowsService(options => + { + options.ServiceName = "ChromeboxFanControl"; + }) + .ConfigureServices(services => + { + services.AddHostedService(); + }) + .Build(); + +await host.RunAsync(); diff --git a/ChromeboxFanControlService/appsettings.json b/ChromeboxFanControlService/appsettings.json new file mode 100644 index 0000000..2996c3f --- /dev/null +++ b/ChromeboxFanControlService/appsettings.json @@ -0,0 +1,15 @@ +{ + "EctoolPath": "C:\\Program Files\\crosec\\ectool.exe", + "PollIntervalMs": 1500, + "FanRpmPollEveryNCycles": 1, + "FanRpmArgs": [ "pwmgetfanrpm", "0" ], + "FanDutyArgs": null, + "AutoFanCtrlArgs": [ "autofanctrl" ], + "TempSource": "AverageCore", + "FailSafeAfterConsecutiveErrors": 5, + "FailSafeFanPercent": 100, + "FailSafeRestoreAutoFan": false, + "ChartHistoryMinutes": 15, + "ChartMaxPoints": 900, + "CurvePoints": [ 0, 0, 10, 25, 40, 55, 70, 80, 90, 95, 97, 98, 99, 100 ] +} diff --git a/README.md b/README.md index a850c37..f912c20 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ `C:\Program Files\crosec\ectool.exe` 若路径不同,在程序「高级」选项卡中修改。 2. **以管理员身份运行**本程序(清单已要求提升权限;LHM 与 EC 通信通常需要管理员)。 -3. 本机安装 [.NET 8 运行时](https://dotnet.microsoft.com/download/dotnet/8.0)(若使用 `dotnet publish --self-contained false` 发布,目标机需运行时)。 +3. **客户机**:`.\build.ps1 -publish`(及带 `-msi` 时)默认**自包含单文件**发布,**已内含 .NET 8 运行时**,`dist\` 仅含数个 exe,无需再装运行时。若需多 DLL 或依赖系统 .NET 8 以减小体积,可加 `-multiFile` 或 `-frameworkDependent`。 ## 编译 @@ -16,28 +16,49 @@ .\build.ps1 ``` -发布到 `dist\`: +### 发布 MSI 安装包(推荐) ```powershell -.\build.ps1 -publish +.\build.ps1 -publish -msi ``` -需已安装 [.NET 8 SDK](https://dotnet.microsoft.com/download)。 +`-publish` 只生成 `dist\`;加上 **`-msi`** 才用 WiX 生成安装包(需已安装 WiX)。输出两个 MSI: + +| 文件 | 说明 | +|------|------| +| `dist-installer\ChromeboxFanControl-Setup.msi` | **桌面版**:GUI + 可选 Windows 服务(功能树中可勾选) | +| `dist-installer\ChromeboxFanControlService-Setup.msi` | **服务版**:仅后台服务,适合无头部署 | + +**构建机**需 [.NET 8 SDK](https://dotnet.microsoft.com/download) 与 [WiX Toolset 3](https://wixtoolset.org/docs/wix3/)。**构建前请关闭正在运行的 ChromeboxFanControl.exe**。 + +以**管理员身份**运行 MSI。若尚未安装 ectool,可访问 [Chrultrabook 的 ectool 安装说明](https://docs.chrultrabook.com/docs/installing/ectool.html)。服务模式下配置从 `%ProgramData%\ChromeboxFanControl\config.json` 读取。 + +### 仅发布到 dist\(用于调试或手动打包) + +| 命令 | 说明 | +|------|------| +| `.\build.ps1 -publish` | 同时发布桌面版与服务版到 `dist\`(默认单文件) | +| `.\build.ps1 -publishGui` | 仅发布 GUI | +| `.\build.ps1 -publishService` | 仅发布服务并合并进 `dist\` | +| `.\build.ps1 -publish -multiFile` | 保留多 DLL 输出(默认单 exe) | +| `.\build.ps1 -publish -frameworkDependent` | 依赖系统 .NET 8(无 bundled 运行时) | ## 配置 - 安装目录旁的 **`appsettings.json`**:默认选项。 -- **`%AppData%\ChromeboxFanControl\config.json`**:保存后会覆盖同名项(用户配置)。 +- **`%AppData%\ChromeboxFanControl\config.json`**:GUI 模式下保存后覆盖(用户配置)。 +- **`%ProgramData%\ChromeboxFanControl\config.json`**:Windows 服务模式下读取的配置;可与 GUI 共用同一份配置逻辑。 重要字段: | 项 | 说明 | |----|------| +| `Language` | 界面语言:`auto`(跟随系统)、`en`、`zh-Hans`、`zh-Hant`。修改后需重启生效。 | | `FanRpmArgs` | 读取转速时传给 ectool 的参数(默认 `pwmgetfanrpm` `0`),请用本机 `ectool help` 核对。 | | `FanDutyArgs` | 读取占空比时传给 ectool 的参数(如 `pwmget` `0`);空则跳过,显示目标占空比。 | | `AutoFanCtrlArgs` | 退出程序时恢复 EC 自动风扇(默认 `autofanctrl`),若命令名不同请修改。 | | `TempSource` | `AverageCore` 或 `MaxCore`。 | -| `CurvePoints` | 13 个 0–100 的占空比控制点(与 Chrultrabook-Tools 曲线逻辑一致)。 | +| `CurvePoints` | 14 个 0–100 的占空比控制点(对应温度断点 0,40,45,…,100°C,线性插值)。 | ## 使用说明 diff --git a/build.ps1 b/build.ps1 index 624c484..96059f8 100644 --- a/build.ps1 +++ b/build.ps1 @@ -22,6 +22,13 @@ if (-not $exe) { Write-Error "dotnet SDK not found. Install .NET 8 SDK: https://dotnet.microsoft.com/download" } +# -frameworkDependent: no bundled runtime (fewer files, needs .NET 8 on target) +# -multiFile: disable single-file (default is single exe to reduce DLL count) +$frameworkDependent = $args -contains "-frameworkDependent" -or $args -contains "--frameworkDependent" +$multiFile = $args -contains "-multiFile" -or $args -contains "--multiFile" +$selfContained = if ($frameworkDependent) { "false" } else { "true" } +$singleFile = -not $multiFile + function Invoke-Dotnet { if ($exe -eq "dotnet") { dotnet @args @@ -30,28 +37,122 @@ function Invoke-Dotnet { } } +$doPublish = $args -contains "-publish" -or $args -contains "--publish" +$doPublishGui = $args -contains "-publishGui" -or $args -contains "--publishGui" +$doPublishSvc = $args -contains "-publishService" -or $args -contains "--publishService" +# -publish without split = GUI + Service both +if ($doPublish -and -not $doPublishGui -and -not $doPublishSvc) { + $doPublishGui = $true + $doPublishSvc = $true +} +$doPublishAny = $doPublishGui -or $doPublishSvc +# -msi requires WiX; -publish only generates dist, MSI optional +$doMsi = $args -contains "-msi" -or $args -contains "--msi" Write-Host ">> dotnet restore" -Invoke-Dotnet restore "$root\ChromeboxFanControl.sln" +if ($doPublishAny -or $doMsi) { + Invoke-Dotnet restore "$root\ChromeboxFanControl.sln" -r win-x64 +} else { + Invoke-Dotnet restore "$root\ChromeboxFanControl.sln" +} Write-Host ">> dotnet build Release" Invoke-Dotnet build "$root\ChromeboxFanControl.sln" -c Release --no-restore if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -$doPublish = $args -contains "-publish" -or $args -contains "--publish" -if ($doPublish) { +if ($doPublishAny) { $out = Join-Path $root "dist" - if (Test-Path $out) { Remove-Item $out -Recurse -Force } - New-Item -ItemType Directory -Path $out | Out-Null - Write-Host ">> dotnet publish -> $out" - Invoke-Dotnet publish "$root\ChromeboxFanControl\ChromeboxFanControl.csproj" ` - -c Release ` - -o $out ` - -r win-x64 ` - --self-contained false ` - -p:PublishSingleFile=false ` - --no-restore - Write-Host "Done: $out\ChromeboxFanControl.exe" -} else { - Write-Host "Done: $root\ChromeboxFanControl\bin\Release\net8.0-windows\ChromeboxFanControl.exe" - Write-Host "(Run .\build.ps1 -publish to output to dist\)" + if ($doPublishGui -and $doPublishSvc) { + if (Test-Path $out) { Remove-Item $out -Recurse -Force } + New-Item -ItemType Directory -Path $out | Out-Null + } elseif ($doPublishGui -and -not (Test-Path $out)) { + New-Item -ItemType Directory -Path $out | Out-Null + } + if ($doPublishGui) { + Write-Host ">> dotnet publish GUI -> $out" + $guiArgs = @("-c", "Release", "-o", $out, "-r", "win-x64", "--self-contained", "false", "--no-restore") + if ($singleFile) { $guiArgs += "-p:PublishSingleFile=true", "-p:IncludeNativeLibrariesForSelfExtract=true" } + Invoke-Dotnet publish "$root\ChromeboxFanControl\ChromeboxFanControl.csproj" @guiArgs + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } + if ($doPublishSvc) { + Write-Host ">> dotnet publish Service -> dist-svc" + $svcOut = Join-Path $root "dist-svc" + $svcArgs = @("-c", "Release", "-o", $svcOut, "-r", "win-x64", "--self-contained", $selfContained, "--no-restore") + if ($singleFile) { $svcArgs += "-p:PublishSingleFile=true", "-p:IncludeNativeLibrariesForSelfExtract=true" } + Invoke-Dotnet publish "$root\ChromeboxFanControlService\ChromeboxFanControlService.csproj" @svcArgs + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if (-not (Test-Path $out)) { New-Item -ItemType Directory -Path $out | Out-Null } + Copy-Item "$svcOut\*" $out -Recurse -Force -Exclude "appsettings.json" + if (-not $doMsi) { Remove-Item $svcOut -Recurse -Force } + } + if ($doPublishGui -and $doPublishSvc) { + Write-Host "Done: $out\ChromeboxFanControl.exe (GUI), $out\ChromeboxFanControlService.exe (Service)" + } elseif ($doPublishGui) { + Write-Host "Done (GUI only): $out\ChromeboxFanControl.exe" + } else { + Write-Host "Done (Service only): $out\ChromeboxFanControlService.exe (merged into dist\)" + } +} + +if ($doMsi) { + $dist = Join-Path $root "dist" + $distSvc = Join-Path $root "dist-svc" + if (-not (Test-Path $dist)) { + Write-Error "dist\ not found. Run .\build.ps1 -publish first." + } + if (-not (Test-Path $distSvc)) { + Write-Host ">> dist-svc missing, publishing Service for service MSI" + $svcArgs = @("-c", "Release", "-o", $distSvc, "-r", "win-x64", "--self-contained", $selfContained, "--no-restore") + if ($singleFile) { $svcArgs += "-p:PublishSingleFile=true", "-p:IncludeNativeLibrariesForSelfExtract=true" } + Invoke-Dotnet publish "$root\ChromeboxFanControlService\ChromeboxFanControlService.csproj" @svcArgs + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } + $wixBin = $null + foreach ($v in @("3.11", "3.14", "3.10")) { + $p = "${env:ProgramFiles(x86)}\WiX Toolset v$v\bin" + if (Test-Path "$p\candle.exe") { $wixBin = $p; break } + } + if (-not $wixBin) { + Write-Warning "WiX Toolset not found. Skipping MSI. Install from https://wixtoolset.org/docs/wix3/ to build installers." + } else { + $wixDir = Join-Path $root "wix" + $outMsi = Join-Path $root "dist-installer" + if (-not (Test-Path $outMsi)) { New-Item -ItemType Directory -Path $outMsi | Out-Null } + $objDir = Join-Path $root "wix\obj" + if (-not (Test-Path $objDir)) { New-Item -ItemType Directory -Path $objDir | Out-Null } + $ext = "-ext", "$wixBin\WixUtilExtension.dll", "-ext", "$wixBin\WixUIExtension.dll" + + Write-Host ">> WiX: Desktop MSI" + $harvested = Join-Path $root "wix\Harvested.wxs" + & "$wixBin\heat.exe" dir $dist -cg AppFiles -gg -sf -srd -sreg -scom -dr INSTALLFOLDER -out $harvested -t "$wixDir\exclude-service.xsl" -sw5151 + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & "$wixBin\candle.exe" -arch x64 -out "$objDir\\" @ext "$wixDir\Product.wxs" $harvested + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & "$wixBin\light.exe" -out "$outMsi\ChromeboxFanControl-Setup.msi" -b $dist @ext "$objDir\Product.wixobj" "$objDir\Harvested.wixobj" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Remove-Item $harvested -Force -ErrorAction SilentlyContinue + + Write-Host ">> WiX: Service MSI" + $harvestedSvc = Join-Path $root "wix\Harvested-Service.wxs" + & "$wixBin\heat.exe" dir $distSvc -cg ServiceFiles -gg -sf -srd -sreg -scom -dr INSTALLFOLDER -out $harvestedSvc -t "$wixDir\exclude-service.xsl" -sw5151 + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & "$wixBin\candle.exe" -arch x64 -out "$objDir\\" @ext "$wixDir\Product-Service.wxs" $harvestedSvc + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & "$wixBin\light.exe" -out "$outMsi\ChromeboxFanControlService-Setup.msi" -b $distSvc @ext "$objDir\Product-Service.wixobj" "$objDir\Harvested-Service.wixobj" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Remove-Item $harvestedSvc -Force -ErrorAction SilentlyContinue + + if (Test-Path $distSvc) { Remove-Item $distSvc -Recurse -Force } + Write-Host "Done: $outMsi\ChromeboxFanControl-Setup.msi (Desktop), $outMsi\ChromeboxFanControlService-Setup.msi (Service)" + } +} + +if (-not $doPublishAny -and -not $doMsi) { + Write-Host "Done: $root\ChromeboxFanControl\bin\Release\net8.0-windows\ChromeboxFanControl.exe" + Write-Host " .\build.ps1 -publish -> dist\ GUI + Service (self-contained)" + Write-Host " .\build.ps1 -publishGui -> GUI only" + Write-Host " .\build.ps1 -publishService -> Service only (merge into dist\)" + Write-Host " .\build.ps1 -publish -msi -> dist + two MSI (needs WiX)" + Write-Host " .\build.ps1 -publish -frameworkDependent -> no bundled runtime (needs .NET 8)" + Write-Host " .\build.ps1 -publish -multiFile -> keep multiple DLLs (default: single exe)" } diff --git a/wix/Product-Service.wxs b/wix/Product-Service.wxs new file mode 100644 index 0000000..7ce7bf4 --- /dev/null +++ b/wix/Product-Service.wxs @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wix/Product.wxs b/wix/Product.wxs new file mode 100644 index 0000000..cd727da --- /dev/null +++ b/wix/Product.wxs @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wix/exclude-service.xsl b/wix/exclude-service.xsl new file mode 100644 index 0000000..3b71a9f --- /dev/null +++ b/wix/exclude-service.xsl @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/wix314.exe b/wix314.exe new file mode 100644 index 0000000..5e45d5c Binary files /dev/null and b/wix314.exe differ