diff --git a/resources/webhook-server.ico b/resources/webhook-server.ico new file mode 100644 index 0000000..dd4e491 Binary files /dev/null and b/resources/webhook-server.ico differ diff --git a/resources/webhook-server.png b/resources/webhook-server.png new file mode 100644 index 0000000..be9ef7d Binary files /dev/null and b/resources/webhook-server.png differ diff --git a/scripts/generate-icons.ps1 b/scripts/generate-icons.ps1 new file mode 100644 index 0000000..d68f977 --- /dev/null +++ b/scripts/generate-icons.ps1 @@ -0,0 +1,138 @@ +<# +.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)" diff --git a/src/WebhookServer.Gui/MainWindow.xaml b/src/WebhookServer.Gui/MainWindow.xaml index 17ea76f..6dcf6eb 100644 --- a/src/WebhookServer.Gui/MainWindow.xaml +++ b/src/WebhookServer.Gui/MainWindow.xaml @@ -7,6 +7,7 @@ xmlns:models="clr-namespace:WebhookServer.Core.Models;assembly=WebhookServer.Core" mc:Ignorable="d" Title="Webhook Server" Height="600" Width="1000" + Icon="/webhook-server.ico" d:DataContext="{d:DesignInstance Type=vm:MainViewModel}"> diff --git a/src/WebhookServer.Gui/Views/AboutDialog.xaml b/src/WebhookServer.Gui/Views/AboutDialog.xaml index 8612b27..455f611 100644 --- a/src/WebhookServer.Gui/Views/AboutDialog.xaml +++ b/src/WebhookServer.Gui/Views/AboutDialog.xaml @@ -2,9 +2,10 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="About Webhook Server" - Height="320" Width="420" + Height="360" Width="440" ResizeMode="NoResize" WindowStartupLocation="CenterOwner" + Icon="/webhook-server.ico" ShowInTaskbar="False"> diff --git a/src/WebhookServer.Gui/WebhookServer.Gui.csproj b/src/WebhookServer.Gui/WebhookServer.Gui.csproj index 4ffed94..428feba 100644 --- a/src/WebhookServer.Gui/WebhookServer.Gui.csproj +++ b/src/WebhookServer.Gui/WebhookServer.Gui.csproj @@ -14,6 +14,13 @@ enable enable true + ..\..\resources\webhook-server.ico + Webhook Server + + + + +