Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,24 @@ jobs:
Compress-Archive -Path "publish-selfcontained/*" -DestinationPath $zipNameSelfContained
echo "ZIP_NAME_SELFCONTAINED=$zipNameSelfContained" >> $env:GITHUB_ENV

- name: Install Inno Setup
run: winget install --id JRSoftware.InnoSetup.7 -e --source winget --accept-package-agreements --accept-source-agreements --disable-interactivity

- name: Build installer
shell: pwsh
run: |
$version = "${{ github.ref_name }}".TrimStart('v')
$iscc = (Get-ChildItem "$env:LocalAppData\Programs\Inno Setup*\ISCC.exe", "C:\Program Files*\Inno Setup*\ISCC.exe" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1).FullName
& $iscc installer\PayBeat.iss "/DAppVersion=$version"
$setupName = Get-ChildItem "installer-output\*.exe" | Select-Object -First 1 -ExpandProperty Name
echo "SETUP_NAME=installer-output/$setupName" >> $env:GITHUB_ENV

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: PayBeat ${{ github.ref_name }}
files: |
${{ env.ZIP_NAME }}
${{ env.ZIP_NAME_SELFCONTAINED }}
${{ env.SETUP_NAME }}
generate_release_notes: true
7 changes: 5 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,13 @@ WPF floating widget app (.NET 10, MVVM). Shows real-time earnings as a borderles
- `StartupService` manages the Windows startup registry entry (`HKCU\...\Run`) for the "Run at startup" setting.
- `LocalizationService` handles runtime language switching by swapping `ResourceDictionary` entries in `MergedDictionaries`.
- Localization: `Strings.en.xaml` / `Strings.zh-CN.xaml` are swapped into `MergedDictionaries` at startup; UI strings use `{DynamicResource}`. `"auto"` resolves from `CultureInfo.CurrentUICulture`.
- `ThemeService` swaps `Theme.Light.xaml`/`Theme.Dark.xaml` (palette-only resource dictionaries) into `MergedDictionaries` the same way localization does; `"auto"` resolves from the `AppsUseLightTheme` registry value under `HKCU\...\Themes\Personalize`.

**Models:**
- `SalarySettings` — immutable `record`; defaults: `DailySalary=500`, `WorkStart=09:00`, `WorkEnd=18:00`, `Currency="¥"`, `DisplayMode=Normal`, `AlwaysOnTop=true`, `Opacity=1.0`, `RefreshInterval=1`, `Language="auto"`, `HotkeyModifiers=0x0003` (Ctrl+Alt), `HotkeyVirtualKey=0x58` (X). `MaxDailySalary` caps input at 99,999,999. Stores per-mode `WindowPosition` (Left, Top, ScreenDeviceName). Also carries `LunchBreakEnabled`/`LunchBreakStart`/`LunchBreakEnd`, `WorkOnWeekends`, and tray-balloon reminder toggles (`EnableEndOfDayReminder`/`EndOfDayReminderMinutes`, `EnableMilestoneNotifications`/`MilestoneAmount`).
- `EarningsCalculator` — all earnings math is a pure function of `SalarySettings` + `DateTime`; `IsWorkday()` gates weekends, and `EffectiveWorkSeconds()`/`EffectiveElapsedSeconds()` subtract the lunch break window (elapsed time holds steady during the break) before `Calculate()`/`RatePerSecond()`/`WorkdayProgress()` divide by it.

**UI theme:** Catppuccin Mocha dark palette (background `#1E1E2E`, surface `#313244`, text `#CDD6F4`, green accent `#A6E3A1`, blue accent `#89B4FA`). Styles live in `src/PayBeat.App/Resources/Styles.xaml`. UI strings live in `Strings.en.xaml` / `Strings.zh-CN.xaml` and are accessed via `{DynamicResource}`.
**UI theme:** Catppuccin-derived palette, with separate `Theme.Light.xaml`/`Theme.Dark.xaml` dictionaries (dark: background `#1E1E2E`, surface `#313244`, text `#CDD6F4`, green accent `#A6E3A1`, blue accent `#89B4FA`) swapped at runtime by `ThemeService`; structural styles live in `src/PayBeat.App/Resources/Styles.xaml` and reference the palette via `{DynamicResource}`. UI strings live in `Strings.en.xaml` / `Strings.zh-CN.xaml`, also accessed via `{DynamicResource}`.

## Solution Configuration

