Skip to content

Commit 4d4a25c

Browse files
jaredthirskclaude
andcommitted
Add multiple features and improvements
Features: - Add run at startup setting with Windows Registry integration - Add logo to About pane in settings window - Add double-click tray icon to open settings - Add separate font settings for analog vs digital clocks - Add window maximized state persistence - Add click-through mode persistence UI/UX Improvements: - Fix number alignment in analog clock circles - Fix About page showing version twice - Update tray icon to use actual app icon - Change minimum window opacity to 0.05 with 0.05 granularity - Fix icon path on About page to use pack URI CI/CD & Release: - Update build workflow to create GitHub releases on tags - Separate build artifacts for VirusTotal scanning (32MB limit) - Add VirusTotal scanning integration for smaller builds - Add status badges to README (build, release, downloads, license) - Add security section to README - Create VirusTotal setup documentation Technical: - Update project file to embed icon as resource - Fix FontFamily ambiguity in AnalogClock - Add analog font properties to ClockSettings - Implement font UI controls in Analog tab 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d188dfa commit 4d4a25c

File tree

10 files changed

+638
-39
lines changed

10 files changed

+638
-39
lines changed

.github/workflows/build.yml

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@ name: Build and Package
22

33
on:
44
push:
5-
branches: [ main ]
5+
# branches: [ main ] # Uncomment to also build on main branch pushes
66
tags:
77
- 'v*'
8+
paths-ignore:
9+
- 'docs/**'
10+
- '**.md'
11+
- '.gitignore'
12+
- 'LICENSE'
813
pull_request:
914
branches: [ main ]
15+
paths-ignore:
16+
- 'docs/**'
17+
- '**.md'
18+
- '.gitignore'
19+
- 'LICENSE'
1020
workflow_dispatch:
1121

