Public Access
Merge branch 'claude/connection-host-default-text-9e501d'
This commit is contained in:
@@ -0,0 +1,124 @@
|
|||||||
|
# Regenerates dodossh.ico from the same geometry the Android launcher icon draws.
|
||||||
|
#
|
||||||
|
# The phone's mark is a vector — Resources/drawable/ic_launcher_foreground.xml — and the whole
|
||||||
|
# reason it is a vector is that there is then one geometry to change and no set of PNG densities
|
||||||
|
# to forget one of. Windows will not take a vector: <ApplicationIcon> wants an .ico and nothing
|
||||||
|
# else, and Window.Icon wants a bitmap. So the raster exists, and this script is how it stays
|
||||||
|
# honest: the numbers below are the ones in that XML, and regenerating is the whole edit.
|
||||||
|
#
|
||||||
|
# pwsh -File src/DodoSSH.Client.App/Assets/dodossh-icon.ps1
|
||||||
|
#
|
||||||
|
# Coordinates are the launcher's 108-unit viewport, mapped so the middle 72 fills the canvas.
|
||||||
|
# That 72 is not an arbitrary crop: it is the part of an adaptive icon a launcher actually shows,
|
||||||
|
# the outer 18 on each edge being what it eats for masking and parallax. Rendering the whole 108
|
||||||
|
# here would draw the mark at 39% of the width — correct arithmetic, and a stamp lost in a field
|
||||||
|
# of background. Matching what the phone displays means matching the 72.
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
Add-Type -AssemblyName System.Drawing
|
||||||
|
|
||||||
|
$window = [System.Drawing.ColorTranslator]::FromHtml('#0E1220') # dodo_window / CanvasColor
|
||||||
|
$accent = [System.Drawing.ColorTranslator]::FromHtml('#5B8CFF') # AccentColor
|
||||||
|
|
||||||
|
# Every size Windows asks for: 16 in a titlebar and a tree, 32 on the desktop, 48 in a large-icon
|
||||||
|
# view, 256 for the preview pane. Shipping fewer means Windows downsamples one of these to fill
|
||||||
|
# the gap, and its downsampler is not kind to a hairline.
|
||||||
|
$sizes = @(16, 20, 24, 32, 40, 48, 64, 128, 256)
|
||||||
|
|
||||||
|
function New-MarkPng([int]$size)
|
||||||
|
{
|
||||||
|
$bitmap = New-Object System.Drawing.Bitmap($size, $size, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
||||||
|
$g = [System.Drawing.Graphics]::FromImage($bitmap)
|
||||||
|
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
|
||||||
|
$g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
|
||||||
|
|
||||||
|
# The background, rounded as a launcher mask rounds it. A square-cornered tile would be the
|
||||||
|
# one icon on the taskbar with corners, which reads as unfinished rather than as deliberate.
|
||||||
|
$radius = [double]$size * 0.22
|
||||||
|
$d = $radius * 2.0
|
||||||
|
$path = New-Object System.Drawing.Drawing2D.GraphicsPath
|
||||||
|
$path.AddArc(0.0, 0.0, $d, $d, 180, 90)
|
||||||
|
$path.AddArc($size - $d, 0.0, $d, $d, 270, 90)
|
||||||
|
$path.AddArc($size - $d, $size - $d, $d, $d, 0, 90)
|
||||||
|
$path.AddArc(0.0, $size - $d, $d, $d, 90, 90)
|
||||||
|
$path.CloseFigure()
|
||||||
|
$brush = New-Object System.Drawing.SolidBrush($window)
|
||||||
|
$g.FillPath($brush, $path)
|
||||||
|
|
||||||
|
# 108-viewport units to pixels, with the outer 18 dropped on each edge.
|
||||||
|
$scale = [double]$size / 72.0
|
||||||
|
function P([double]$x, [double]$y) { New-Object System.Drawing.PointF((($x - 18.0) * $scale), (($y - 18.0) * $scale)) }
|
||||||
|
|
||||||
|
# A stroke thinner than a pixel renders as a grey suggestion of itself, which at 16px is the
|
||||||
|
# difference between a mark and a smudge. The phone's file already bumps these widths for the
|
||||||
|
# same reason at 48dp; the floor is that argument carried down to the sizes Windows asks for.
|
||||||
|
function Pen([double]$units)
|
||||||
|
{
|
||||||
|
$pen = New-Object System.Drawing.Pen($accent, [float][Math]::Max($units * $scale, 1.0))
|
||||||
|
$pen.StartCap = [System.Drawing.Drawing2D.LineCap]::Round
|
||||||
|
$pen.EndCap = [System.Drawing.Drawing2D.LineCap]::Round
|
||||||
|
$pen.LineJoin = [System.Drawing.Drawing2D.LineJoin]::Round
|
||||||
|
return $pen
|
||||||
|
}
|
||||||
|
|
||||||
|
# The box: 42 across, centred, square-cornered as the Border in the app is.
|
||||||
|
$box = Pen 2.2
|
||||||
|
$g.DrawPolygon($box, @((P 33 33), (P 75 33), (P 75 75), (P 33 75)))
|
||||||
|
|
||||||
|
# The chevron of >_ and the underscore on the baseline it bottoms out at.
|
||||||
|
$ink = Pen 2.8
|
||||||
|
$g.DrawLines($ink, @((P 44.8 48.75), (P 51.9 54), (P 44.8 59.25)))
|
||||||
|
$g.DrawLine($ink, (P 54 59.7), (P 63.2 59.7))
|
||||||
|
|
||||||
|
$stream = New-Object System.IO.MemoryStream
|
||||||
|
$bitmap.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||||
|
|
||||||
|
$box.Dispose(); $ink.Dispose(); $brush.Dispose(); $path.Dispose(); $g.Dispose(); $bitmap.Dispose()
|
||||||
|
return $stream.ToArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
# ICO is a six-byte header, a sixteen-byte directory entry per image, then the images. The entries
|
||||||
|
# carry PNG payloads rather than the older BMP-with-AND-mask form, which every Windows since Vista
|
||||||
|
# reads and which is what keeps a 256px entry from costing 256KB.
|
||||||
|
#
|
||||||
|
# The cast on each frame is load-bearing. A byte[] returned through PowerShell's output collector
|
||||||
|
# comes back as Object[] of boxed bytes, which BinaryWriter has no overload for — it binds to one
|
||||||
|
# of the scalar Write()s instead and puts a single byte on the stream. The first run of this
|
||||||
|
# script produced a 159-byte .ico that way, header and directory intact and nine one-byte images.
|
||||||
|
$frames = New-Object 'System.Collections.Generic.List[byte[]]'
|
||||||
|
foreach ($size in $sizes)
|
||||||
|
{
|
||||||
|
[byte[]]$png = New-MarkPng $size
|
||||||
|
$frames.Add($png)
|
||||||
|
}
|
||||||
|
|
||||||
|
$out = New-Object System.IO.MemoryStream
|
||||||
|
$w = New-Object System.IO.BinaryWriter($out)
|
||||||
|
$w.Write([uint16]0) # reserved
|
||||||
|
$w.Write([uint16]1) # type: icon
|
||||||
|
$w.Write([uint16]$sizes.Count)
|
||||||
|
|
||||||
|
$offset = 6 + (16 * $sizes.Count)
|
||||||
|
for ($i = 0; $i -lt $sizes.Count; $i++)
|
||||||
|
{
|
||||||
|
$size = $sizes[$i]
|
||||||
|
$w.Write([byte]($(if ($size -ge 256) { 0 } else { $size }))) # 0 means 256
|
||||||
|
$w.Write([byte]($(if ($size -ge 256) { 0 } else { $size })))
|
||||||
|
$w.Write([byte]0) # palette entries: none, this is truecolour
|
||||||
|
$w.Write([byte]0) # reserved
|
||||||
|
$w.Write([uint16]1) # colour planes
|
||||||
|
$w.Write([uint16]32) # bits per pixel
|
||||||
|
$w.Write([uint32]$frames[$i].Length)
|
||||||
|
$w.Write([uint32]$offset)
|
||||||
|
$offset += $frames[$i].Length
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($frame in $frames) { $w.Write($frame) }
|
||||||
|
$w.Flush()
|
||||||
|
|
||||||
|
$target = Join-Path $PSScriptRoot 'dodossh.ico'
|
||||||
|
[System.IO.File]::WriteAllBytes($target, $out.ToArray())
|
||||||
|
$w.Dispose(); $out.Dispose()
|
||||||
|
|
||||||
|
Write-Output "Wrote $target ($($sizes.Count) sizes, $((Get-Item $target).Length) bytes)"
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
@@ -5,6 +5,14 @@
|
|||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The icon on the executable itself — what Explorer, the Start menu and a pinned taskbar button
|
||||||
|
draw, all of which read it from the PE resource and never start the process. Window.Icon in
|
||||||
|
MainWindow.axaml is a separate thing that only exists once the application is running; both are
|
||||||
|
needed, and both point at this file.
|
||||||
|
-->
|
||||||
|
<ApplicationIcon>Assets/dodossh.ico</ApplicationIcon>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
False here, unlike every server project. The root Directory.Build.props sets it true because
|
False here, unlike every server project. The root Directory.Build.props sets it true because
|
||||||
the API is container-hosted, UTC-only and has no business formatting anything for a human.
|
the API is container-hosted, UTC-only and has no business formatting anything for a human.
|
||||||
@@ -15,6 +23,14 @@
|
|||||||
<InvariantGlobalization>false</InvariantGlobalization>
|
<InvariantGlobalization>false</InvariantGlobalization>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!--
|
||||||
|
Named rather than globbed as Assets/**, because the folder also holds the script that draws the
|
||||||
|
icon and a build has no reason to carry a copy of it around inside the binary.
|
||||||
|
-->
|
||||||
|
<AvaloniaResource Include="Assets/dodossh.ico" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Avalonia" />
|
<PackageReference Include="Avalonia" />
|
||||||
<PackageReference Include="Avalonia.Desktop" />
|
<PackageReference Include="Avalonia.Desktop" />
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
x:Class="DodoSSH.Client.App.Views.MainWindow"
|
x:Class="DodoSSH.Client.App.Views.MainWindow"
|
||||||
x:DataType="vm:MainWindowViewModel"
|
x:DataType="vm:MainWindowViewModel"
|
||||||
Title="DodoSSH"
|
Title="DodoSSH"
|
||||||
|
Icon="/Assets/dodossh.ico"
|
||||||
Width="1180"
|
Width="1180"
|
||||||
Height="760"
|
Height="760"
|
||||||
MinWidth="1016"
|
MinWidth="1016"
|
||||||
|
|||||||
Reference in New Issue
Block a user