diff --git a/ee/maintained-apps/inputs/winget/paint-dot-net.json b/ee/maintained-apps/inputs/winget/paint-dot-net.json new file mode 100644 index 0000000000..49acae490a --- /dev/null +++ b/ee/maintained-apps/inputs/winget/paint-dot-net.json @@ -0,0 +1,12 @@ +{ + "name": "Paint.NET", + "slug": "paint-dot-net/windows", + "package_identifier": "dotPDN.PaintDotNet", + "unique_identifier": "Paint.NET", + "install_script_path": "ee/maintained-apps/inputs/winget/scripts/paint_dot_net_install.ps1", + "uninstall_script_path": "ee/maintained-apps/inputs/winget/scripts/paint_dot_net_uninstall.ps1", + "installer_arch": "x64", + "installer_type": "zip", + "installer_scope": "machine", + "default_categories": ["Productivity"] +} diff --git a/ee/maintained-apps/inputs/winget/scripts/paint_dot_net_install.ps1 b/ee/maintained-apps/inputs/winget/scripts/paint_dot_net_install.ps1 new file mode 100644 index 0000000000..755418c915 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/paint_dot_net_install.ps1 @@ -0,0 +1,79 @@ +# Paint.NET ships as a zip containing its installer .exe. + +$zipFilePath = "${env:INSTALLER_PATH}" + +$installTimeoutSeconds = 420 +$registrationTimeoutSeconds = 120 + +$machineKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*' +$machineKey32on64 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' + +# Same DisplayName and Publisher the catalog's exists query uses. +function Get-PaintDotNetEntry { + Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue | + ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } | + Where-Object { $_.DisplayName -eq "Paint.NET" -and $_.Publisher -eq "dotPDN LLC" } | + Select-Object -First 1 +} + +try { + +$extractPath = Join-Path $env:TEMP "PaintDotNetInstall" +if (Test-Path $extractPath) { Remove-Item -Path $extractPath -Recurse -Force } +Expand-Archive -Path $zipFilePath -DestinationPath $extractPath -Force + +$installer = Get-ChildItem -Path $extractPath -Filter "*.exe" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 +if (-not $installer) { + Write-Host "Error: installer .exe not found under $extractPath" + Exit 1 +} + +# /auto is the vendor's silent switch. -Wait would also wait on descendants. +$process = Start-Process -FilePath $installer.FullName -ArgumentList "/auto" -PassThru +$null = $process.Handle # keeps .ExitCode readable after exit + +$killed = $false +if (-not $process.WaitForExit($installTimeoutSeconds * 1000)) { + Write-Host "Installer process did not exit within ${installTimeoutSeconds}s, stopping it." + Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue + $null = $process.WaitForExit(30 * 1000) + $killed = $true +} + +$exitCode = $null +if ($process.HasExited) { + $exitCode = $process.ExitCode + Write-Host "Install exit code: $exitCode" +} + +# The installer can return before the ARP entry is written. +$elapsed = 0 +while (-not (Get-PaintDotNetEntry) -and ($elapsed -lt $registrationTimeoutSeconds)) { + Start-Sleep -Seconds 5 + $elapsed += 5 + Write-Host "Waiting for Paint.NET to register... ($elapsed seconds)" +} + +Remove-Item -Path $extractPath -Recurse -Force -ErrorAction SilentlyContinue + +Stop-Process -Name "paintdotnet" -Force -ErrorAction SilentlyContinue + +$entry = Get-PaintDotNetEntry +if (-not $entry) { + Write-Host "Paint.NET did not register in Add/Remove Programs." + Exit 1 +} +Write-Host "Registered '$($entry.DisplayName)' by '$($entry.Publisher)', version $($entry.DisplayVersion)." + +# Registration is the success signal; a killed process's code means nothing. +if ($killed -or $null -eq $exitCode) { Exit 0 } + +# 3010/1641 = reboot required/initiated. +if ($exitCode -eq 3010 -or $exitCode -eq 1641) { Exit 0 } + +Exit $exitCode + +} catch { + Write-Host "Error: $_" + Exit 1 +} diff --git a/ee/maintained-apps/inputs/winget/scripts/paint_dot_net_uninstall.ps1 b/ee/maintained-apps/inputs/winget/scripts/paint_dot_net_uninstall.ps1 new file mode 100644 index 0000000000..e7c622bad4 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/paint_dot_net_uninstall.ps1 @@ -0,0 +1,32 @@ +# The ProductCode changes every release and differs between the .exe and .msi +# variants; the UpgradeCode is stable across both. +$upgradeCode = '{04A40F40-A207-4B48-AED7-6AA532E43275}' +$timeoutSeconds = 300 +$successCodes = @(0, 3010, 1641) + +try { + $inst = New-Object -ComObject "WindowsInstaller.Installer" + # An empty list means nothing to remove; a failed query means we don't know. + $productCodes = @() + try { + $productCodes = @($inst.RelatedProducts($upgradeCode)) + } catch { + Write-Host "Could not query related products for upgrade code $upgradeCode. Error: $_" + Exit 1 + } + + if ($productCodes.Count -eq 0) { Write-Host "No installed product found for upgrade code $upgradeCode."; Exit 0 } + + foreach ($productCode in $productCodes) { + $process = Start-Process msiexec -ArgumentList @("/quiet", "/x", $productCode, "/norestart") -PassThru + if (-not $process.WaitForExit($timeoutSeconds * 1000)) { + Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue + Write-Host "Uninstall for $productCode timed out." + Exit 1603 + } + Write-Host "Uninstall for $productCode exited $($process.ExitCode)" + if ($successCodes -notcontains $process.ExitCode) { Exit $process.ExitCode } + } +} catch { Write-Host "Error: $_"; Exit 1 } + +Exit 0 diff --git a/ee/maintained-apps/outputs/apps.json b/ee/maintained-apps/outputs/apps.json index 581b2b5139..2e4fddb5c9 100644 --- a/ee/maintained-apps/outputs/apps.json +++ b/ee/maintained-apps/outputs/apps.json @@ -6392,6 +6392,13 @@ "unique_identifier": "com.charlessoft.pacifist", "description": "Pacifist is an extract files and folders from package files, disk images, and archives." }, + { + "name": "Paint.NET", + "slug": "paint-dot-net/windows", + "platform": "windows", + "unique_identifier": "Paint.NET", + "description": "Paint.NET is an image and photo editor with support for layers, effects, and a range of image formats." + }, { "name": "Pale Moon", "slug": "pale-moon/darwin", diff --git a/ee/maintained-apps/outputs/paint-dot-net/windows.json b/ee/maintained-apps/outputs/paint-dot-net/windows.json new file mode 100644 index 0000000000..77f6261110 --- /dev/null +++ b/ee/maintained-apps/outputs/paint-dot-net/windows.json @@ -0,0 +1,22 @@ +{ + "versions": [ + { + "version": "5.1.12", + "queries": { + "exists": "SELECT 1 FROM programs WHERE name = 'Paint.NET' AND publisher = 'dotPDN LLC';", + "patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = 'Paint.NET' AND publisher = 'dotPDN LLC' AND version_compare(version, '5.1.12') < 0);" + }, + "installer_url": "https://github.com/paintdotnet/release/releases/download/v5.1.12/paint.net.5.1.12.install.x64.zip", + "install_script_ref": "1dd07f15", + "uninstall_script_ref": "a23d064d", + "sha256": "3cd861b5af3f85bd28666d4a9017d03de237c08ad0103ee9d92b2cd921f8c867", + "default_categories": [ + "Productivity" + ] + } + ], + "refs": { + "1dd07f15": "# Paint.NET ships as a zip containing its installer .exe.\n\n$zipFilePath = \"${env:INSTALLER_PATH}\"\n\n$installTimeoutSeconds = 420\n$registrationTimeoutSeconds = 120\n\n$machineKey = 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$machineKey32on64 = 'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n\n# Same DisplayName and Publisher the catalog's exists query uses.\nfunction Get-PaintDotNetEntry {\n Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |\n Where-Object { $_.DisplayName -eq \"Paint.NET\" -and $_.Publisher -eq \"dotPDN LLC\" } |\n Select-Object -First 1\n}\n\ntry {\n\n$extractPath = Join-Path $env:TEMP \"PaintDotNetInstall\"\nif (Test-Path $extractPath) { Remove-Item -Path $extractPath -Recurse -Force }\nExpand-Archive -Path $zipFilePath -DestinationPath $extractPath -Force\n\n$installer = Get-ChildItem -Path $extractPath -Filter \"*.exe\" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1\nif (-not $installer) {\n Write-Host \"Error: installer .exe not found under $extractPath\"\n Exit 1\n}\n\n# /auto is the vendor's silent switch. -Wait would also wait on descendants.\n$process = Start-Process -FilePath $installer.FullName -ArgumentList \"/auto\" -PassThru\n$null = $process.Handle # keeps .ExitCode readable after exit\n\n$killed = $false\nif (-not $process.WaitForExit($installTimeoutSeconds * 1000)) {\n Write-Host \"Installer process did not exit within ${installTimeoutSeconds}s, stopping it.\"\n Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue\n $null = $process.WaitForExit(30 * 1000)\n $killed = $true\n}\n\n$exitCode = $null\nif ($process.HasExited) {\n $exitCode = $process.ExitCode\n Write-Host \"Install exit code: $exitCode\"\n}\n\n# The installer can return before the ARP entry is written.\n$elapsed = 0\nwhile (-not (Get-PaintDotNetEntry) -and ($elapsed -lt $registrationTimeoutSeconds)) {\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"Waiting for Paint.NET to register... ($elapsed seconds)\"\n}\n\nRemove-Item -Path $extractPath -Recurse -Force -ErrorAction SilentlyContinue\n\nStop-Process -Name \"paintdotnet\" -Force -ErrorAction SilentlyContinue\n\n$entry = Get-PaintDotNetEntry\nif (-not $entry) {\n Write-Host \"Paint.NET did not register in Add/Remove Programs.\"\n Exit 1\n}\nWrite-Host \"Registered '$($entry.DisplayName)' by '$($entry.Publisher)', version $($entry.DisplayVersion).\"\n\n# Registration is the success signal; a killed process's code means nothing.\nif ($killed -or $null -eq $exitCode) { Exit 0 }\n\n# 3010/1641 = reboot required/initiated.\nif ($exitCode -eq 3010 -or $exitCode -eq 1641) { Exit 0 }\n\nExit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n", + "a23d064d": "# The ProductCode changes every release and differs between the .exe and .msi\n# variants; the UpgradeCode is stable across both.\n$upgradeCode = '{04A40F40-A207-4B48-AED7-6AA532E43275}'\n$timeoutSeconds = 300\n$successCodes = @(0, 3010, 1641)\n\ntry {\n $inst = New-Object -ComObject \"WindowsInstaller.Installer\"\n # An empty list means nothing to remove; a failed query means we don't know.\n $productCodes = @()\n try {\n $productCodes = @($inst.RelatedProducts($upgradeCode))\n } catch {\n Write-Host \"Could not query related products for upgrade code $upgradeCode. Error: $_\"\n Exit 1\n }\n\n if ($productCodes.Count -eq 0) { Write-Host \"No installed product found for upgrade code $upgradeCode.\"; Exit 0 }\n\n foreach ($productCode in $productCodes) {\n $process = Start-Process msiexec -ArgumentList @(\"/quiet\", \"/x\", $productCode, \"/norestart\") -PassThru\n if (-not $process.WaitForExit($timeoutSeconds * 1000)) {\n Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue\n Write-Host \"Uninstall for $productCode timed out.\"\n Exit 1603\n }\n Write-Host \"Uninstall for $productCode exited $($process.ExitCode)\"\n if ($successCodes -notcontains $process.ExitCode) { Exit $process.ExitCode }\n }\n} catch { Write-Host \"Error: $_\"; Exit 1 }\n\nExit 0\n" + } +} diff --git a/frontend/pages/SoftwarePage/components/icons/PaintDotNet.tsx b/frontend/pages/SoftwarePage/components/icons/PaintDotNet.tsx new file mode 100644 index 0000000000..3fb3ea630a --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/PaintDotNet.tsx @@ -0,0 +1,14 @@ +import * as React from "react"; + +import type { SVGProps } from "react"; + +const PaintDotNet = (props: SVGProps) => ( + + + +); +export default PaintDotNet; diff --git a/frontend/pages/SoftwarePage/components/icons/index.ts b/frontend/pages/SoftwarePage/components/icons/index.ts index f6e9311aff..7c4b0c96ec 100644 --- a/frontend/pages/SoftwarePage/components/icons/index.ts +++ b/frontend/pages/SoftwarePage/components/icons/index.ts @@ -752,6 +752,7 @@ import OrigamiStudio from "./OrigamiStudio"; import P4V from "./P4V"; import Pacifist from "./Pacifist"; import Package from "./Package"; +import PaintDotNet from "./PaintDotNet"; import PaleMoon from "./PaleMoon"; import Paletro from "./Paletro"; import ParallelsDesktop from "./ParallelsDesktop"; @@ -1915,6 +1916,7 @@ export const SOFTWARE_NAME_TO_ICON_MAP = { p4v: P4V, pacifist: Pacifist, package: Package, + "paint.net": PaintDotNet, "pale moon": PaleMoon, paletro: Paletro, "parallels desktop": ParallelsDesktop, diff --git a/website/assets/images/app-icon-paint-dot-net-60x60@2x.png b/website/assets/images/app-icon-paint-dot-net-60x60@2x.png new file mode 100644 index 0000000000..a338577ac2 Binary files /dev/null and b/website/assets/images/app-icon-paint-dot-net-60x60@2x.png differ