a808964cf1
* Phase 3: app icon (multi-resolution ICO + master PNG) scripts/generate-icons.ps1 renders the icon programmatically with System.Drawing - rounded teal square (#0E7C66) with a stylized white hook glyph - at 16/24/32/48/64/128/256 px and assembles a proper multi-resolution Microsoft ICO. The PNG and ICO outputs land in resources/. The script is the source of truth; re-run after editing the design. GUI csproj uses ApplicationIcon for the EXE icon and embeds the .ico + .png as Resources so MainWindow and AboutDialog can use them via WPF's resource URI scheme. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Phase 5: tray icon with minimize-to-tray and context menu GUI csproj enables UseWindowsForms (NotifyIcon lives in WinForms even in .NET 8). New Services/TrayIcon.cs wraps NotifyIcon with a context menu (Open / Restart service / Exit) and the embedded webhook-server icon. MainWindow creates the TrayIcon, hides itself on minimize and restores on tray double-click. Adds GlobalUsings.cs to alias the WPF defaults for types that exist in both WPF and WinForms (Application, MessageBox, TextBox, Binding, etc.) so existing code keeps compiling without per-file changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Phase 6+7: Inno Setup installer + GitHub Actions release pipeline installer/webhook-server.iss is an Inno Setup 6 script that: - Installs to %ProgramFiles%\WebhookServer - Creates Start Menu folder + GUI shortcut (and optional desktop icon) - Runs install-service.ps1 post-install to register the Windows Service - Runs uninstall-service.ps1 pre-uninstall to remove it - Bundles the webhook-server icon for the installer / uninstaller scripts/build-installer.ps1 is the local build helper: publishes both projects, finds ISCC.exe (PATH or standard install path), compiles the installer with the version pulled from Directory.Build.props, drops the output in dist/. .github/workflows/ci.yml runs build + test on every push/PR to main. .github/workflows/release.yml triggers on v* tags (or manual dispatch), runs tests, installs Inno Setup via choco, builds the installer, and attaches the .exe to a GitHub Release. Pre-1.0 versions are flagged prerelease automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Phase 4: backups + import/export config ConfigStore.SaveAsync now snapshots the previous config to %ProgramData%\WebhookServer\backups\config-<timestamp>.json before overwriting, retaining the last 30. Failures are silent so a backup-write hiccup never blocks an actual save. Three new admin pipe ops: - list-backups: returns newest 50 entries with timestamps and sizes - restore-backup: takes a fileName, refuses path-traversal chars, loads the named backup over the live config (which itself triggers a fresh backup of the current state via the SaveAsync hook) - import-config: replaces the current config with a GUI-supplied ServerConfig, merging encrypted secrets where the GUI didn't supply new plaintext GUI File menu items are wired: - Import config: file picker -> ImportConfigAsync - Export config: SaveFileDialog writes the current config as JSON - Backups: dynamic submenu auto-refreshed when opened, listing backups with timestamp + size; click to confirm-and-restore Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
139 lines
5.3 KiB
PowerShell
139 lines
5.3 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Generates webhook-server.ico (multi-resolution) and webhook-server.png from
|
|
a programmatic design. Re-run after changing Draw-Icon to refresh assets.
|
|
|
|
.DESCRIPTION
|
|
Renders the icon at 16/24/32/48/64/128/256 px using System.Drawing, then
|
|
assembles a Microsoft-format ICO file with each size embedded as PNG. No
|
|
external tools required.
|
|
|
|
Design: a rounded-square teal background (#0E7C66) with a stylized white
|
|
hook shape (a "J"-like curve with an arrow tip).
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[string]$OutputDir = (Join-Path $PSScriptRoot '..\resources')
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
Add-Type -AssemblyName System.Drawing
|
|
|
|
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
|
|
|
function New-IconBitmap([int]$size) {
|
|
$bmp = New-Object System.Drawing.Bitmap $size, $size, ([System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
|
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
|
|
$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
|
$g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
|
|
|
|
# Background: rounded square in brand teal.
|
|
$bgColor = [System.Drawing.Color]::FromArgb(0xFF, 0x0E, 0x7C, 0x66)
|
|
$bgBrush = New-Object System.Drawing.SolidBrush $bgColor
|
|
$radius = [int]($size * 0.22)
|
|
$rect = New-Object System.Drawing.RectangleF 0, 0, $size, $size
|
|
|
|
$path = New-Object System.Drawing.Drawing2D.GraphicsPath
|
|
$d = $radius * 2
|
|
$path.AddArc($rect.X, $rect.Y, $d, $d, 180, 90)
|
|
$path.AddArc($rect.Right - $d, $rect.Y, $d, $d, 270, 90)
|
|
$path.AddArc($rect.Right - $d, $rect.Bottom - $d, $d, $d, 0, 90)
|
|
$path.AddArc($rect.X, $rect.Bottom - $d, $d, $d, 90, 90)
|
|
$path.CloseFigure()
|
|
$g.FillPath($bgBrush, $path)
|
|
|
|
# Foreground: white hook shape - a thick curved stroke shaped like a "J"
|
|
# tipped with an arrowhead, sized relative to the canvas.
|
|
$fgColor = [System.Drawing.Color]::White
|
|
$stroke = [Math]::Max(2, [int]($size * 0.12))
|
|
$pen = New-Object System.Drawing.Pen $fgColor, $stroke
|
|
$pen.StartCap = [System.Drawing.Drawing2D.LineCap]::Round
|
|
$pen.EndCap = [System.Drawing.Drawing2D.LineCap]::Round
|
|
$pen.LineJoin = [System.Drawing.Drawing2D.LineJoin]::Round
|
|
|
|
# Hook curve: vertical down-stroke on the right, then a half-circle arc
|
|
# curling to the left and ending in a small filled dot for the hook tip.
|
|
$cx = [single]($size * 0.62)
|
|
$top = [single]($size * 0.22)
|
|
$bottom = [single]($size * 0.58)
|
|
$arcD = [single]($size * 0.34) # arc diameter
|
|
$arcLeft = [single]($cx - $arcD) # left edge of arc circle
|
|
|
|
# Vertical stroke from (cx, top) to (cx, bottom).
|
|
$g.DrawLine($pen, $cx, $top, $cx, $bottom)
|
|
|
|
# Half-circle arc beneath: starts at (cx, bottom), curls to (cx - arcD, bottom).
|
|
$arcRect = New-Object System.Drawing.RectangleF $arcLeft, ([single]($bottom - $arcD / 2)), $arcD, $arcD
|
|
$g.DrawArc($pen, $arcRect, 0, 180)
|
|
|
|
# Filled circle at the tip end of the arc.
|
|
$tipR = [single]($stroke * 0.6)
|
|
$tipX = $arcLeft
|
|
$tipY = [single]($bottom)
|
|
$brushFg = New-Object System.Drawing.SolidBrush $fgColor
|
|
$g.FillEllipse($brushFg, [single]($tipX - $tipR), [single]($tipY - $tipR), [single]($tipR * 2), [single]($tipR * 2))
|
|
|
|
$brushFg.Dispose(); $pen.Dispose(); $bgBrush.Dispose(); $path.Dispose()
|
|
$g.Dispose()
|
|
return $bmp
|
|
}
|
|
|
|
# Generate each size as PNG bytes. Hashtable keys are prefixed with "s" because
|
|
# PowerShell hashtable lookups by integer key behave inconsistently with PSObject
|
|
# wrapping; string keys round-trip cleanly.
|
|
$sizes = @(16, 24, 32, 48, 64, 128, 256)
|
|
$pngs = @{}
|
|
foreach ($s in $sizes) {
|
|
$bmp = New-IconBitmap $s
|
|
$ms = New-Object System.IO.MemoryStream
|
|
$bmp.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
$pngs["s$s"] = $ms.ToArray()
|
|
$ms.Dispose()
|
|
$bmp.Dispose()
|
|
}
|
|
|
|
# Save the master 256 PNG separately for places that need a transparent PNG.
|
|
$pngPath = Join-Path $OutputDir 'webhook-server.png'
|
|
[System.IO.File]::WriteAllBytes($pngPath, [byte[]]$pngs['s256'])
|
|
|
|
# Assemble multi-resolution ICO.
|
|
$icoPath = Join-Path $OutputDir 'webhook-server.ico'
|
|
$ms = New-Object System.IO.MemoryStream
|
|
$bw = New-Object System.IO.BinaryWriter $ms
|
|
try {
|
|
$count = $sizes.Count
|
|
$bw.Write([UInt16]0) # idReserved
|
|
$bw.Write([UInt16]1) # idType: 1 = ICO
|
|
$bw.Write([UInt16]$count) # idCount
|
|
|
|
# Directory entries (16 bytes each).
|
|
$offset = 6 + 16 * $count
|
|
foreach ($s in $sizes) {
|
|
$bytes = $pngs["s$s"]
|
|
$w = if ($s -ge 256) { 0 } else { $s }
|
|
$h = if ($s -ge 256) { 0 } else { $s }
|
|
$bw.Write([byte]$w) # width
|
|
$bw.Write([byte]$h) # height
|
|
$bw.Write([byte]0) # colorCount
|
|
$bw.Write([byte]0) # reserved
|
|
$bw.Write([UInt16]1) # planes
|
|
$bw.Write([UInt16]32) # bitCount
|
|
$bw.Write([UInt32]$bytes.Length)
|
|
$bw.Write([UInt32]$offset)
|
|
$offset += $bytes.Length
|
|
}
|
|
|
|
# Image data.
|
|
foreach ($s in $sizes) { $bw.Write($pngs["s$s"]) }
|
|
|
|
$bw.Flush()
|
|
[System.IO.File]::WriteAllBytes($icoPath, $ms.ToArray())
|
|
}
|
|
finally {
|
|
$bw.Dispose(); $ms.Dispose()
|
|
}
|
|
|
|
Write-Host "Wrote $icoPath ($((Get-Item $icoPath).Length) bytes)"
|
|
Write-Host "Wrote $pngPath ($((Get-Item $pngPath).Length) bytes)"
|