1222
jobs:
@@ -83,30 +93,102 @@ jobs:
8393
Remove-Item -Recurse -Force artifacts/self-contained
8494
Remove-Item -Recurse -Force artifacts/portable
8595
86-
# Upload artifacts
87-
- name: Upload Build Artifacts
96+
# Upload artifacts separately for VirusTotal size limits
97+
- name: Upload Framework-Dependent Build
8898
uses: actions/upload-artifact@v4
8999
with:
90-
name: hudclock-builds
91-
path: artifacts/*.zip
100+
name: hudclock-framework-dependent
101+
path: artifacts/HudClock-win-x64-framework-dependent.zip
92102

93-
release:
103+
- name: Upload Portable Build
104+
uses: actions/upload-artifact@v4
105+
with:
106+
name: hudclock-portable
107+
path: artifacts/HudClock-win-x64-portable.zip
108+
109+
- name: Upload Self-Contained Build (Large)
110+
uses: actions/upload-artifact@v4
111+
with:
112+
name: hudclock-self-contained
113+
path: artifacts/HudClock-win-x64-self-contained.zip
114+
115+
virustotal-scan:
94116
needs: build
95117
runs-on: ubuntu-latest
96118
if: startsWith(github.ref, 'refs/tags/v')
119+
continue-on-error: true # Don't fail the workflow if scanning fails
120+
121+
steps:
122+
- name: Download Framework-Dependent artifact
123+
uses: actions/download-artifact@v4
124+
with:
125+
name: hudclock-framework-dependent
126+
path: scan-artifacts
127+
128+
- name: Download Portable artifact
129+
uses: actions/download-artifact@v4
130+
with:
131+
name: hudclock-portable
132+
path: scan-artifacts
133+
134+
- name: VirusTotal Scan
135+
uses: crazy-max/ghaction-virustotal@v4
136+
id: virustotal
137+
with:
138+
vt_api_key: ${{ secrets.VT_API_KEY }}
139+
files: |
140+
scan-artifacts/*.zip
141+
142+
- name: Generate scan results
143+
if: steps.virustotal.outputs.analysis != ''
144+
run: |
145+
echo "## VirusTotal Scan Results" >> $GITHUB_STEP_SUMMARY
146+
echo "" >> $GITHUB_STEP_SUMMARY
147+
echo "Framework-dependent build: [View scan](https://www.virustotal.com/gui/file-analysis/${{ fromJSON(steps.virustotal.outputs.analysis).HudClock-win-x64-framework-dependent.zip.id }})" >> $GITHUB_STEP_SUMMARY
148+
echo "Portable build: [View scan](https://www.virustotal.com/gui/file-analysis/${{ fromJSON(steps.virustotal.outputs.analysis).HudClock-win-x64-portable.zip.id }})" >> $GITHUB_STEP_SUMMARY
149+
150+
- name: Comment on Release (if created)
151+
if: steps.virustotal.outputs.analysis != ''
152+
env:
153+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
154+
run: |
155+
# Wait for release to be created
156+
sleep 30
157+
# Add VirusTotal info to release
158+
gh release edit "${{ github.ref_name }}" \
159+
--notes-file - <<EOF
160+
$(gh release view "${{ github.ref_name }}" --json body -q .body)
161+
162+
## 🛡️ Security Scans
163+
- Framework-dependent build: [VirusTotal scan](https://www.virustotal.com/gui/file-analysis/${{ fromJSON(steps.virustotal.outputs.analysis).HudClock-win-x64-framework-dependent.zip.id }})
164+
- Portable build: [VirusTotal scan](https://www.virustotal.com/gui/file-analysis/${{ fromJSON(steps.virustotal.outputs.analysis).HudClock-win-x64-portable.zip.id }})
165+
166+
*Note: Self-contained build exceeds VirusTotal's free API size limit (32MB)*
167+
EOF
168+
169+
release:
170+
needs: [build, virustotal-scan]
171+
runs-on: ubuntu-latest
172+
if: startsWith(github.ref, 'refs/tags/v')
97173
permissions:
98174
contents: write
99175

100176
steps:
101177
- name: Checkout code
102178
uses: actions/checkout@v4
103179

104-
- name: Download artifacts
180+
- name: Download all artifacts
105181
uses: actions/download-artifact@v4
106182
with:
107-
name: hudclock-builds
108183
path: artifacts
109184

185+
- name: Prepare release files
186+
run: |
187+
# Move all zip files to a single directory
188+
mkdir -p release-files
189+
find artifacts -name "*.zip" -exec mv {} release-files/ \;
190+
ls -la release-files/
191+
110192
- name: Create Release
111193
env:
112194
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -134,4 +216,4 @@ jobs:
134216
135217
## What's New
136218
See [commits](https://github.com/lionfire/hudclock/commits/${{ github.ref_name }}) for details." \
137-
artifacts/*.zip
219+
release-files/*.zip

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44
<img src="assets/icon/hud_clock_icon_small.svg" alt="HudClock Icon" width="128" height="128">
55
</p>
66

7+
<p align="center">
8+
<a href="https://github.com/lionfire/hudclock/actions/workflows/build.yml"><img src="https://img.shields.io/github/actions/workflow/status/lionfire/hudclock/build.yml?label=Build&logo=github" alt="Build Status"></a>
9+
<a href="https://github.com/lionfire/hudclock/releases/latest"><img src="https://img.shields.io/github/v/release/lionfire/hudclock?label=Release&logo=github" alt="Latest Release"></a>
10+
<a href="https://github.com/lionfire/hudclock/releases/latest"><img src="https://img.shields.io/github/downloads/lionfire/hudclock/total?label=Downloads&logo=github" alt="Total Downloads"></a>
11+
<a href="https://github.com/lionfire/hudclock/blob/main/LICENSE"><img src="https://img.shields.io/github/license/lionfire/hudclock?label=License" alt="License"></a>
12+
<img src="https://img.shields.io/badge/Platform-Windows-blue?logo=windows" alt="Platform">
13+
<img src="https://img.shields.io/badge/Framework-.NET%208-512BD4?logo=dotnet" alt=".NET 8">
14+
</p>
15+
716
<p align="center">
817
A transparent, always-on-top clock overlay for Windows that provides unique time visualization options including 24-hour analog, metric time, and minimalist designs.
918
</p>
@@ -21,6 +30,18 @@ HudClock is a WPF application designed to provide an unobtrusive yet constantly
2130
- **System Tray Integration**: Minimize to tray with configurable visibility
2231
- **High Performance**: Configurable update rates up to 120fps for smooth animations
2332

33+
## Download & Installation
34+
35+
Download the latest release from the [Releases page](https://github.com/lionfire/hudclock/releases/latest).
36+
37+
### Available Versions
38+
- **Self-contained** (Recommended) - Single ~90MB executable with everything included
39+
- **Framework-dependent** - Smaller ~5MB executable (requires .NET 8 Desktop Runtime)
40+
- **Portable** - Traditional multi-file portable application
41+
42+
### Security
43+
The framework-dependent and portable builds are automatically scanned with VirusTotal when released. Scan results are included in each release's notes. The self-contained build exceeds VirusTotal's free API size limit (32MB) but uses the same verified source code.
44+
2445
## Technical Details
2546

2647
- **Platform**: Windows (WPF/.NET 8.0)

docs/VIRUSTOTAL_SETUP.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# VirusTotal Integration Setup
2+
3+
## Prerequisites
4+
5+
1. Create a free VirusTotal account at https://www.virustotal.com
6+
2. Get your API key from: https://www.virustotal.com/gui/my-apikey
7+
3. Add the API key to your GitHub repository secrets:
8+
- Go to Settings → Secrets and variables → Actions
9+
- Click "New repository secret"
10+
- Name: `VT_API_KEY`
11+
- Value: Your VirusTotal API key
12+
13+
## How It Works
14+
15+
When you push a new tag (e.g., `v1.0.1`), the GitHub Actions workflow will:
16+
17+
1. Build three versions of HudClock:
18+
- Framework-dependent (~5MB)
19+
- Portable (~10MB)
20+
- Self-contained (~90MB)
21+
22+
2. Scan the smaller builds with VirusTotal (free API limit is 32MB)
23+
24+
3. Create a GitHub release with:
25+
- All three build artifacts
26+
- VirusTotal scan links in the release notes
27+
- Download statistics and version badges
28+
29+
## API Limitations
30+
31+
- **Free API**:
32+
- 4 requests per minute
33+
- 500 requests per day
34+
- 32MB file size limit
35+
- Public scan results
36+
37+
- **Premium API** (optional):
38+
- Higher rate limits
39+
- Larger file sizes
40+
- Private scans
41+
42+
## Manual Scanning
43+
44+
For the self-contained build or if automated scanning fails:
45+
46+
1. Download the release file
47+
2. Upload to https://www.virustotal.com
48+
3. Copy the scan URL
49+
4. Add to release notes manually
50+
51+
## Troubleshooting
52+
53+
If VirusTotal scanning fails:
54+
- Check the API key is correct
55+
- Verify file sizes are under 32MB
56+
- Check API rate limits haven't been exceeded
57+
- The workflow continues even if scanning fails (continue-on-error: true)
58+
59+
## Badge Updates
60+
61+
The README includes status badges that update automatically:
62+
- Build status
63+
- Latest release version
64+
- Total downloads
65+
- License
66+
67+
No manual updates needed for these badges.

src/wpf/AnalogClock.xaml.cs

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,11 @@ private void ApplyStyle()
252252
{
253253
Text = "",
254254
Foreground = System.Windows.Media.Brushes.Black,
255-
FontSize = 10 * elementScale,
256-
FontWeight = FontWeights.Bold,
255+
FontFamily = new System.Windows.Media.FontFamily(ClockSettings.Instance.AnalogFontFamily),
256+
FontSize = ClockSettings.Instance.AnalogFontSize * elementScale,
257+
FontWeight = (FontWeight)new FontWeightConverter().ConvertFromString(ClockSettings.Instance.AnalogFontWeight),
257258
TextAlignment = TextAlignment.Center,
259+
VerticalAlignment = VerticalAlignment.Center,
258260
Opacity = 1.0
259261
};
260262
ClockCanvas.Children.Add(hourNumber);
@@ -263,9 +265,11 @@ private void ApplyStyle()
263265
{
264266
Text = "",
265267
Foreground = System.Windows.Media.Brushes.Black,
266-
FontSize = 9 * elementScale,
267-
FontWeight = FontWeights.Bold,
268+
FontFamily = new System.Windows.Media.FontFamily(ClockSettings.Instance.AnalogFontFamily),
269+
FontSize = ClockSettings.Instance.AnalogFontSize * elementScale * 0.9,
270+
FontWeight = (FontWeight)new FontWeightConverter().ConvertFromString(ClockSettings.Instance.AnalogFontWeight),
268271
TextAlignment = TextAlignment.Center,
272+
VerticalAlignment = VerticalAlignment.Center,
269273
Opacity = 1.0
270274
};
271275
ClockCanvas.Children.Add(minuteNumber);
@@ -274,9 +278,11 @@ private void ApplyStyle()
274278
{
275279
Text = "",
276280
Foreground = System.Windows.Media.Brushes.Black,
277-
FontSize = 8 * elementScale,
278-
FontWeight = FontWeights.Bold,
281+
FontFamily = new System.Windows.Media.FontFamily(ClockSettings.Instance.AnalogFontFamily),
282+
FontSize = ClockSettings.Instance.AnalogFontSize * elementScale * 0.8,
283+
FontWeight = (FontWeight)new FontWeightConverter().ConvertFromString(ClockSettings.Instance.AnalogFontWeight),
279284
TextAlignment = TextAlignment.Center,
285+
VerticalAlignment = VerticalAlignment.Center,
280286
Opacity = 1.0
281287
};
282288
ClockCanvas.Children.Add(secondNumber);
@@ -306,9 +312,11 @@ private void ApplyStyle()
306312
{
307313
Text = "",
308314
Foreground = System.Windows.Media.Brushes.Black,
309-
FontSize = 10 * elementScale,
310-
FontWeight = FontWeights.Bold,
315+
FontFamily = new System.Windows.Media.FontFamily(ClockSettings.Instance.AnalogFontFamily),
316+
FontSize = ClockSettings.Instance.AnalogFontSize * elementScale,
317+
FontWeight = (FontWeight)new FontWeightConverter().ConvertFromString(ClockSettings.Instance.AnalogFontWeight),
311318
TextAlignment = TextAlignment.Center,
319+
VerticalAlignment = VerticalAlignment.Center,
312320
Opacity = 1.0
313321
};
314322
ClockCanvas.Children.Add(hourNumber);
@@ -320,9 +328,11 @@ private void ApplyStyle()
320328
{
321329
Text = "",
322330
Foreground = System.Windows.Media.Brushes.Black,
323-
FontSize = 9 * elementScale,
324-
FontWeight = FontWeights.Bold,
331+
FontFamily = new System.Windows.Media.FontFamily(ClockSettings.Instance.AnalogFontFamily),
332+
FontSize = ClockSettings.Instance.AnalogFontSize * elementScale * 0.9,
333+
FontWeight = (FontWeight)new FontWeightConverter().ConvertFromString(ClockSettings.Instance.AnalogFontWeight),
325334
TextAlignment = TextAlignment.Center,
335+
VerticalAlignment = VerticalAlignment.Center,
326336
Opacity = 1.0
327337
};
328338
ClockCanvas.Children.Add(minuteNumber);
@@ -334,9 +344,11 @@ private void ApplyStyle()
334344
{
335345
Text = "",
336346
Foreground = System.Windows.Media.Brushes.Black,
337-
FontSize = 8 * elementScale,
338-
FontWeight = FontWeights.Bold,
347+
FontFamily = new System.Windows.Media.FontFamily(ClockSettings.Instance.AnalogFontFamily),
348+
FontSize = ClockSettings.Instance.AnalogFontSize * elementScale * 0.8,
349+
FontWeight = (FontWeight)new FontWeightConverter().ConvertFromString(ClockSettings.Instance.AnalogFontWeight),
339350
TextAlignment = TextAlignment.Center,
351+
VerticalAlignment = VerticalAlignment.Center,
340352
Opacity = 1.0
341353
};
342354
ClockCanvas.Children.Add(secondNumber);
@@ -351,9 +363,24 @@ private void ApplyStyle()
351363
}
352364

353365
// Update font sizes if numbers exist
354-
if (hourNumber != null) hourNumber.FontSize = 10 * elementScale;
355-
if (minuteNumber != null) minuteNumber.FontSize = 9 * elementScale;
356-
if (secondNumber != null) secondNumber.FontSize = 8 * elementScale;
366+
if (hourNumber != null)
367+
{
368+
hourNumber.FontSize = ClockSettings.Instance.AnalogFontSize * elementScale;
369+
hourNumber.FontFamily = new System.Windows.Media.FontFamily(ClockSettings.Instance.AnalogFontFamily);
370+
hourNumber.FontWeight = (FontWeight)new FontWeightConverter().ConvertFromString(ClockSettings.Instance.AnalogFontWeight);
371+
}
372+
if (minuteNumber != null)
373+
{
374+
minuteNumber.FontSize = ClockSettings.Instance.AnalogFontSize * elementScale * 0.9;
375+
minuteNumber.FontFamily = new System.Windows.Media.FontFamily(ClockSettings.Instance.AnalogFontFamily);
376+
minuteNumber.FontWeight = (FontWeight)new FontWeightConverter().ConvertFromString(ClockSettings.Instance.AnalogFontWeight);
377+
}
378+
if (secondNumber != null)
379+
{
380+
secondNumber.FontSize = ClockSettings.Instance.AnalogFontSize * elementScale * 0.8;
381+
secondNumber.FontFamily = new System.Windows.Media.FontFamily(ClockSettings.Instance.AnalogFontFamily);
382+
secondNumber.FontWeight = (FontWeight)new FontWeightConverter().ConvertFromString(ClockSettings.Instance.AnalogFontWeight);
383+
}
357384
}
358385
}
359386

@@ -519,7 +546,8 @@ private void UpdateNumberPosition(TextBlock? number, double angleDegrees, double
519546

520547
double radians = angleDegrees * Math.PI / 180;
521548

522-
// Calculate position to center the text exactly at the circle position
549+
// Calculate position to center the text exactly at the circle's center
550+
// The radius here is the same as the circle's radius, so the text will be centered in the circle
523551
double x = centerX + radius * Math.Cos(radians) - textWidth / 2;
524552
double y = centerY + radius * Math.Sin(radians) - textHeight / 2;
525553

0 commit comments

Comments
 (0)