diff --git a/AI_HANDOFF.md b/AI_HANDOFF.md
index bb71c35..283d5cb 100644
--- a/AI_HANDOFF.md
+++ b/AI_HANDOFF.md
@@ -1,5 +1,97 @@
# Project State & Handoff
+## 2026-08-13 GitHub upload (complete)
+
+- User authorized uploading the complete current verified worktree, including the WPF redesign and installer-experience hardening. Scope is this repository only.
+- Remote audit after `fetch --prune --tags` found local `main` and `origin/main` identical with no other pre-existing branches. Initial source commit `66b6498` was pushed to `agent/ui-installer-experience`; Draft PR #10 targets `main`: https://github.com/nojackno2-ctrl/WinBat-Lens/pull/10
+- GitHub connector PR creation returned `403 Resource not accessible by integration`; the authenticated host `gh pr create` fallback succeeded. A final handoff-only commit records the completed remote state on the same PR branch.
+- This authorization does not include a GitHub Release, tag, installer execution, or installed-application update.
+
+## 2026-08-13 installer experience hardening (complete, uncommitted)
+
+- Scope is limited to `installer/WinBatLens.iss` and a new installer contract test; the concurrent uncommitted WPF redesign remains preserved and untouched.
+- Start Menu application/uninstall shortcuts are now always created with a stable AppUserModelID and `{app}` working directory. Desktop and Windows-startup choices now default unchecked, and upgrades continue to restore previous task choices.
+- Added GitHub support/update metadata, installed README registration, setup/uninstall logging, explicit uninstall identity/version fields, narrowed Restart Manager filtering from every executable to `WinBatLens.exe`, and matching README guidance for manual Start/Taskbar pinning.
+- First test compile failed only because the new standalone contract test omitted the repository's explicit `using Xunit`; the import was added before the successful rerun. No product or installer failure was observed in that attempt.
+- Verification after correction: tests passed 22/22 with no warnings; changed-file `dotnet format --verify-no-changes --no-restore` and `git diff --check` passed; .NET 10 self-contained publish, portable packaging, and Inno Setup 6.7.3 compilation succeeded. Final installer: `dist/WinBatLens_v1.1.7_Setup_x64.exe`, 80,174,427 bytes, SHA-256 `9918D06B8EB4B43B94FD635E789A80B2AFC7ED30EF47B9AFB66631BE4CB10DBC`, file version 1.1.7. The full-solution format check still reports pre-existing whitespace in unrelated files, so only the new test was used as the scoped format gate. Inno's existing, documented admin/HKCU startup warning remains expected.
+- No installer was executed, no installed application was updated, and no commit/push/release was performed.
+
+## 2026-08-13 UI redesign baseline and design audit
+
+- Scope is a preserve-mode redesign of the active WPF application. The legacy
+ Electron prototype under `src/` is not part of the shipped .NET build and is
+ not being treated as the product UI.
+- Baseline before edits: clean `main` at `6c4d5b2`, tracking `origin/main`, with
+ no uncommitted diff. `AGENTS.md`, this handoff, the latest five commits,
+ `App.xaml`, `MainWindow.xaml`, localization wiring, and the relevant code-behind
+ state transitions were reviewed.
+- Design read: dense Windows battery and hardware monitoring for laptop users,
+ with a precise, quiet, instrument-like dark UI. Native WPF remains the only UI
+ system and no dependency will be added.
+- Dials: `DESIGN_VARIANCE 4`, `MOTION_INTENSITY 2`, `VISUAL_DENSITY 8`.
+ Redesign mode is `Redesign - Preserve / targeted evolution`.
+- Preserve: all six information areas, control names and event handlers, report
+ and history data, measured-versus-unavailable telemetry boundaries, the shared
+ physical watt scale, and semantic power colours. The historical compact-layout
+ constraint remains: recover space from padding and chrome before reducing type.
+- Retire: scattered one-off colours, emoji-dependent labels, weak or missing
+ keyboard focus/disabled/pressed states, inconsistent radii and nested-card
+ contrast, and blank data surfaces with no empty-state explanation.
+- Planned design system: one cyan interaction accent; graphite neutral surfaces;
+ red/green/blue/amber reserved only for real telemetry and health semantics;
+ Segoe UI Variable/Segoe UI with tabular numerals for measurements; 10 px cards,
+ 6 px controls, and pill radii only for semantic status badges.
+- No hardware/UI runtime result is claimed yet. Release build, tests, formatting,
+ `git diff --check`, XAML parsing/build validation, and safe visual QA remain to
+ be completed. No commit, push, release, deployment, or installed-app update is
+ authorized.
+
+### Implementation milestone
+
+- `App.xaml` now defines one graphite instrument-dark token system, one cyan
+ interaction accent, shared card/data-row/status shapes, and complete hover,
+ pressed, keyboard-focus, selected, and disabled states for buttons, tabs,
+ checkboxes, list rows, headers, and progress bars.
+- `MainWindow.xaml` keeps the existing 1200x780 compact architecture and six
+ information areas, but replaces scattered one-off surfaces with the shared
+ system, gives the primary measured-power card slightly more weight, uses
+ tabular numerals on live measurements and axes, and adds contextual empty
+ states plus a focused report-loading surface.
+- Emoji-dependent product labels were removed from the window and bilingual
+ localization. Runtime dGPU/tip text follows the same rule. Visible range text
+ now uses regular hyphens instead of typographic dash separators.
+- New bilingual strings cover hardware-source labels, empty states, and loading
+ states. Existing names, handlers, bindings, report content, history data, and
+ measured/unavailable telemetry decisions remain intact.
+- Validation has started; no build, test, render, or live hardware result is
+ claimed at this milestone.
+
+### Final validation
+
+- The first Release solution build with the repository's normal self-contained
+ settings and `--no-restore` failed before compilation with `NETSDK1112` because
+ this local restore state lacks the .NET 10 `win-x64` runtime packs. No product
+ or XAML error was reported. To validate source without downloading anything,
+ the build was rerun with `SelfContained=false` and an empty
+ `RuntimeIdentifier`; it passed with 0 warnings and 0 errors.
+- Release tests passed 20/20. Changed-file `dotnet format
+ --verify-no-changes` passed for `MainWindow.xaml.cs` and
+ `Services/LocalizationService.cs` after applying its whitespace fixes.
+- Both XAML files parse as XML, WPF XAML compilation passed as part of the
+ Release build, `git diff --check` passed, and scans found no emoji in the
+ redesigned window/runtime/localization labels and no em/en dash in visible
+ C# strings.
+- No live UI screenshot or hardware smoke test was performed. An installed
+ monitor may already own the single-instance mutex, so launching this build
+ could foreground the old installed UI and produce false evidence or disturb
+ ongoing monitoring. Visual QA is therefore limited to source audit plus
+ successful WPF compilation; live layout, focus appearance, and min-size
+ behaviour remain unverified.
+- Final working tree intentionally contains only `App.xaml`,
+ `MainWindow.xaml`, `MainWindow.xaml.cs`, `Services/LocalizationService.cs`,
+ and this handoff. No commit, push, release, deployment, or installed-app
+ update was performed.
+
## 2026-08-12 v1.1.7 publication and installed update
- Published GitHub Release `v1.1.7` from `66e4f3f147fad925841a09528f8a73f9cd2d0cf6`; branch CI and tag Release succeeded. Downloaded server digests: portable EXE `352B0A73...BC9497`, ZIP `3FB557F0...82D17`, Setup `1CD18AEC...B16AA`.
@@ -1338,3 +1430,11 @@ Verified on a live AMD iGPU + NVIDIA RTX 3060 dGPU laptop.
- **No automated tests.** Every verification in this file is a manual measurement;
the parser (`BatteryReportParser`) in particular is regex-heavy and would
benefit from unit tests over the two saved sample reports.
+
+## 2026-08-13 installed software update verification
+
+- Silently updated the current-user installation with `dist\WinBatLens_v1.1.7_Setup_x64.exe`; the elevated installer returned exit code `0` and did not request a reboot.
+- Installed executable: `%LOCALAPPDATA%\Programs\WinBat Lens\WinBatLens.exe`; FileVersion `1.1.7.0`, ProductVersion `1.1.7+6c4d5b22e196ca3f17333d35f141184bbab706a9`, SHA-256 `D1477D4783758F820A6535E793B3E9F8890B3CA544C9A57F826DCBFC9772F4E7`. It is byte-for-byte identical to the current publish output.
+- Verified the registered version, install location, uninstall command, project/issues/update URLs, and the Start Menu launch/uninstall shortcuts. Removed the stale duplicate English uninstall shortcut; the current localized uninstall shortcut remains valid.
+- Launched the installed build with `--background`; PID `2196` reported `Responding=True`. This verifies launch and process responsiveness, not every hardware reading or UI state.
+- The user's authorization in this milestone covers updating installed software; this handoff entry is not yet committed or pushed.
diff --git a/App.xaml b/App.xaml
index 8193933..87e2a56 100644
--- a/App.xaml
+++ b/App.xaml
@@ -5,63 +5,126 @@
StartupUri="MainWindow.xaml">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
+
diff --git a/MainWindow.xaml b/MainWindow.xaml
index 0ceda8d..cb75bc2 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -6,18 +6,22 @@
WindowStartupLocation="CenterScreen"
Background="{StaticResource BgDark}"
Foreground="{StaticResource TextMain}"
+ FontFamily="Segoe UI Variable Text, Segoe UI"
+ FontSize="12"
+ UseLayoutRounding="True"
+ SnapsToDevicePixels="True"
+ TextOptions.TextFormattingMode="Display"
Icon="pack://application:,,,/app_icon.png">
-
+
-
+
@@ -26,8 +30,11 @@
-
-
+
+
@@ -35,9 +42,10 @@
-
+
@@ -47,14 +55,14 @@
-
+
-
+
-
-
-
+
+
+
@@ -77,8 +85,8 @@
-
+
@@ -88,7 +96,7 @@
the status wording uses. The arc is rotated so it
starts at twelve o'clock instead of three. -->
-
+
@@ -103,7 +111,7 @@
-
+
@@ -113,10 +121,10 @@
-
+
-
+
@@ -160,7 +168,7 @@
-
+
@@ -173,8 +181,8 @@
-
+
@@ -182,14 +190,14 @@
-
+
-
+
@@ -209,7 +217,7 @@
-
+
@@ -218,12 +226,12 @@
-
+
-
@@ -246,31 +254,30 @@
-
+
-
+
-
-
-
+
+
+
-
+
-
+
-
-
+
+
-
+
-
+
-
-
+
+
-
+
-
+
-
+
-
+
-
+
@@ -328,7 +335,7 @@
-
+
@@ -343,11 +350,11 @@
-
-
-
-
-
+
+
+
+
+
-
+
-
+
-
+
@@ -376,20 +383,20 @@
-
+
-
-
+
+
-
+
@@ -397,7 +404,7 @@
-
+
@@ -409,7 +416,7 @@
-
+
@@ -417,14 +424,14 @@
-
+
-
+
@@ -434,7 +441,7 @@
for any of them on this hardware. -->
-
+
@@ -442,14 +449,14 @@
-
+
-
+
@@ -462,15 +469,15 @@
-
-
+
+
-
+
@@ -478,35 +485,51 @@
-
-
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
@@ -516,21 +539,21 @@
-
+
-
+
-
+
@@ -542,11 +565,11 @@
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -639,12 +726,14 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index b12c325..f7fabbf 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -283,10 +283,19 @@ private void ApplyLanguage()
LblHwBatteryTelemetry.Text = LocalizationService.Get("HwBatteryTelemetry");
LblHwEnergy.Text = LocalizationService.Get("HwEnergy");
LblHwPowerPlan.Text = LocalizationService.Get("HwPowerPlan");
+ TxtHwTelemetrySource.Text = LocalizationService.Get("HwTelemetrySource");
+ TxtHwPowerPlanState.Text = LocalizationService.Get("HwPowerPlanState");
TxtHistoryLogHeader.Text = LocalizationService.Get("HistoryLogHeader");
BtnExportPowerCsv.Content = LocalizationService.Get("BtnExportCsv");
BtnClearPowerHistory.Content = LocalizationService.Get("BtnClearHistory");
+ TxtPowerHistoryEmpty.Text = LocalizationService.Get("PowerHistoryEmpty");
+ TxtVoltageHistoryEmpty.Text = LocalizationService.Get("VoltageHistoryEmpty");
+ TxtDiagnosticsEmpty.Text = LocalizationService.Get("DiagnosticsEmpty");
+ TxtLifeEstimatesEmpty.Text = LocalizationService.Get("LifeEstimatesEmpty");
+ TxtRecentUsageEmpty.Text = LocalizationService.Get("RecentUsageEmpty");
+ TxtLoadingTitle.Text = LocalizationService.Get("LoadingTitle");
+ TxtLoadingDetail.Text = LocalizationService.Get("LoadingDetail");
// System tray menu follows the same language as the window.
if (_trayItemShow != null) _trayItemShow.Text = LocalizationService.Get("TrayShow");
@@ -723,14 +732,16 @@ private void InitSystemTrayIcon()
_trayItemShow = new ToolStripMenuItem(LocalizationService.Get("TrayShow"), null, (s, args) => RestoreFromTray());
_trayItemShow.Font = new System.Drawing.Font(_trayItemShow.Font, System.Drawing.FontStyle.Bold);
- _trayItemCheck = new ToolStripMenuItem(LocalizationService.Get("TrayCheck"), null, async (s, args) => {
+ _trayItemCheck = new ToolStripMenuItem(LocalizationService.Get("TrayCheck"), null, async (s, args) =>
+ {
RestoreFromTray();
await RunBatteryCheckAsync();
});
_trayItemAutoStart = new ToolStripMenuItem(LocalizationService.Get("TrayAutoStart"));
_trayItemAutoStart.Checked = StartupService.IsAutoStartEnabled();
- _trayItemAutoStart.Click += (s, args) => {
+ _trayItemAutoStart.Click += (s, args) =>
+ {
bool newState = !_trayItemAutoStart!.Checked;
if (StartupService.SetAutoStart(newState))
{
@@ -1057,8 +1068,8 @@ private void UpdateLivePowerUI(RealTimePowerState state, bool recordSample = tru
TxtLiveAcState.Foreground = BrushRose;
TxtLiveAcState.Text = en
- ? $"🔌⚠️ Charger cannot keep up — battery covering -{state.DischargeRateW:F1}W (measured at the pack)"
- : $"🔌⚠️ 外接電源供電不足 — 電池補上 -{state.DischargeRateW:F1}W(電池實測)";
+ ? $"Charger cannot keep up. Battery covering -{state.DischargeRateW:F1}W (measured at the pack)"
+ : $"外接電源供電不足。電池補上 -{state.DischargeRateW:F1}W(電池實測)";
}
else if (state.IsAcOnline)
{
@@ -1077,20 +1088,20 @@ private void UpdateLivePowerUI(RealTimePowerState state, bool recordSample = tru
if (state.IsCharging && state.IsChargeRateMeasured)
{
TxtLiveAcState.Text = en
- ? $"🔌 Charging the battery at +{state.ChargingRateW:F1}W (measured at the pack)"
- : $"🔌 電池充電中 +{state.ChargingRateW:F1}W(電池實測)";
+ ? $"Charging the battery at +{state.ChargingRateW:F1}W (measured at the pack)"
+ : $"電池充電中 +{state.ChargingRateW:F1}W(電池實測)";
}
else if (state.IsCharging)
{
TxtLiveAcState.Text = en
- ? "🔌 Charging — this battery does not report a charge rate"
- : "🔌 電池充電中 — 此電池未回報充電功率";
+ ? "Charging. This battery does not report a charge rate"
+ : "電池充電中。此電池未回報充電功率";
}
else
{
TxtLiveAcState.Text = en
- ? "🔌 On AC, battery idle — no current in or out, so there is nothing to measure"
- : "🔌 市電直供中,電池無充放電電流 — 此狀態下沒有可量測的功率";
+ ? "On AC, battery idle. No current in or out, so there is nothing to measure"
+ : "市電直供中,電池無充放電電流。此狀態下沒有可量測的功率";
}
}
else
@@ -1109,14 +1120,14 @@ private void UpdateLivePowerUI(RealTimePowerState state, bool recordSample = tru
if (state.IsDischargeRateMeasured)
{
TxtLiveAcState.Text = en
- ? $"🔋 Battery Discharging (-{state.DischargeRateW:F1}W measured at the pack — whole system)"
- : $"🔋 電池放電中 (-{state.DischargeRateW:F1}W 電池實測 — 全系統真實耗電)";
+ ? $"Battery Discharging (-{state.DischargeRateW:F1}W measured at the pack, whole system)"
+ : $"電池放電中 (-{state.DischargeRateW:F1}W 電池實測,為全系統真實耗電)";
}
else
{
TxtLiveAcState.Text = en
- ? $"🔋 Battery Discharging (~-{state.DischargeRateW:F1}W estimated)"
- : $"🔋 電池放電中 (~-{state.DischargeRateW:F1}W 推估)";
+ ? $"Battery Discharging (~-{state.DischargeRateW:F1}W estimated)"
+ : $"電池放電中 (~-{state.DischargeRateW:F1}W 推估)";
}
}
@@ -1131,16 +1142,16 @@ private void UpdateLivePowerUI(RealTimePowerState state, bool recordSample = tru
// Amber, not red: red means power leaving the pack, and
// this line is a warning about the supply, not a rate.
TxtLiveChargerSupply.Text = en
- ? "⚠️ Windows reports the external supply as inadequate for this system"
- : "⚠️ Windows 判定:外接電源供電能力不足以支撐目前的系統負載";
+ ? "Windows reports the external supply as inadequate for this system"
+ : "Windows 判定:外接電源供電能力不足以支撐目前的系統負載";
TxtLiveChargerSupply.Foreground = BrushAmber;
TxtLiveChargerSupply.Visibility = Visibility.Visible;
break;
case PowerSupplyCapability.Adequate:
TxtLiveChargerSupply.Text = en
- ? "🔌 Windows reports the external supply as adequate. The adapter's own wattage is not exposed by Windows."
- : "🔌 Windows 判定:外接電源供電充足(變壓器 / USB-C 充電器本身的瓦數,Windows 並未提供)";
+ ? "Windows reports the external supply as adequate. The adapter's own wattage is not exposed by Windows."
+ : "Windows 判定:外接電源供電充足(變壓器 / USB-C 充電器本身的瓦數,Windows 並未提供)";
TxtLiveChargerSupply.Foreground = BrushSlate;
TxtLiveChargerSupply.Visibility = Visibility.Visible;
break;
@@ -1209,7 +1220,7 @@ private void UpdateLivePowerUI(RealTimePowerState state, bool recordSample = tru
TxtHwEnergySub.Text = state.DriverHealthPercent > 0
? (en
- ? $"Full charge {state.BatteryCapacityHealthText} — health {state.DriverHealthPercent:F1}%"
+ ? $"Full charge {state.BatteryCapacityHealthText}, health {state.DriverHealthPercent:F1}%"
: $"滿電 {state.BatteryCapacityHealthText},健康度 {state.DriverHealthPercent:F1}%")
: (en ? "Measured by the battery driver" : "電池驅動實測容量");
}
@@ -1220,7 +1231,7 @@ private void UpdateLivePowerUI(RealTimePowerState state, bool recordSample = tru
// The discrete GPU is the only component with a real power
// sensor, so it is the only component row left on this page.
- TxtDgpuName.Text = $"🎮 {state.DgpuName}";
+ TxtDgpuName.Text = state.DgpuName;
PbDgpuUsage.Value = state.DgpuUsagePercent;
TxtDgpuUsageVal.Text = state.DgpuStatusText;
TxtDgpuPowerW.Text = state.IsDgpuPowerMeasured
@@ -1248,22 +1259,22 @@ private void UpdateLivePowerUI(RealTimePowerState state, bool recordSample = tru
if (state.IsChargerDeficit)
{
TxtLivePowerTip.Text = en
- ? $"🔌⚠️ Plugged in but still discharging: the charger is short by at least {state.DischargeRateW:F1} W, which the battery is covering.{dgpuPart} Typical of charging over USB-C — a PD charger rated below this machine's draw cannot hold it up. Use a higher-wattage charger, or reduce load, to charge while working."
- : $"🔌⚠️ 已接上外接電源,但電池仍在放電:充電器至少差 {state.DischargeRateW:F1} W,缺口由電池補上。{dgpuPart}這是 USB-C 充電最常見的情況 — PD 充電器的瓦數低於本機耗電就撐不住。請改用瓦數更高的充電器,或降低負載才能邊用邊充。";
+ ? $"Plugged in but still discharging: the charger is short by at least {state.DischargeRateW:F1} W, which the battery is covering.{dgpuPart} This is common with USB-C charging. A PD charger rated below this machine's draw cannot sustain it. Use a higher-wattage charger or reduce load to charge while working."
+ : $"已接上外接電源,但電池仍在放電:充電器至少差 {state.DischargeRateW:F1} W,缺口由電池補上。{dgpuPart}這是 USB-C 充電常見的情況。PD 充電器的瓦數低於本機耗電就無法維持供電。請改用瓦數更高的充電器,或降低負載才能邊用邊充。";
}
else if (state.IsAcOnline)
{
if (state.IsCharging && state.IsChargeRateMeasured)
{
TxtLivePowerTip.Text = en
- ? $"🔌 On AC. Battery charging at {state.ChargingRateW:F1} W (measured at the pack). {state.EstimatedTimeRemainingText}.{dgpuPart}"
- : $"🔌 市電供電中,電池充電功率 {state.ChargingRateW:F1} W(電池實測)。{state.EstimatedTimeRemainingText}。{dgpuPart}";
+ ? $"On AC. Battery charging at {state.ChargingRateW:F1} W (measured at the pack). {state.EstimatedTimeRemainingText}.{dgpuPart}"
+ : $"市電供電中,電池充電功率 {state.ChargingRateW:F1} W(電池實測)。{state.EstimatedTimeRemainingText}。{dgpuPart}";
}
else
{
TxtLivePowerTip.Text = en
- ? $"🔌 On AC, no current flowing to or from the battery — there is no measurable system wattage in this state. Adapter input is not exposed by Windows.{dgpuPart}"
- : $"🔌 市電直供中,電池無充放電電流,此狀態下沒有可量測的系統功率(變壓器輸入功率 Windows 並未提供)。{dgpuPart}";
+ ? $"On AC, no current is flowing to or from the battery. There is no measurable system wattage in this state. Adapter input is not exposed by Windows.{dgpuPart}"
+ : $"市電直供中,電池無充放電電流,此狀態下沒有可量測的系統功率(變壓器輸入功率 Windows 並未提供)。{dgpuPart}";
}
}
else if (state.IsDischargeRateMeasured)
@@ -1271,21 +1282,21 @@ private void UpdateLivePowerUI(RealTimePowerState state, bool recordSample = tru
if (state.DischargeRateW > 20.0 || state.DgpuUsagePercent > 40.0)
{
TxtLivePowerTip.Text = en
- ? $"⚠️ High draw: {state.DischargeRateW:F1} W measured at the battery — the whole machine.{dgpuPart} Lowering screen brightness (now {state.ScreenBrightnessPercent}%) extends runtime."
- : $"⚠️ 目前放電 {state.DischargeRateW:F1} W(電池實測,為整機真實耗電)。{dgpuPart}建議調低螢幕亮度(目前 {state.ScreenBrightnessPercent}%)以延長續航。";
+ ? $"High draw: {state.DischargeRateW:F1} W measured at the battery for the whole machine.{dgpuPart} Lowering screen brightness (now {state.ScreenBrightnessPercent}%) extends runtime."
+ : $"目前放電 {state.DischargeRateW:F1} W(電池實測,為整機真實耗電)。{dgpuPart}建議調低螢幕亮度(目前 {state.ScreenBrightnessPercent}%)以延長續航。";
}
else
{
TxtLivePowerTip.Text = en
- ? $"💡 On battery: {state.DischargeRateW:F1} W measured at the pack — the whole machine's real draw.{dgpuPart}"
- : $"💡 電池供電中,實測放電 {state.DischargeRateW:F1} W(量測自電池,即整機真實耗電)。{dgpuPart}";
+ ? $"On battery: {state.DischargeRateW:F1} W measured at the pack, the whole machine's real draw.{dgpuPart}"
+ : $"電池供電中,實測放電 {state.DischargeRateW:F1} W(量測自電池,即整機真實耗電)。{dgpuPart}";
}
}
else
{
TxtLivePowerTip.Text = en
- ? $"💡 On battery. This machine's battery does not report a discharge rate, so no wattage can be shown.{dgpuPart}"
- : $"💡 電池供電中。此裝置的電池未回報放電功率,因此無法顯示瓦數。{dgpuPart}";
+ ? $"On battery. This machine's battery does not report a discharge rate, so no wattage can be shown.{dgpuPart}"
+ : $"電池供電中。此裝置的電池未回報放電功率,因此無法顯示瓦數。{dgpuPart}";
}
}
catch (Exception ex)
@@ -1308,12 +1319,12 @@ private void BtnExportPowerCsv_Click(object sender, RoutedEventArgs e)
bool success = RealTimePowerHistoryService.ExportToCsv(saveDialog.FileName);
if (success)
{
- MessageBox.Show(LocalizationService.CurrentLanguage == AppLanguage.English ? $"Successfully exported to:\n{saveDialog.FileName}" : $"已成功匯出歷史日誌至:\n{saveDialog.FileName}",
+ MessageBox.Show(LocalizationService.CurrentLanguage == AppLanguage.English ? $"Successfully exported to:\n{saveDialog.FileName}" : $"已成功匯出歷史日誌至:\n{saveDialog.FileName}",
LocalizationService.CurrentLanguage == AppLanguage.English ? "Export Success" : "匯出成功", MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
- MessageBox.Show(LocalizationService.CurrentLanguage == AppLanguage.English ? "Export failed due to write permissions." : "匯出失敗,請確認檔案寫入權限。",
+ MessageBox.Show(LocalizationService.CurrentLanguage == AppLanguage.English ? "Export failed due to write permissions." : "匯出失敗,請確認檔案寫入權限。",
LocalizationService.CurrentLanguage == AppLanguage.English ? "Error" : "錯誤", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
@@ -1321,7 +1332,7 @@ private void BtnExportPowerCsv_Click(object sender, RoutedEventArgs e)
private void BtnClearPowerHistory_Click(object sender, RoutedEventArgs e)
{
- var result = MessageBox.Show(LocalizationService.CurrentLanguage == AppLanguage.English ? "Clear all live power and event history logs?" : "確定要清除所有即時功耗與充放電事件紀錄嗎?",
+ var result = MessageBox.Show(LocalizationService.CurrentLanguage == AppLanguage.English ? "Clear all live power and event history logs?" : "確定要清除所有即時功耗與充放電事件紀錄嗎?",
LocalizationService.CurrentLanguage == AppLanguage.English ? "Confirm Clear" : "確認清除", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
@@ -1416,7 +1427,7 @@ private void BtnExportData_Click(object sender, RoutedEventArgs e)
{
if (_currentReport == null)
{
- MessageBox.Show(LocalizationService.CurrentLanguage == AppLanguage.English ? "No battery report data available to export." : "目前尚無可供匯出的電池資料。",
+ MessageBox.Show(LocalizationService.CurrentLanguage == AppLanguage.English ? "No battery report data available to export." : "目前尚無可供匯出的電池資料。",
LocalizationService.CurrentLanguage == AppLanguage.English ? "Info" : "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
@@ -1434,7 +1445,7 @@ private void BtnExportData_Click(object sender, RoutedEventArgs e)
{
string json = JsonSerializer.Serialize(_currentReport, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(saveDialog.FileName, json);
- MessageBox.Show(LocalizationService.CurrentLanguage == AppLanguage.English ? $"Successfully exported to:\n{saveDialog.FileName}" : $"已成功匯出至:\n{saveDialog.FileName}",
+ MessageBox.Show(LocalizationService.CurrentLanguage == AppLanguage.English ? $"Successfully exported to:\n{saveDialog.FileName}" : $"已成功匯出至:\n{saveDialog.FileName}",
LocalizationService.CurrentLanguage == AppLanguage.English ? "Export Success" : "匯出成功", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
@@ -1452,7 +1463,7 @@ private void DisplayReport(BatteryReportData report)
bool isEn = LocalizationService.CurrentLanguage == AppLanguage.English;
string naText = isEn ? "N/A" : "未提供";
- string dash = "—";
+ string dash = "-";
// Score & Badge
bool healthMeasured = metrics.HasBattery && metrics.IsHealthMeasured;
@@ -1527,8 +1538,8 @@ private void DisplayReport(BatteryReportData report)
SepSpecMade.Visibility = Visibility.Collapsed;
}
- TxtReportTime.Text = string.IsNullOrWhiteSpace(report.SystemInfo.ReportTime)
- ? $"{DateTime.Now:yyyy-MM-dd HH:mm}"
+ TxtReportTime.Text = string.IsNullOrWhiteSpace(report.SystemInfo.ReportTime)
+ ? $"{DateTime.Now:yyyy-MM-dd HH:mm}"
: report.SystemInfo.ReportTime;
// Bind Lists
diff --git a/README.md b/README.md
index 2012451..546978b 100644
--- a/README.md
+++ b/README.md
@@ -94,7 +94,7 @@
| 格式 | 檔案名稱 | 說明 |
| :--- | :--- | :--- |
-| **🚀 安裝版** | `WinBatLens_v1.1.7_Setup_x64.exe` | Inno Setup 安裝程式,支援開機啟動設定與乾淨解除安裝。 |
+| **🚀 安裝版** | `WinBatLens_v1.1.7_Setup_x64.exe` | Inno Setup 安裝程式;固定建立開始功能表的啟動/解除安裝捷徑,可自行釘選到開始或工作列。桌面捷徑與開機啟動為預設不勾選的選用項目。 |
| **📦 免安裝單檔版** | `WinBatLens_v1.1.7_Portable_x64.exe` | 單一可執行檔(Single-File),免安裝、隨開即用。 |
| **📁 免安裝壓縮包** | `WinBatLens_v1.1.7_Portable_x64.zip` | 包含可執行檔、README 與授權說明的 ZIP 封裝包。 |
diff --git a/Services/LocalizationService.cs b/Services/LocalizationService.cs
index c096b9e..4da04a4 100644
--- a/Services/LocalizationService.cs
+++ b/Services/LocalizationService.cs
@@ -25,8 +25,8 @@ public class LocalizationService
private static readonly Dictionary ZhTwStrings = new Dictionary
{
- ["TabVoltageHistory"] = " 🔋 電池電壓紀錄 ",
- ["VoltageHistoryTitle"] = "🔋 電池電壓紀錄(0–100%)",
+ ["TabVoltageHistory"] = " 電池電壓紀錄 ",
+ ["VoltageHistoryTitle"] = "電池電壓紀錄 (0-100%)",
["VoltageHistorySummary"] = "已記錄 {0} / 101 個電量區間,累積 {1} 次電量變化事件",
["VoltageHistoryHint"] = "僅顯示電池驅動回報的實測電壓;同一電量未變化時不重複記錄。",
["BtnClearVoltageHistory"] = "清除電壓紀錄",
@@ -37,14 +37,14 @@ public class LocalizationService
["VoltageLastColumn"] = "最後紀錄",
["AppTitle"] = "WinBat Lens - Windows 電池健康度與即時硬體耗電監測",
["SubTitle"] = "Windows 電池健康診斷與全系統耗電監測",
- ["AutoStart"] = "🚀 開機自動啟動",
- ["BtnOpenReport"] = "📁 開啟 HTML 報告",
- ["BtnCheck"] = "⚡ 執行電池檢測",
- ["BtnExportJson"] = "📥 匯出 JSON",
- ["BtnLanguage"] = "🌐 English",
+ ["AutoStart"] = "開機自動啟動",
+ ["BtnOpenReport"] = "開啟 HTML 報告",
+ ["BtnCheck"] = "執行電池檢測",
+ ["BtnExportJson"] = "匯出 JSON",
+ ["BtnLanguage"] = "English",
["HealthTitle"] = "電池健康度 (Health Score)",
["HealthSummaryDefault"] = "請點擊右上角「執行電池檢測」按鈕載入最新報告。",
- ["BatterySpecsTitle"] = "🔋 電池規格參數",
+ ["BatterySpecsTitle"] = "電池規格參數",
["SpecName"] = "電池名稱",
["SpecMfg"] = "製造商",
["SpecChem"] = "化學材質",
@@ -53,32 +53,41 @@ public class LocalizationService
["SpecFull"] = "滿電容量",
["SpecLoss"] = "容量損耗",
["SpecCycles"] = "充放電循環",
- ["CapacityHistoryTitle"] = "📈 容量歷史紀錄",
+ ["CapacityHistoryTitle"] = "容量歷史紀錄",
["ColPeriod"] = "期間",
["ColFullCap"] = "滿電容量",
["ColDesignCap"] = "設計容量",
["ColHealthPct"] = "健康%",
- ["TabRealTime"] = " ⚡ 全系統硬體功耗分佈 ",
- ["TabHistoryLogs"] = " 📉 即時功耗與充放電歷史紀錄 ",
- ["TabDiagnostics"] = " 💡 智慧診斷與維護建議 ",
- ["TabLifeEstimates"] = " ⏱️ 續航估算表 ",
- ["TabRecentUsage"] = " 📊 最近使用紀錄 ",
- ["CardTotalPower"] = "⚡ 電池充放電功率 (實測)",
- ["CardEstTime"] = "⏱️ 電腦估算剩餘續航時間",
- ["CardTelemetry"] = "🔋 電池物理狀態 (電壓 / 電流)",
- ["CardPowerPlan"] = "⚙️ Windows 電源模式",
- ["WaveformTitle"] = "📈 60 秒即時動態走勢波形圖 (Task Manager Style Live Graph)",
+ ["TabRealTime"] = " 全系統硬體功耗分佈 ",
+ ["TabHistoryLogs"] = " 即時功耗與充放電歷史紀錄 ",
+ ["TabDiagnostics"] = " 智慧診斷與維護建議 ",
+ ["TabLifeEstimates"] = " 續航估算表 ",
+ ["TabRecentUsage"] = " 最近使用紀錄 ",
+ ["CardTotalPower"] = "電池充放電功率 (實測)",
+ ["CardEstTime"] = "電腦估算剩餘續航時間",
+ ["CardTelemetry"] = "電池物理狀態 (電壓 / 電流)",
+ ["CardPowerPlan"] = "Windows 電源模式",
+ ["WaveformTitle"] = "60 秒即時功率走勢",
["LegendDischarge"] = "放電 (W)",
["LegendCharge"] = "充電 (+W)",
["LegendGpu"] = "獨顯 (W)",
- ["HardwareTitle"] = "💻 硬體實測功耗 (Measured Hardware Power)",
- ["HwBatteryTelemetry"] = "🔋 電池物理狀態 (電壓 / 電流)",
- ["HwEnergy"] = "🔋 電池蓄電量 (Wh)",
- ["HwPowerPlan"] = "⚙️ Windows 運作電源計劃",
- ["HwDgpu"] = "🎮 獨立顯示卡 (dGPU)",
- ["BtnExportCsv"] = "📥 匯出 CSV 紀錄",
- ["BtnClearHistory"] = "🗑️ 清除紀錄",
- ["HistoryLogHeader"] = "📋 即時動態功耗與充放電事件歷史日誌 (Live Power & Event Logs)",
+ ["HardwareTitle"] = "硬體實測功耗",
+ ["HwBatteryTelemetry"] = "電池物理狀態 (電壓 / 電流)",
+ ["HwEnergy"] = "電池蓄電量 (Wh)",
+ ["HwPowerPlan"] = "Windows 運作電源計劃",
+ ["HwDgpu"] = "獨立顯示卡 (dGPU)",
+ ["HwTelemetrySource"] = "電池類別驅動程式",
+ ["HwPowerPlanState"] = "目前使用中",
+ ["BtnExportCsv"] = "匯出 CSV 紀錄",
+ ["BtnClearHistory"] = "清除紀錄",
+ ["HistoryLogHeader"] = "即時功耗與充放電事件紀錄",
+ ["PowerHistoryEmpty"] = "監測開始後,功耗事件會顯示在這裡。",
+ ["VoltageHistoryEmpty"] = "電量百分比改變且驅動回報實測電壓後,紀錄會顯示在這裡。",
+ ["DiagnosticsEmpty"] = "執行電池檢測後,診斷與維護建議會顯示在這裡。",
+ ["LifeEstimatesEmpty"] = "載入含續航資料的電池報告後,估算結果會顯示在這裡。",
+ ["RecentUsageEmpty"] = "載入含使用紀錄的電池報告後,資料會顯示在這裡。",
+ ["LoadingTitle"] = "正在執行 Windows 電池健康檢測",
+ ["LoadingDetail"] = "powercfg /batteryreport 正在產生並分析最新報告。",
["ColTime"] = "時間戳記",
["ColEvent"] = "事件類型",
["ColPower"] = "放電/充電功率",
@@ -88,10 +97,10 @@ public class LocalizationService
["ColDgpu"] = "獨顯負載",
["ColScreen"] = "螢幕功耗",
["ColSummary"] = "詳細狀態記錄",
- ["TrayShow"] = "⚡ 開啟 WinBat Lens 主畫面",
- ["TrayCheck"] = "🔄 執行電池健康檢測",
- ["TrayAutoStart"] = "🚀 開機自動啟動",
- ["TrayExit"] = "❌ 結束程式",
+ ["TrayShow"] = "開啟 WinBat Lens 主畫面",
+ ["TrayCheck"] = "執行電池健康檢測",
+ ["TrayAutoStart"] = "開機自動啟動",
+ ["TrayExit"] = "結束程式",
["TrayTooltip"] = "WinBat Lens - 電池健康度與即時耗電監測",
["TrayBalloonTitle"] = "WinBat Lens 已縮小至托盤",
["TrayBalloonText"] = "程式將在背景持續為您進行即時耗電與電池狀態監測。",
@@ -105,8 +114,8 @@ public class LocalizationService
private static readonly Dictionary EnUsStrings = new Dictionary
{
- ["TabVoltageHistory"] = " 🔋 Battery Voltage History ",
- ["VoltageHistoryTitle"] = "🔋 Battery Voltage by Charge Level (0–100%)",
+ ["TabVoltageHistory"] = " Battery Voltage History ",
+ ["VoltageHistoryTitle"] = "Battery Voltage by Charge Level (0-100%)",
["VoltageHistorySummary"] = "Recorded {0} / 101 charge-level ranges, {1} percentage-change events",
["VoltageHistoryHint"] = "Only battery-driver measurements are shown; the same percentage is not recorded again until it changes.",
["BtnClearVoltageHistory"] = "Clear Voltage History",
@@ -117,14 +126,14 @@ public class LocalizationService
["VoltageLastColumn"] = "Last recorded",
["AppTitle"] = "WinBat Lens - Windows Battery Health & Real-Time Hardware Power Monitor",
["SubTitle"] = "Windows Battery Health Diagnostics & Full System Power Monitor",
- ["AutoStart"] = "🚀 Launch on Windows Startup",
- ["BtnOpenReport"] = "📁 Open HTML Report",
- ["BtnCheck"] = "⚡ Run Battery Check",
- ["BtnExportJson"] = "📥 Export JSON",
- ["BtnLanguage"] = "🌐 繁體中文",
+ ["AutoStart"] = "Launch on Windows Startup",
+ ["BtnOpenReport"] = "Open HTML Report",
+ ["BtnCheck"] = "Run Battery Check",
+ ["BtnExportJson"] = "Export JSON",
+ ["BtnLanguage"] = "繁體中文",
["HealthTitle"] = "Battery Health Score",
["HealthSummaryDefault"] = "Click 'Run Battery Check' in the top right to load report.",
- ["BatterySpecsTitle"] = "🔋 Battery Specifications",
+ ["BatterySpecsTitle"] = "Battery Specifications",
["SpecName"] = "Battery Name",
["SpecMfg"] = "Manufacturer",
["SpecChem"] = "Chemistry",
@@ -133,32 +142,41 @@ public class LocalizationService
["SpecFull"] = "Full Charge Capacity",
["SpecLoss"] = "Capacity Loss",
["SpecCycles"] = "Cycle Count",
- ["CapacityHistoryTitle"] = "📈 Capacity Degradation History",
+ ["CapacityHistoryTitle"] = "Capacity Degradation History",
["ColPeriod"] = "Period",
["ColFullCap"] = "Full Charge Cap",
["ColDesignCap"] = "Design Cap",
["ColHealthPct"] = "Health %",
- ["TabRealTime"] = " ⚡ Hardware Power Breakdown ",
- ["TabHistoryLogs"] = " 📉 Power & Event History Logs ",
- ["TabDiagnostics"] = " 💡 Smart Diagnostics & Tips ",
- ["TabLifeEstimates"] = " ⏱️ Battery Life Estimates ",
- ["TabRecentUsage"] = " 📊 Recent Battery Usage ",
- ["CardTotalPower"] = "⚡ Battery Charge & Discharge (measured)",
- ["CardEstTime"] = "⏱️ Estimated Battery Life Remaining",
- ["CardTelemetry"] = "🔋 Battery Hardware State (V / A)",
- ["CardPowerPlan"] = "⚙️ Active Windows Power Scheme",
- ["WaveformTitle"] = "📈 60-Second Real-Time Power & Load Waveform Graph",
+ ["TabRealTime"] = " Hardware Power Breakdown ",
+ ["TabHistoryLogs"] = " Power & Event History Logs ",
+ ["TabDiagnostics"] = " Smart Diagnostics & Tips ",
+ ["TabLifeEstimates"] = " Battery Life Estimates ",
+ ["TabRecentUsage"] = " Recent Battery Usage ",
+ ["CardTotalPower"] = "Battery Charge & Discharge (measured)",
+ ["CardEstTime"] = "Estimated Battery Life Remaining",
+ ["CardTelemetry"] = "Battery Hardware State (V / A)",
+ ["CardPowerPlan"] = "Active Windows Power Scheme",
+ ["WaveformTitle"] = "60-Second Real-Time Power Waveform",
["LegendDischarge"] = "Discharge (W)",
["LegendCharge"] = "Charge (+W)",
["LegendGpu"] = "dGPU (W)",
- ["HardwareTitle"] = "💻 Measured Hardware Power",
- ["HwBatteryTelemetry"] = "🔋 Battery Telemetry (V / A)",
- ["HwEnergy"] = "🔋 Battery Energy (Wh)",
- ["HwPowerPlan"] = "⚙️ Windows Power Scheme",
- ["HwDgpu"] = "🎮 Discrete GPU (dGPU)",
- ["BtnExportCsv"] = "📥 Export CSV",
- ["BtnClearHistory"] = "🗑️ Clear History",
- ["HistoryLogHeader"] = "📋 Real-Time Dynamic Power & Charge/Discharge Event Logs",
+ ["HardwareTitle"] = "Measured Hardware Power",
+ ["HwBatteryTelemetry"] = "Battery Telemetry (V / A)",
+ ["HwEnergy"] = "Battery Energy (Wh)",
+ ["HwPowerPlan"] = "Windows Power Scheme",
+ ["HwDgpu"] = "Discrete GPU (dGPU)",
+ ["HwTelemetrySource"] = "Battery class driver",
+ ["HwPowerPlanState"] = "Active scheme",
+ ["BtnExportCsv"] = "Export CSV",
+ ["BtnClearHistory"] = "Clear History",
+ ["HistoryLogHeader"] = "Power and Charge/Discharge Event History",
+ ["PowerHistoryEmpty"] = "Power events will appear here after monitoring begins.",
+ ["VoltageHistoryEmpty"] = "Records appear after charge level changes and the battery driver reports a measured voltage.",
+ ["DiagnosticsEmpty"] = "Run a battery check to see diagnostics and maintenance guidance.",
+ ["LifeEstimatesEmpty"] = "Load a battery report with runtime data to see estimates here.",
+ ["RecentUsageEmpty"] = "Load a battery report with usage records to see data here.",
+ ["LoadingTitle"] = "Running Windows Battery Health Check",
+ ["LoadingDetail"] = "powercfg /batteryreport is generating and analyzing the latest report.",
["ColTime"] = "Timestamp",
["ColEvent"] = "Event Type",
["ColPower"] = "Power (W)",
@@ -168,10 +186,10 @@ public class LocalizationService
["ColDgpu"] = "dGPU Load",
["ColScreen"] = "Screen Power",
["ColSummary"] = "Log Details",
- ["TrayShow"] = "⚡ Open WinBat Lens",
- ["TrayCheck"] = "🔄 Run Battery Health Check",
- ["TrayAutoStart"] = "🚀 Launch on Windows Startup",
- ["TrayExit"] = "❌ Exit",
+ ["TrayShow"] = "Open WinBat Lens",
+ ["TrayCheck"] = "Run Battery Health Check",
+ ["TrayAutoStart"] = "Launch on Windows Startup",
+ ["TrayExit"] = "Exit",
["TrayTooltip"] = "WinBat Lens - Battery Health & Live Power Monitor",
["TrayBalloonTitle"] = "WinBat Lens minimized to tray",
["TrayBalloonText"] = "Power and battery monitoring continues in the background.",
@@ -199,8 +217,8 @@ public static string Get(string key)
///
public static void ToggleLanguage()
{
- CurrentLanguage = CurrentLanguage == AppLanguage.TraditionalChinese
- ? AppLanguage.English
+ CurrentLanguage = CurrentLanguage == AppLanguage.TraditionalChinese
+ ? AppLanguage.English
: AppLanguage.TraditionalChinese;
}
}
diff --git a/installer/WinBatLens.iss b/installer/WinBatLens.iss
index 25719c5..00bc7f7 100644
--- a/installer/WinBatLens.iss
+++ b/installer/WinBatLens.iss
@@ -1,8 +1,10 @@
; WinBat Lens Inno Setup Script
#define MyAppName "WinBat Lens"
#define MyAppPublisher "WinBat Lens Team"
+#define MyAppURL "https://github.com/nojackno2-ctrl/WinBat-Lens"
#define MyAppExeName "WinBatLens.exe"
#define MyAppId "{{D2B3F0E1-8E4B-4D2A-9A2C-5F1B3E7A902A}"
+#define MyAppUserModelId "nojackno2.WinBatLens"
#define MyPublishExe "..\bin\Release\net10.0-windows\win-x64\publish\WinBatLens.exe"
; Named kernel objects the running app owns. Both are part of a cross-version
@@ -25,29 +27,45 @@
AppId={#MyAppId}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
+AppVerName={#MyAppName} v{#MyAppVersion}
AppPublisher={#MyAppPublisher}
+AppPublisherURL={#MyAppURL}
+AppSupportURL={#MyAppURL}/issues
+AppUpdatesURL={#MyAppURL}/releases
+AppReadmeFile={app}\README.md
+VersionInfoVersion={#MyAppVersion}.0
+VersionInfoCompany={#MyAppPublisher}
+VersionInfoDescription={#MyAppName} installer
+VersionInfoTextVersion={#MyAppVersion}
+VersionInfoProductVersion={#MyAppVersion}.0
+VersionInfoProductName={#MyAppName}
DefaultDirName={autopf}\{#MyAppName}
DefaultGroupName={#MyAppName}
LicenseFile=..\LICENSE
SetupIconFile=..\app_icon.ico
UninstallDisplayIcon={app}\{#MyAppExeName}
+UninstallDisplayName={#MyAppName} v{#MyAppVersion}
+Uninstallable=yes
OutputDir=..\dist
OutputBaseFilename=WinBatLens_v{#MyAppVersion}_Setup_x64
Compression=lzma2/ultra64
SolidCompression=yes
WizardStyle=modern
-; The Start Menu folder is fixed to DefaultGroupName; whether shortcuts get
-; created is controlled by the "startmenuicon" task on the Tasks page instead.
+; Always create an application and uninstall shortcut in this stable folder so
+; Windows Search and the user's manual Pin to Start/Taskbar actions can find it.
DisableProgramGroupPage=yes
ShowLanguageDialog=yes
+PrivilegesRequired=admin
PrivilegesRequiredOverridesAllowed=commandline dialog
AppMutex={#MyAppMutex}
UsePreviousAppDir=yes
UsePreviousGroup=yes
UsePreviousTasks=yes
CreateUninstallRegKey=yes
+SetupLogging=yes
+UninstallLogging=yes
CloseApplications=yes
-CloseApplicationsFilter=*.exe
+CloseApplicationsFilter={#MyAppExeName}
RestartApplications=no
[Languages]
@@ -58,9 +76,8 @@ Name: "chinesetrad"; MessagesFile: "ChineseTraditional.isl"
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
-Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"
-Name: "startmenuicon"; Description: "{cm:CreateStartMenuIcon}"; GroupDescription: "{cm:AdditionalIcons}"
-Name: "autostart"; Description: "{cm:AutoStartTask}"; GroupDescription: "{cm:StartupOptions}"
+Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
+Name: "autostart"; Description: "{cm:AutoStartTask}"; GroupDescription: "{cm:StartupOptions}"; Flags: unchecked
[Files]
Source: "{#MyPublishExe}"; DestDir: "{app}"; Flags: ignoreversion
@@ -68,15 +85,13 @@ Source: "..\README.md"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\LICENSE"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
-Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: startmenuicon
-Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"; Tasks: startmenuicon
-Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
+Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; AppUserModelID: "{#MyAppUserModelId}"
+Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
+Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; AppUserModelID: "{#MyAppUserModelId}"; Tasks: desktopicon
[CustomMessages]
-chinesetrad.CreateStartMenuIcon=建立開始功能表捷徑
chinesetrad.AutoStartTask=開機時自動於背景啟動 WinBat Lens
chinesetrad.StartupOptions=啟動選項:
-english.CreateStartMenuIcon=Create a &Start Menu shortcut
english.AutoStartTask=Automatically start WinBat Lens in the background on Windows startup
english.StartupOptions=Startup options:
diff --git a/tests/WinBatLens.Tests/InstallerContractTests.cs b/tests/WinBatLens.Tests/InstallerContractTests.cs
new file mode 100644
index 0000000..8063373
--- /dev/null
+++ b/tests/WinBatLens.Tests/InstallerContractTests.cs
@@ -0,0 +1,73 @@
+using Xunit;
+
+namespace WinBatLens.Tests;
+
+public sealed class InstallerContractTests
+{
+ [Fact]
+ public void InstallerProvidesConsistentUpgradeAndDiagnosticsMetadata()
+ {
+ var script = ReadInstallerScript();
+
+ foreach (var directive in new[]
+ {
+ "DisableProgramGroupPage=yes",
+ "UsePreviousAppDir=yes",
+ "UsePreviousGroup=yes",
+ "UsePreviousTasks=yes",
+ "CreateUninstallRegKey=yes",
+ "SetupLogging=yes",
+ "UninstallLogging=yes",
+ "CloseApplicationsFilter={#MyAppExeName}",
+ "RestartApplications=no"
+ })
+ {
+ Assert.Contains(directive, script.Lines, StringComparer.Ordinal);
+ }
+
+ Assert.Contains("AppPublisherURL={#MyAppURL}", script.Lines, StringComparer.Ordinal);
+ Assert.Contains("AppSupportURL={#MyAppURL}/issues", script.Lines, StringComparer.Ordinal);
+ Assert.Contains("AppUpdatesURL={#MyAppURL}/releases", script.Lines, StringComparer.Ordinal);
+ }
+
+ [Fact]
+ public void StartMenuAndOptionalShortcutPolicyIsPreserved()
+ {
+ var script = ReadInstallerScript();
+ var startMenuShortcut = Assert.Single(script.Lines, line =>
+ line.StartsWith("Name: \"{group}\\{#MyAppName}\";", StringComparison.Ordinal));
+ var uninstallShortcut = Assert.Single(script.Lines, line =>
+ line.Contains("{cm:UninstallProgram,{#MyAppName}}", StringComparison.Ordinal));
+ var desktopTask = Assert.Single(script.Lines, line =>
+ line.StartsWith("Name: \"desktopicon\";", StringComparison.Ordinal));
+ var startupTask = Assert.Single(script.Lines, line =>
+ line.StartsWith("Name: \"autostart\";", StringComparison.Ordinal));
+
+ Assert.DoesNotContain("Tasks:", startMenuShortcut, StringComparison.Ordinal);
+ Assert.DoesNotContain("Tasks:", uninstallShortcut, StringComparison.Ordinal);
+ Assert.Contains("WorkingDir: \"{app}\"", startMenuShortcut, StringComparison.Ordinal);
+ Assert.Contains("AppUserModelID:", startMenuShortcut, StringComparison.Ordinal);
+ Assert.Contains("Flags: unchecked", desktopTask, StringComparison.Ordinal);
+ Assert.Contains("Flags: unchecked", startupTask, StringComparison.Ordinal);
+ Assert.DoesNotContain(script.Lines, line => line.StartsWith("Name: \"startmenuicon\";", StringComparison.Ordinal));
+ }
+
+ private static (string Content, string[] Lines) ReadInstallerScript()
+ {
+ var repositoryRoot = FindRepositoryRoot();
+ var content = File.ReadAllText(Path.Combine(repositoryRoot, "installer", "WinBatLens.iss"));
+ return (content, content.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries));
+ }
+
+ private static string FindRepositoryRoot()
+ {
+ var directory = new DirectoryInfo(AppContext.BaseDirectory);
+ while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "WinBatLens.csproj")))
+ {
+ directory = directory.Parent;
+ }
+
+ Assert.NotNull(directory);
+ return directory.FullName;
+ }
+}