Expand All @@ -74,7 +75,9 @@ Artifacts output to `artifacts/bin/<ProjectName>/<config>/` (SDK artifacts layou

## CI / Release

`.github/workflows/ci.yml` has two jobs (Windows runner, .NET 10). `build` runs on every push and just compiles. `release` runs `needs: build` with a job-level `if: startsWith(github.ref, 'refs/tags/v')` — only on a `v*` tag push does it publish both a portable (`--no-self-contained`) and a self-contained `win-x64` build, zip each (`PayBeat-<version>-portable-runtime-required-win-x64.zip`, `PayBeat-<version>-portable-standalone-win-x64.zip`), and create a GitHub Release via `softprops/action-gh-release`. Versioning is derived from git tags via MinVer (e.g. `v1.2.0`); locally, ensure the tag is reachable from HEAD for a meaningful version.
`.github/workflows/ci.yml` has two jobs (Windows runner, .NET 10). `build` runs on every push and just compiles. `release` runs `needs: build` with a job-level `if: startsWith(github.ref, 'refs/tags/v')` — only on a `v*` tag push does it publish both a portable (`--no-self-contained`) and a self-contained `win-x64` build, zip each (`PayBeat-<version>-portable-runtime-required-win-x64.zip`, `PayBeat-<version>-portable-standalone-win-x64.zip`), compile `installer/PayBeat.iss` with Inno Setup (installed via Chocolatey) into `PayBeat-<version>-setup-win-x64.exe`, and create a GitHub Release via `softprops/action-gh-release` with all three files. Versioning is derived from git tags via MinVer (e.g. `v1.2.0`); locally, ensure the tag is reachable from HEAD for a meaningful version. The installer script itself takes its version from `/DAppVersion=<version>` (tag name with the `v` prefix stripped), not MinVer.

`installer/PayBeat.iss` packages the self-contained publish output into a per-user installer (`PrivilegesRequired=lowest`, installs under `{localappdata}\Programs\PayBeat`, no UAC prompt). It refuses to install/uninstall while the app is running (checks the `PayBeat_SingleInstance` mutex from `App.xaml.cs`) and cleans up `%APPDATA%\PayBeat` and the HKCU `Run` startup entry on uninstall. Build the publish output first (`dotnet publish ... --self-contained -o publish-selfcontained/`), then compile with `ISCC.exe installer\PayBeat.iss /DAppVersion=<version>`.

User settings are persisted to `%APPDATA%\PayBeat\settings.json`.

Expand Down
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ PayBeat is a borderless, always-on-top Windows widget that shows your real-time

## Download

Grab the latest release from the [Releases page](https://github.com/coldhighsun/PayBeat/releases/latest). Two `win-x64` packages are published per version:
Grab the latest release from the [Releases page](https://github.com/coldhighsun/PayBeat/releases/latest). Three `win-x64` packages are published per version:

- `PayBeat-<version>-portable-runtime-required-win-x64.zip` — smaller, requires the .NET 10 Desktop Runtime
- `PayBeat-<version>-portable-standalone-win-x64.zip` — larger, runs standalone with no prerequisites
- `PayBeat-<version>-setup-win-x64.exe` — self-contained installer (no runtime prerequisite); installs per-user, adds a Start Menu shortcut, and registers for uninstall

## Build

Expand All @@ -61,9 +62,13 @@ dotnet publish src/PayBeat.App/PayBeat.App.csproj -c Release

# Publish (self-contained)
dotnet publish src/PayBeat.App/PayBeat.App.csproj -c Release -r win-x64 --self-contained

# Build installer (requires Inno Setup 7 and a self-contained publish at publish-selfcontained/)
dotnet publish src/PayBeat.App/PayBeat.App.csproj -c Release -r win-x64 --self-contained -o publish-selfcontained/
ISCC.exe installer\PayBeat.iss /DAppVersion=1.2.3
```

Output goes to `artifacts/bin/PayBeat.App/release/`. CI tags produce both a `PayBeat-<version>-portable-runtime-required-win-x64.zip` and a `PayBeat-<version>-portable-standalone-win-x64.zip` release artifact.
Output goes to `artifacts/bin/PayBeat.App/release/`. CI tags produce a `PayBeat-<version>-portable-runtime-required-win-x64.zip`, a `PayBeat-<version>-portable-standalone-win-x64.zip`, and a `PayBeat-<version>-setup-win-x64.exe` release artifact.

## Usage

Expand Down Expand Up @@ -133,10 +138,11 @@ PayBeat 是一款 Windows 桌面悬浮组件,以秒为单位实时显示当天

## 下载

前往 [Releases 页面](https://github.com/coldhighsun/PayBeat/releases/latest) 下载最新版本。每个版本会发布两个 `win-x64` 压缩包
前往 [Releases 页面](https://github.com/coldhighsun/PayBeat/releases/latest) 下载最新版本。每个版本会发布三个 `win-x64` 安装包

- `PayBeat-<version>-portable-runtime-required-win-x64.zip` — 体积较小,需预装 .NET 10 Desktop Runtime
- `PayBeat-<version>-portable-standalone-win-x64.zip` — 体积较大,内置运行时,无需任何前置依赖
- `PayBeat-<version>-setup-win-x64.exe` — 内置运行时的安装程序,无需前置依赖;按当前用户安装,创建开始菜单快捷方式并支持卸载

## 构建

Expand All @@ -152,9 +158,13 @@ dotnet publish src/PayBeat.App/PayBeat.App.csproj -c Release

# 发布(独立版)
dotnet publish src/PayBeat.App/PayBeat.App.csproj -c Release -r win-x64 --self-contained

# 生成安装包(需要 Inno Setup 7,且需先生成独立版发布产物至 publish-selfcontained/)
dotnet publish src/PayBeat.App/PayBeat.App.csproj -c Release -r win-x64 --self-contained -o publish-selfcontained/
ISCC.exe installer\PayBeat.iss /DAppVersion=1.2.3
```

产物输出至 `artifacts/bin/PayBeat.App/release/`。CI 打标签时会同时生成 `PayBeat-<version>-portable-runtime-required-win-x64.zip``PayBeat-<version>-portable-standalone-win-x64.zip` 两个发布包
产物输出至 `artifacts/bin/PayBeat.App/release/`。CI 打标签时会同时生成 `PayBeat-<version>-portable-runtime-required-win-x64.zip``PayBeat-<version>-portable-standalone-win-x64.zip` 和 `PayBeat-<version>-setup-win-x64.exe` 三个发布包

## 使用

Expand Down
95 changes: 95 additions & 0 deletions installer/PayBeat.iss
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
; Inno Setup 7 script for PayBeat.
; Packages the self-contained win-x64 publish output into a per-user installer.
; Build the publish output first:
; dotnet publish src/PayBeat.App/PayBeat.App.csproj -c Release -r win-x64 --self-contained -o publish-selfcontained/
; Then compile with:
; ISCC.exe installer\PayBeat.iss /DAppVersion=1.2.3

#ifndef AppVersion
#define AppVersion "0.0.0"
#endif

#define AppName "PayBeat"
#define AppPublisher "coldhighsun"
#define AppUrl "https://github.com/coldhighsun/PayBeat"
#define AppExeName "PayBeat.exe"
#define PublishDir "..\publish-selfcontained"

[Setup]
AppId={{305F5C59-E76E-43F4-BB60-9C97994A151C}
AppName={#AppName}
AppVersion={#AppVersion}
AppPublisher={#AppPublisher}
AppPublisherURL={#AppUrl}
AppSupportURL={#AppUrl}
AppUpdatesURL={#AppUrl}
DefaultDirName={localappdata}\Programs\{#AppName}
DefaultGroupName={#AppName}
PrivilegesRequired=lowest
DisableProgramGroupPage=yes
OutputBaseFilename=PayBeat-{#AppVersion}-setup-win-x64
OutputDir=..\installer-output
SetupIconFile=..\src\PayBeat.App\Resources\Icons\PayBeat.ico
UninstallDisplayIcon={app}\{#AppExeName}
Compression=lzma2/max
SolidCompression=yes
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
LicenseFile=..\LICENSE
WizardStyle=modern

[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl"

[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"

[Files]
Source: "{#PublishDir}\*"; DestDir: "{app}"; Flags: recursesubdirs ignoreversion

[Icons]
Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"
Name: "{group}\{cm:UninstallProgram,{#AppName}}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon

[Run]
Filename: "{app}\{#AppExeName}"; Description: "{cm:LaunchProgram,{#AppName}}"; Flags: nowait postinstall skipifsilent

[UninstallDelete]
Type: filesandordirs; Name: "{userappdata}\{#AppName}"

[Code]
function InitializeSetup(): Boolean;
begin
Result := True;
while CheckForMutexes('PayBeat_SingleInstance') do
begin
if MsgBox('{#AppName} is currently running. Please close it before continuing setup.', mbError, MB_RETRYCANCEL) = IDCANCEL then
begin
Result := False;
Exit;
end;
end;
end;

function InitializeUninstall(): Boolean;
begin
Result := True;
while CheckForMutexes('PayBeat_SingleInstance') do
begin
if MsgBox('{#AppName} is currently running. Please close it before continuing uninstall.', mbError, MB_RETRYCANCEL) = IDCANCEL then
begin
Result := False;
Exit;
end;
end;
end;

procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
begin
if CurUninstallStep = usUninstall then
begin
RegDeleteValue(HKEY_CURRENT_USER, 'Software\Microsoft\Windows\CurrentVersion\Run', '{#AppName}');
end;
end;
Loading