jh_PwaTools.psm1
|
# PwaTools.psm1 # Chrome-grade AutoIcon-enabled PWA shortcut generator Add-Type -AssemblyName System.Drawing #region Helpers function Get-PWTBrowserInfo { [CmdletBinding()] param([switch]$Edge) if ($Edge) { $exe = "$Env:ProgramFiles(x86)\Microsoft\Edge\Application\msedge.exe" if (-not (Test-Path $exe)) { $exe = "$Env:ProgramFiles\Microsoft\Edge\Application\msedge.exe" } $name = 'Edge' } else { $exe = "$Env:ProgramFiles\Google\Chrome\Application\chrome.exe" if (-not (Test-Path $exe)) { $exe = "$Env:ProgramFiles(x86)\Google\Chrome\Application\chrome.exe" } $name = 'Chrome' } if (-not (Test-Path $exe)) { throw "Browser executable not found for $name. Checked: $exe" } [pscustomobject]@{ Name = $name; Path = $exe } } function New-PWTShortcut { [CmdletBinding()] param( [string]$Name, [string]$TargetPath, [string]$Arguments, [string]$ShortcutPath, [string]$IconPath ) $shell = New-Object -ComObject WScript.Shell $shortcut = $shell.CreateShortcut($ShortcutPath) $shortcut.TargetPath = $TargetPath $shortcut.Arguments = $Arguments $shortcut.WorkingDirectory = Split-Path $TargetPath -Parent if ($IconPath -and (Test-Path $IconPath)) { $shortcut.IconLocation = $IconPath } $shortcut.Save() } function Get-PWTStartMenuFolder { $startMenu = [Environment]::GetFolderPath('Programs') $pwaFolder = Join-Path $startMenu 'PWAs' if (-not (Test-Path $pwaFolder)) { New-Item -ItemType Directory -Path $pwaFolder | Out-Null } return $pwaFolder } function Convert-PWTPngToIco { param( [string]$PngPath, [string]$IcoPath ) try { $bmp = [System.Drawing.Bitmap]::FromFile($PngPath) $iconStream = New-Object System.IO.FileStream($IcoPath, 'Create') $iconWriter = New-Object System.IO.BinaryWriter($iconStream) # ICO header $iconWriter.Write([UInt16]0) $iconWriter.Write([UInt16]1) $iconWriter.Write([UInt16]1) $width = [byte]($bmp.Width) $height = [byte]($bmp.Height) $iconWriter.Write($width) $iconWriter.Write($height) $iconWriter.Write([byte]0) $iconWriter.Write([byte]0) $iconWriter.Write([UInt16]1) $iconWriter.Write([UInt16]32) $pngBytes = [System.IO.File]::ReadAllBytes($PngPath) $iconWriter.Write([UInt32]$pngBytes.Length) $iconWriter.Write([UInt32]22) $iconWriter.Write($pngBytes) $iconWriter.Close() $iconStream.Close() } catch { Write-Warning "Failed to convert PNG to ICO: $($_.Exception.Message)" } } function Convert-PWTSvgToPng { param( [string]$SvgUrl, [string]$PngPath ) try { $svgBytes = Invoke-WebRequest -Uri $SvgUrl -ErrorAction Stop -UseBasicParsing $svgTemp = [System.IO.Path]::GetTempFileName() + ".svg" [System.IO.File]::WriteAllBytes($svgTemp, $svgBytes.Content) # Requires rsvg-convert (Linux) or Inkscape (Windows) # For Windows, use Inkscape CLI if installed $inkscape = "C:\Program Files\Inkscape\bin\inkscape.exe" if (Test-Path $inkscape) { & $inkscape $svgTemp --export-type=png --export-filename=$PngPath | Out-Null return $PngPath } Write-Warning "SVG conversion skipped: no converter available" return $null } catch { Write-Warning "Failed to convert SVG: $($_.Exception.Message)" return $null } } function Get-PWTAutoIcon { [CmdletBinding()] param( [string]$Url, [string]$AppName ) $cacheRoot = Join-Path $Env:LOCALAPPDATA "PwaTools\Icons" if (-not (Test-Path $cacheRoot)) { New-Item -ItemType Directory -Path $cacheRoot | Out-Null } $cachedIco = Join-Path $cacheRoot "$AppName.ico" if (Test-Path $cachedIco) { return $cachedIco } $baseUri = [System.Uri]$Url $candidates = @() # Helper to resolve relative URLs function Resolve-Url($href) { try { return (New-Object System.Uri($baseUri, $href)).AbsoluteUri } catch { return $null } } # Fetch HTML try { $html = Invoke-WebRequest -Uri $Url -UseBasicParsing -ErrorAction Stop } catch { Write-Warning "AutoIcon: Failed to fetch $Url" return $null } # 1. Collect <link rel="icon"> candidates foreach ($link in $html.Links) { if ($link.rel -match 'icon') { $resolved = Resolve-Url $link.href if ($resolved) { $candidates += $resolved } } } # 2. Check for manifest.json $manifestLink = $html.Links | Where-Object { $_.rel -eq 'manifest' } | Select-Object -First 1 if ($manifestLink) { $manifestUrl = Resolve-Url $manifestLink.href try { $manifest = Invoke-RestMethod -Uri $manifestUrl -ErrorAction Stop foreach ($icon in $manifest.icons) { $resolved = Resolve-Url $icon.src if ($resolved) { $candidates += $resolved } } } catch {} } # 3. Add Chrome-style fallback paths $fallbacks = @( "/favicon.ico", "/favicon.png", "/favicon.svg", "/logo.png", "/icon.png", "/static/favicon.ico", "/static/img/favicon.ico" ) foreach ($f in $fallbacks) { $candidates += "$($baseUri.Scheme)://$($baseUri.Host)$f" } # Deduplicate $candidates = $candidates | Select-Object -Unique # 4. Download all candidates and pick the largest $best = $null $bestSize = 0 foreach ($iconUrl in $candidates) { try { $ext = [System.IO.Path]::GetExtension($iconUrl).ToLower() $tmp = Join-Path $cacheRoot "$AppName-temp$ext" Invoke-WebRequest -Uri $iconUrl -OutFile $tmp -ErrorAction Stop if ($ext -eq ".svg") { $pngTmp = Join-Path $cacheRoot "$AppName-temp.png" $converted = Convert-PWTSvgToPng -SvgUrl $iconUrl -PngPath $pngTmp if ($converted) { $tmp = $pngTmp } } $img = [System.Drawing.Bitmap]::FromFile($tmp) $size = $img.Width * $img.Height if ($size -gt $bestSize) { $best = $tmp $bestSize = $size } } catch {} } if ($best) { Convert-PWTPngToIco -PngPath $best -IcoPath $cachedIco return $cachedIco } return $null } #endregion Helpers function New-PWAShortcut { [CmdletBinding()] param( [string]$Name, [string]$Url, [string]$Profile, [switch]$Desktop, [string]$Icon, [switch]$Edge, [switch]$NoAutoIcon ) $browser = Get-PWTBrowserInfo -Edge:$Edge $args = @("--app=`"$Url`"") if ($Profile) { $args += "--profile-directory=`"$Profile`"" } $startMenuFolder = Get-PWTStartMenuFolder $shortcutPath = Join-Path $startMenuFolder "$Name.lnk" $iconPath = $Icon if (-not $iconPath -and -not $NoAutoIcon) { $auto = Get-PWTAutoIcon -Url $Url -AppName $Name if ($auto) { $iconPath = $auto } } New-PWTShortcut -Name $Name ` -TargetPath $browser.Path ` -Arguments ($args -join ' ') ` -ShortcutPath $shortcutPath ` -IconPath $iconPath if ($Desktop) { $desktop = [Environment]::GetFolderPath('Desktop') Copy-Item $shortcutPath (Join-Path $desktop "$Name.lnk") -Force } [pscustomobject]@{ Name = $Name Url = $Url Browser = $browser.Name Profile = $Profile Desktop = [bool]$Desktop Icon = $iconPath ShortcutPath = $shortcutPath } } function Import-PWAShortcutsFromCsv { [CmdletBinding()] param( [string]$Path, [switch]$Edge, [switch]$NoAutoIcon ) $rows = Import-Csv -Path $Path $results = @() foreach ($row in $rows) { $useEdge = $Edge -or ($row.Browser -eq 'Edge') $result = New-PWAShortcut -Name $row.Name ` -Url $row.Url ` -Profile $row.Profile ` -Desktop:([bool]($row.Desktop -match '^(true|1|yes)$')) ` -Icon $row.IconOverride ` -Edge:$useEdge ` -NoAutoIcon:$NoAutoIcon $results += $result } return $results } function New-PWACsvTemplate { param([string]$Path) @( [pscustomobject]@{ Name='netbox'; Url='https://netbox.example/'; Browser='Chrome'; Profile='Default'; Desktop='true'; IconOverride='' } ) | Export-Csv -Path $Path -NoTypeInformation } function New-PWAFromTraefik { [CmdletBinding()] param( [string]$TraefikApiBaseUrl, [switch]$Desktop, [string]$Profile, [string]$IconDirectory, [switch]$Edge, [switch]$NoAutoIcon ) $base = $TraefikApiBaseUrl.TrimEnd('/') $routersUrl = "$base/routers" $routers = Invoke-RestMethod -Uri $routersUrl -SkipCertificateCheck $results = @() foreach ($router in $routers) { if ($router.provider -ne 'docker') { continue } if ($router.rule -notmatch 'Host\(`(?<host>[^`]+)`\)') { continue } $hostname = $Matches['host'] $name = ($hostname -split '\.')[0] $url = "https://$hostname/" $iconPath = $null if ($IconDirectory -and (Test-Path $IconDirectory)) { $candidate = Join-Path $IconDirectory "$name.ico" if (Test-Path $candidate) { $iconPath = $candidate } } $result = New-PWAShortcut -Name $name ` -Url $url ` -Profile $Profile ` -Desktop:$Desktop ` -Icon $iconPath ` -Edge:$Edge ` -NoAutoIcon:$NoAutoIcon $results += $result } return $results } Export-ModuleMember -Function ` New-PWAShortcut, ` Import-PWAShortcutsFromCsv, ` New-PWACsvTemplate, ` New-PWAFromTraefik |