Files
windows-builder/includes/$OEM$/$$/OEM/scripts/windowsupdate/UpdateNotifications.psm1
2026-06-02 03:37:09 -07:00

927 lines
35 KiB
PowerShell

<#
.SYNOPSIS
Windows Update Notification System
.DESCRIPTION
Handles Windows 10/11 Action Center notifications and popup dialogs
showing detailed update information
.NOTES
Author: oxmc
Version: 2.1
#>
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName PresentationFramework
# Load Windows Runtime for Toast Notifications
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.UI.Notifications.NotificationData, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
$script:APP_ID = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe'
$script:ProgressTag = "wum-progress"
$script:ProgressSeq = [uint32]1
#region Windows Toast Notifications
function Show-UpdateAvailableToast {
param(
[int]$UpdateCount,
[array]$Updates
)
$updateList = ""
$maxDisplay = [Math]::Min(3, $Updates.Count)
for ($i = 0; $i -lt $maxDisplay; $i++) {
$updateList += "* $($Updates[$i].Title)`n"
}
if ($Updates.Count -gt 3) {
$updateList += "* And $($Updates.Count - 3) more..."
}
$template = @"
<toast launch="action=viewUpdates" scenario="reminder">
<visual>
<binding template="ToastGeneric">
<text>Windows Updates Available</text>
<text>$UpdateCount update(s) ready to install</text>
<text>$updateList</text>
</binding>
</visual>
<actions>
<action content="View Details" arguments="action=viewDetails" activationType="foreground"/>
<action content="Install Now" arguments="action=installNow" activationType="foreground"/>
<action content="Remind Me Later" arguments="action=dismiss" activationType="background"/>
</actions>
<audio src="ms-winsoundevent:Notification.Default"/>
</toast>
"@
try {
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml($template)
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($script:APP_ID).Show($toast)
return $true
}
catch {
Write-Host "Toast notification failed: $($_.Exception.Message)" -ForegroundColor Yellow
return $false
}
}
function Show-UpdateInstallingToast {
param([int]$UpdateCount)
$template = @"
<toast scenario="reminder">
<visual>
<binding template="ToastGeneric">
<text>Installing Windows Updates</text>
<text>Installing $UpdateCount update(s)...</text>
<text>This may take several minutes. Please do not turn off your computer.</text>
<progress value="indeterminate" status="Installing..." />
</binding>
</visual>
<audio src="ms-winsoundevent:Notification.Default"/>
</toast>
"@
try {
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml($template)
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($script:APP_ID).Show($toast)
}
catch {
Write-Host "Toast notification failed: $($_.Exception.Message)" -ForegroundColor Yellow
}
}
function Show-UpdateCompleteToast {
param(
[int]$SuccessCount,
[bool]$RebootRequired,
[string]$AppId = $script:APP_ID
)
$rebootText = if ($RebootRequired) {
"A restart is required to complete installation."
} else {
"No restart required."
}
$template = @"
<toast launch="action=viewResults" scenario="reminder">
<visual>
<binding template="ToastGeneric">
<text>Updates Installed Successfully</text>
<text>$SuccessCount update(s) installed</text>
<text>$rebootText</text>
</binding>
</visual>
<actions>
<action content="View Details" arguments="action=viewResults" activationType="foreground"/>
<action content="Restart Now" arguments="action=restartNow" activationType="foreground"/>
<action content="Dismiss" arguments="action=dismiss" activationType="background"/>
</actions>
<audio src="ms-winsoundevent:Notification.Default"/>
</toast>
"@
if (-not $RebootRequired) {
$template = @"
<toast launch="action=viewResults">
<visual>
<binding template="ToastGeneric">
<text>Updates Installed Successfully</text>
<text>$SuccessCount update(s) installed</text>
<text>$rebootText</text>
</binding>
</visual>
<actions>
<action content="View Details" arguments="action=viewResults" activationType="foreground"/>
<action content="Dismiss" arguments="action=dismiss" activationType="background"/>
</actions>
<audio src="ms-winsoundevent:Notification.Default"/>
</toast>
"@
}
try {
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml($template)
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($AppId).Show($toast)
}
catch {
Write-Host "Toast notification failed: $($_.Exception.Message)" -ForegroundColor Yellow
}
}
function Show-UpdateFailedToast {
param([string]$ErrorMessage, [string]$AppId = $script:APP_ID)
$template = @"
<toast>
<visual>
<binding template="ToastGeneric">
<text>Update Installation Failed</text>
<text>Some updates could not be installed</text>
<text>$ErrorMessage</text>
</binding>
</visual>
<actions>
<action content="View Logs" arguments="action=viewLogs" activationType="foreground"/>
<action content="Retry" arguments="action=retry" activationType="foreground"/>
<action content="Dismiss" arguments="action=dismiss" activationType="background"/>
</actions>
<audio src="ms-winsoundevent:Notification.Looping.Alarm"/>
</toast>
"@
try {
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml($template)
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($AppId).Show($toast)
}
catch {
Write-Host "Toast notification failed: $($_.Exception.Message)" -ForegroundColor Yellow
}
}
function Show-RebootRequiredToast {
param([int]$MinutesUntilReboot = 60)
$template = @"
<toast scenario="urgent">
<visual>
<binding template="ToastGeneric">
<text>Restart Required</text>
<text>Your PC will restart in $MinutesUntilReboot minutes</text>
<text>Save your work and close all applications</text>
</binding>
</visual>
<actions>
<action content="Restart Now" arguments="action=restartNow" activationType="foreground"/>
<action content="Schedule Restart" arguments="action=scheduleRestart" activationType="foreground"/>
<action content="Postpone" arguments="action=postpone" activationType="background"/>
</actions>
<audio src="ms-winsoundevent:Notification.Looping.Alarm" loop="true"/>
</toast>
"@
try {
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml($template)
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($script:APP_ID).Show($toast)
}
catch {
Write-Host "Toast notification failed: $($_.Exception.Message)" -ForegroundColor Yellow
}
}
#endregion
#region Balloon Tip Notifications (Fallback)
function Show-BalloonNotification {
param(
[string]$Title,
[string]$Message,
[ValidateSet("None", "Info", "Warning", "Error")]
[string]$Icon = "Info",
[int]$Duration = 10000
)
# Balloon tips are silently dropped on Windows 11 (build 22000+); toast already handles it
$build = [int](Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion").CurrentBuildNumber
if ($build -ge 22000) { return }
$balloon = New-Object System.Windows.Forms.NotifyIcon
switch ($Icon) {
"Info" { $balloon.Icon = [System.Drawing.SystemIcons]::Information }
"Warning" { $balloon.Icon = [System.Drawing.SystemIcons]::Warning }
"Error" { $balloon.Icon = [System.Drawing.SystemIcons]::Error }
default { $balloon.Icon = [System.Drawing.SystemIcons]::Application }
}
$balloon.BalloonTipIcon = $Icon
$balloon.BalloonTipText = $Message
$balloon.BalloonTipTitle = $Title
$balloon.Visible = $true
$balloon.ShowBalloonTip($Duration)
$cleanupTimer = [System.Windows.Forms.Timer]::new()
$cleanupTimer.Interval = $Duration + 500
$cleanupTimer.Add_Tick({
$balloon.Dispose()
$cleanupTimer.Stop()
$cleanupTimer.Dispose()
})
$cleanupTimer.Start()
}
#endregion
#region Updates Available Notification
function Show-UpdatesAvailableNotification {
param([int]$UpdateCount)
$form = New-Object System.Windows.Forms.Form
$form.Text = "Windows Update"
$form.Size = New-Object System.Drawing.Size(360, 140)
$form.StartPosition = "Manual"
$form.FormBorderStyle = "FixedDialog"
$form.MaximizeBox = $false
$form.MinimizeBox = $false
$form.TopMost = $true
# Position bottom-right above taskbar
$screen = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea
$form.Location = New-Object System.Drawing.Point(
($screen.Right - $form.Width - 12),
($screen.Bottom - $form.Height - 12))
$icon = New-Object System.Windows.Forms.PictureBox
$icon.Image = [System.Drawing.SystemIcons]::Information.ToBitmap()
$icon.SizeMode = "StretchImage"
$icon.Size = New-Object System.Drawing.Size(32, 32)
$icon.Location = New-Object System.Drawing.Point(14, 20)
$form.Controls.Add($icon)
$label = New-Object System.Windows.Forms.Label
$label.Text = "$UpdateCount update(s) available"
$label.Font = New-Object System.Drawing.Font("Segoe UI", 10, [System.Drawing.FontStyle]::Bold)
$label.Location = New-Object System.Drawing.Point(56, 14)
$label.Size = New-Object System.Drawing.Size(280, 22)
$form.Controls.Add($label)
$sub = New-Object System.Windows.Forms.Label
$sub.Text = "Review and choose which updates to install."
$sub.Font = New-Object System.Drawing.Font("Segoe UI", 9)
$sub.Location = New-Object System.Drawing.Point(56, 36)
$sub.Size = New-Object System.Drawing.Size(280, 18)
$form.Controls.Add($sub)
$viewBtn = New-Object System.Windows.Forms.Button
$viewBtn.Text = "View Details"
$viewBtn.Size = New-Object System.Drawing.Size(100, 28)
$viewBtn.Location = New-Object System.Drawing.Point(140, 70)
$viewBtn.DialogResult = [System.Windows.Forms.DialogResult]::OK
$viewBtn.FlatStyle = "Flat"
$viewBtn.BackColor = [System.Drawing.Color]::FromArgb(0, 120, 215)
$viewBtn.ForeColor = [System.Drawing.Color]::White
$form.Controls.Add($viewBtn)
$form.AcceptButton = $viewBtn
$laterBtn = New-Object System.Windows.Forms.Button
$laterBtn.Text = "Later"
$laterBtn.Size = New-Object System.Drawing.Size(80, 28)
$laterBtn.Location = New-Object System.Drawing.Point(250, 70)
$laterBtn.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$laterBtn.FlatStyle = "Flat"
$form.Controls.Add($laterBtn)
$form.CancelButton = $laterBtn
return $form.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK
}
#endregion
#region Loading Splash
function Show-LoadingSplash {
param([string]$Message = "Checking for updates...")
$splash = New-Object System.Windows.Forms.Form
$splash.Text = "Windows Update Manager"
$splash.Size = New-Object System.Drawing.Size(380, 110)
$splash.StartPosition = "CenterScreen"
$splash.FormBorderStyle = "FixedDialog"
$splash.MaximizeBox = $false
$splash.MinimizeBox = $false
$splash.ControlBox = $false
$splash.TopMost = $true
$label = New-Object System.Windows.Forms.Label
$label.Text = $Message
$label.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$label.Location = New-Object System.Drawing.Point(20, 15)
$label.Size = New-Object System.Drawing.Size(340, 22)
$splash.Controls.Add($label)
$bar = New-Object System.Windows.Forms.ProgressBar
$bar.Style = "Marquee"
$bar.Location = New-Object System.Drawing.Point(20, 45)
$bar.Size = New-Object System.Drawing.Size(340, 20)
$splash.Controls.Add($bar)
$splash.Show()
[System.Windows.Forms.Application]::DoEvents()
return $splash
}
function Close-LoadingSplash {
param([System.Windows.Forms.Form]$Splash)
if ($Splash -and -not $Splash.IsDisposed) {
$Splash.Close()
$Splash.Dispose()
}
}
#endregion
#region Update Details Popup Window
function Show-UpdateDetailsWindow {
param([array]$Updates)
$form = New-Object System.Windows.Forms.Form
$form.Text = "Windows Updates Available - Details"
$form.Size = New-Object System.Drawing.Size(900, 750)
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedDialog"
$form.MaximizeBox = $false
$form.MinimizeBox = $true
$form.Icon = [System.Drawing.SystemIcons]::Information
$form.TopMost = $true
# Header Panel
$headerPanel = New-Object System.Windows.Forms.Panel
$headerPanel.Dock = "Top"
$headerPanel.Height = 80
$headerPanel.BackColor = [System.Drawing.Color]::FromArgb(0, 120, 215)
$form.Controls.Add($headerPanel)
# Icon
$iconBox = New-Object System.Windows.Forms.PictureBox
$iconBox.Location = New-Object System.Drawing.Point(20, 15)
$iconBox.Size = New-Object System.Drawing.Size(48, 48)
$iconBox.Image = [System.Drawing.SystemIcons]::Information.ToBitmap()
$iconBox.SizeMode = "StretchImage"
$headerPanel.Controls.Add($iconBox)
# Title
$titleLabel = New-Object System.Windows.Forms.Label
$titleLabel.Location = New-Object System.Drawing.Point(80, 15)
$titleLabel.Size = New-Object System.Drawing.Size(800, 30)
$titleLabel.Font = New-Object System.Drawing.Font("Segoe UI", 16, [System.Drawing.FontStyle]::Bold)
$titleLabel.ForeColor = [System.Drawing.Color]::White
$titleLabel.Text = "$($Updates.Count) Windows Update(s) Available"
$headerPanel.Controls.Add($titleLabel)
# Subtitle
$subtitleLabel = New-Object System.Windows.Forms.Label
$subtitleLabel.Location = New-Object System.Drawing.Point(80, 45)
$subtitleLabel.Size = New-Object System.Drawing.Size(800, 25)
$subtitleLabel.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$subtitleLabel.ForeColor = [System.Drawing.Color]::White
$subtitleLabel.Text = "Review and install the following updates"
$headerPanel.Controls.Add($subtitleLabel)
# Info Panel
$infoPanel = New-Object System.Windows.Forms.Panel
$infoPanel.Location = New-Object System.Drawing.Point(20, 100)
$infoPanel.Size = New-Object System.Drawing.Size(840, 50)
$infoPanel.BackColor = [System.Drawing.Color]::FromArgb(245, 245, 245)
$infoPanel.BorderStyle = "FixedSingle"
$form.Controls.Add($infoPanel)
$infoLabel = New-Object System.Windows.Forms.Label
$infoLabel.Location = New-Object System.Drawing.Point(10, 12)
$infoLabel.Size = New-Object System.Drawing.Size(820, 25)
$infoLabel.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$infoLabel.Text = "Total Size: Fetching... | Available: $($Updates.Count) update(s)"
$infoPanel.Controls.Add($infoLabel)
# ListView for updates
$listView = New-Object System.Windows.Forms.ListView
$listView.Location = New-Object System.Drawing.Point(20, 160)
$listView.Size = New-Object System.Drawing.Size(840, 320)
$listView.View = "Details"
$listView.FullRowSelect = $true
$listView.GridLines = $true
$listView.CheckBoxes = $true
$listView.MultiSelect = $false
$form.Controls.Add($listView)
# Add columns
$listView.Columns.Add("", 30) | Out-Null
$listView.Columns.Add("Update Name", 450) | Out-Null
$listView.Columns.Add("Type", 100) | Out-Null
$listView.Columns.Add("Size", 80) | Out-Null
$listView.Columns.Add("KB Article", 100) | Out-Null
$listView.Columns.Add("Severity", 80) | Out-Null
# Add updates to list — size column starts as "Fetching..." and is filled asynchronously
$urlsPerUpdate = @{}
for ($ui = 0; $ui -lt $Updates.Count; $ui++) {
$update = $Updates[$ui]
$item = New-Object System.Windows.Forms.ListViewItem("")
$item.Checked = $true
$item.SubItems.Add($update.Title) | Out-Null
$type = switch ($update.Type) {
1 { "Software" }
2 { "Driver" }
default { "Other" }
}
$item.SubItems.Add($type) | Out-Null
$item.SubItems.Add("Fetching...") | Out-Null
$kbNumber = "N/A"
if ($update.Title -match '(KB\d+)') { $kbNumber = $Matches[1] }
$item.SubItems.Add($kbNumber) | Out-Null
$severity = "Standard"
foreach ($category in $update.Categories) {
if ($category.Name -match "Critical") {
$severity = "Critical"
$item.ForeColor = [System.Drawing.Color]::Red
break
} elseif ($category.Name -match "Important|Security") {
$severity = "Important"
$item.ForeColor = [System.Drawing.Color]::OrangeRed
}
}
$item.SubItems.Add($severity) | Out-Null
$item.Tag = $update
$listView.Items.Add($item) | Out-Null
# Collect per-update metadata from COM now (COM objects are main-thread only)
$isDelta = ($update.DownloadContents | ForEach-Object { $_.IsDeltaCompressedContent }) -contains $true
$urls = @($update.DownloadContents |
ForEach-Object { $_.DownloadUrl } |
Where-Object { $_ -and $_ -match '^https?://' })
$urlsPerUpdate[$ui] = @{
IsDelta = $isDelta
MaxSize = [long]$update.MaxDownloadSize
Urls = $urls
}
}
# Background runspace: HEAD-request every URL to get real Content-Length
$sizeResults = [System.Collections.Concurrent.ConcurrentDictionary[int,long]]::new()
$fetchRS = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
$fetchRS.Open()
$fetchRS.SessionStateProxy.SetVariable('urlsPerUpdate', $urlsPerUpdate)
$fetchRS.SessionStateProxy.SetVariable('sizeResults', $sizeResults)
$fetchRS.SessionStateProxy.SetVariable('updateCount', $Updates.Count)
$fetchPS = [System.Management.Automation.PowerShell]::Create()
$fetchPS.Runspace = $fetchRS
$fetchPS.AddScript({
for ($i = 0; $i -lt $updateCount; $i++) {
$info = $urlsPerUpdate[$i]
if (-not $info.IsDelta -and $info.MaxSize -gt 0) {
# Non-delta: MaxDownloadSize is the exact file size
$sizeResults.TryAdd($i, $info.MaxSize) | Out-Null
continue
}
# Delta-compressed: HEAD the actual CDN URL for real size
$total = 0L
foreach ($url in $info.Urls) {
try {
$req = [System.Net.HttpWebRequest]::Create([uri]$url)
$req.Method = "HEAD"
$req.Timeout = 8000
$req.AllowAutoRedirect = $true
$resp = $req.GetResponse()
if ($resp.ContentLength -gt 0) { $total += $resp.ContentLength }
$resp.Close()
} catch {}
}
$sizeResults.TryAdd($i, $total) | Out-Null
}
}) | Out-Null
$fetchPS.BeginInvoke() | Out-Null
# WinForms Timer: polls results on UI thread and updates rows live
$fetchDone = [System.Collections.Generic.HashSet[int]]::new()
$fetchTimer = [System.Windows.Forms.Timer]::new()
$fetchTimer.Interval = 300
$fetchTimer.Add_Tick({
$fetched = 0L
for ($ti = 0; $ti -lt $Updates.Count; $ti++) {
if ($fetchDone.Contains($ti)) { $v = 0L; $sizeResults.TryGetValue($ti, [ref]$v); $fetched += $v; continue }
$val = 0L
if (-not $sizeResults.TryGetValue($ti, [ref]$val)) { continue }
# Format real size or fall back to WU API estimate
$u = $Updates[$ti]
$isDefinition = $u.Categories | Where-Object { $_.Name -match "Definition" }
if ($isDefinition) {
# Delta-delivery means CDN size is full package, not what downloads — always Unknown
$szStr = "Unknown"
$val = 0
} elseif ($val -gt 0) {
$szStr = if ($val -ge 1GB) { "$([math]::Round($val/1GB,2)) GB" }
elseif ($val -ge 1MB) { "$([math]::Round($val/1MB,1)) MB" }
else { "$([math]::Round($val/1KB,0)) KB" }
} else {
$fb = if ($u.MinDownloadSize -gt 0) { $u.MinDownloadSize } else { $u.MaxDownloadSize }
$px = if ($u.MinDownloadSize -eq 0 -and $u.MaxDownloadSize -gt 0) { "<= " } else { "" }
$szStr = if ($fb -ge 1GB) { "$px$([math]::Round($fb/1GB,2)) GB" }
elseif ($fb -ge 1MB) { "$px$([math]::Round($fb/1MB,1)) MB" }
elseif ($fb -gt 0) { "$px$([math]::Round($fb/1KB,0)) KB" }
else { "Unknown" }
$val = $fb
}
$listView.Items[$ti].SubItems[3].Text = $szStr
$fetchDone.Add($ti) | Out-Null
$fetched += $val
}
if ($fetchDone.Count -ge $Updates.Count) {
$fetchTimer.Stop()
$fetchTimer.Dispose()
$totStr = if ($fetched -ge 1GB) { "$([math]::Round($fetched/1GB,2)) GB" }
elseif ($fetched -ge 1MB) { "$([math]::Round($fetched/1MB,1)) MB" }
elseif ($fetched -gt 0) { "$([math]::Round($fetched/1KB,0)) KB" }
else { "Unknown" }
$infoLabel.Text = "Total Size: $totStr | Available: $($Updates.Count) update(s)"
try { $fetchRS.Close(); $fetchRS.Dispose() } catch {}
}
})
$fetchTimer.Start()
$form.Add_FormClosed({
try { $fetchTimer.Stop(); $fetchTimer.Dispose() } catch {}
try { $fetchPS.Stop() } catch {}
try { $fetchRS.Close(); $fetchRS.Dispose() } catch {}
})
# Description TextBox
$descLabel = New-Object System.Windows.Forms.Label
$descLabel.Location = New-Object System.Drawing.Point(20, 490)
$descLabel.Size = New-Object System.Drawing.Size(200, 20)
$descLabel.Text = "Update Description:"
$descLabel.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
$form.Controls.Add($descLabel)
$descBox = New-Object System.Windows.Forms.TextBox
$descBox.Location = New-Object System.Drawing.Point(20, 515)
$descBox.Size = New-Object System.Drawing.Size(840, 100)
$descBox.Multiline = $true
$descBox.ReadOnly = $true
$descBox.ScrollBars = "Vertical"
$descBox.BackColor = [System.Drawing.Color]::White
$descBox.Font = New-Object System.Drawing.Font("Segoe UI", 9)
$form.Controls.Add($descBox)
# Show description when item is selected
$listView.Add_SelectedIndexChanged({
if ($listView.SelectedItems.Count -gt 0) {
$selectedUpdate = $listView.SelectedItems[0].Tag
$descBox.Text = $selectedUpdate.Description
}
})
# Buttons Panel
$buttonPanel = New-Object System.Windows.Forms.Panel
$buttonPanel.Dock = "Bottom"
$buttonPanel.Height = 60
$buttonPanel.BackColor = [System.Drawing.Color]::FromArgb(240, 240, 240)
$form.Controls.Add($buttonPanel)
# Install Button
$installButton = New-Object System.Windows.Forms.Button
$installButton.Location = New-Object System.Drawing.Point(640, 15)
$installButton.Size = New-Object System.Drawing.Size(100, 30)
$installButton.Text = "Install Now"
$installButton.BackColor = [System.Drawing.Color]::FromArgb(0, 120, 215)
$installButton.ForeColor = [System.Drawing.Color]::White
$installButton.FlatStyle = "Flat"
$installButton.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
$installButton.DialogResult = [System.Windows.Forms.DialogResult]::OK
$buttonPanel.Controls.Add($installButton)
# Later Button
$laterButton = New-Object System.Windows.Forms.Button
$laterButton.Location = New-Object System.Drawing.Point(750, 15)
$laterButton.Size = New-Object System.Drawing.Size(110, 30)
$laterButton.Text = "Remind Later"
$laterButton.BackColor = [System.Drawing.Color]::White
$laterButton.FlatStyle = "Flat"
$laterButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$buttonPanel.Controls.Add($laterButton)
# Select All / Deselect All
$selectAllButton = New-Object System.Windows.Forms.Button
$selectAllButton.Location = New-Object System.Drawing.Point(20, 15)
$selectAllButton.Size = New-Object System.Drawing.Size(90, 30)
$selectAllButton.Text = "Select All"
$selectAllButton.FlatStyle = "Flat"
$selectAllButton.Add_Click({
foreach ($item in $listView.Items) {
$item.Checked = $true
}
})
$buttonPanel.Controls.Add($selectAllButton)
$deselectAllButton = New-Object System.Windows.Forms.Button
$deselectAllButton.Location = New-Object System.Drawing.Point(120, 15)
$deselectAllButton.Size = New-Object System.Drawing.Size(100, 30)
$deselectAllButton.Text = "Deselect All"
$deselectAllButton.FlatStyle = "Flat"
$deselectAllButton.Add_Click({
foreach ($item in $listView.Items) {
$item.Checked = $false
}
})
$buttonPanel.Controls.Add($deselectAllButton)
# Show the form
$result = $form.ShowDialog()
if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
# Return selected updates
$selectedUpdates = @()
foreach ($item in $listView.Items) {
if ($item.Checked) {
$selectedUpdates += $item.Tag
}
}
$form.Dispose()
return @{
Action = "Install"
Updates = $selectedUpdates
}
}
else {
$form.Dispose()
return @{
Action = "Postpone"
Updates = @()
}
}
}
#endregion
#region Installation Progress Window
function Show-InstallationProgressWindow {
param(
[array]$Updates,
[scriptblock]$InstallAction
)
$form = New-Object System.Windows.Forms.Form
$form.Text = "Installing Windows Updates"
$form.Size = New-Object System.Drawing.Size(600, 450)
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedDialog"
$form.MaximizeBox = $false
$form.MinimizeBox = $false
$form.ControlBox = $false
$form.TopMost = $true
# Header
$headerPanel = New-Object System.Windows.Forms.Panel
$headerPanel.Dock = "Top"
$headerPanel.Height = 80
$headerPanel.BackColor = [System.Drawing.Color]::FromArgb(0, 120, 215)
$form.Controls.Add($headerPanel)
$titleLabel = New-Object System.Windows.Forms.Label
$titleLabel.Location = New-Object System.Drawing.Point(20, 15)
$titleLabel.Size = New-Object System.Drawing.Size(560, 30)
$titleLabel.Font = New-Object System.Drawing.Font("Segoe UI", 14, [System.Drawing.FontStyle]::Bold)
$titleLabel.ForeColor = [System.Drawing.Color]::White
$titleLabel.Text = "Installing Windows Updates"
$titleLabel.Name = "TitleLabel"
$headerPanel.Controls.Add($titleLabel)
$subtitleLabel = New-Object System.Windows.Forms.Label
$subtitleLabel.Location = New-Object System.Drawing.Point(20, 45)
$subtitleLabel.Size = New-Object System.Drawing.Size(560, 25)
$subtitleLabel.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$subtitleLabel.ForeColor = [System.Drawing.Color]::White
$subtitleLabel.Text = "Please wait while updates are being installed..."
$subtitleLabel.Name = "SubtitleLabel"
$headerPanel.Controls.Add($subtitleLabel)
# Status Label
$statusLabel = New-Object System.Windows.Forms.Label
$statusLabel.Location = New-Object System.Drawing.Point(20, 100)
$statusLabel.Size = New-Object System.Drawing.Size(560, 25)
$statusLabel.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$statusLabel.Text = "Preparing to install $($Updates.Count) update(s)..."
$statusLabel.Name = "StatusLabel"
$form.Controls.Add($statusLabel)
# Main Progress Bar (for download/install)
$mainProgressBar = New-Object System.Windows.Forms.ProgressBar
$mainProgressBar.Location = New-Object System.Drawing.Point(20, 135)
$mainProgressBar.Size = New-Object System.Drawing.Size(540, 30)
$mainProgressBar.Style = "Continuous"
$mainProgressBar.Name = "MainProgressBar"
$form.Controls.Add($mainProgressBar)
# Progress Label
$progressLabel = New-Object System.Windows.Forms.Label
$progressLabel.Location = New-Object System.Drawing.Point(20, 170)
$progressLabel.Size = New-Object System.Drawing.Size(560, 20)
$progressLabel.Font = New-Object System.Drawing.Font("Segoe UI", 9)
$progressLabel.Text = "0%"
$progressLabel.Name = "ProgressLabel"
$form.Controls.Add($progressLabel)
# Update List
$listBox = New-Object System.Windows.Forms.ListBox
$listBox.Location = New-Object System.Drawing.Point(20, 200)
$listBox.Size = New-Object System.Drawing.Size(540, 150)
$listBox.Font = New-Object System.Drawing.Font("Segoe UI", 9)
$listBox.Name = "UpdateListBox"
$form.Controls.Add($listBox)
foreach ($update in $Updates) {
$listBox.Items.Add("[ ] $($update.Title)") | Out-Null
}
# Warning Label
$warningLabel = New-Object System.Windows.Forms.Label
$warningLabel.Location = New-Object System.Drawing.Point(20, 360)
$warningLabel.Size = New-Object System.Drawing.Size(560, 40)
$warningLabel.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Italic)
$warningLabel.ForeColor = [System.Drawing.Color]::Red
$warningLabel.Text = "WARNING: Do not turn off your computer during installation"
$form.Controls.Add($warningLabel)
# Show form
$form.Show()
$form.Refresh()
return $form
}
function Update-InstallationProgress {
param(
[System.Windows.Forms.Form]$Form,
[int]$Current,
[int]$Total,
[string]$CurrentUpdate,
[string]$Status = "Installing"
)
if ($Form -and -not $Form.IsDisposed) {
$statusLabel = $Form.Controls.Find("StatusLabel", $false)[0]
if ($statusLabel) {
$statusLabel.Text = "$Status update $Current of $Total..."
}
$Form.Refresh()
[System.Windows.Forms.Application]::DoEvents()
}
}
function Update-InstallationProgressBar {
param(
[System.Windows.Forms.Form]$Form,
[int]$PercentComplete,
[string]$Status
)
if ($Form -and -not $Form.IsDisposed) {
$mainProgressBar = $Form.Controls.Find("MainProgressBar", $false)[0]
$progressLabel = $Form.Controls.Find("ProgressLabel", $false)[0]
$statusLabel = $Form.Controls.Find("StatusLabel", $false)[0]
if ($mainProgressBar) {
$mainProgressBar.Value = [Math]::Min(100, [Math]::Max(0, $PercentComplete))
}
if ($progressLabel) {
$progressLabel.Text = "$PercentComplete%"
}
if ($statusLabel -and $Status) {
$statusLabel.Text = $Status
}
$Form.Refresh()
[System.Windows.Forms.Application]::DoEvents()
}
}
#endregion
# Export functions
#region Install Progress Toasts
function Show-InstallProgressToast {
param([int]$Total, [string]$AppId = $script:APP_ID)
$script:ProgressSeq = [uint32]1
$data = [Windows.UI.Notifications.NotificationData]::new()
$data.Values["pv"] = "0"
$data.Values["pvs"] = "0 of $Total"
$data.Values["status"] = "Starting..."
$data.SequenceNumber = $script:ProgressSeq
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml(@"
<toast tag="wum-progress" scenario="incomingCall">
<visual>
<binding template="ToastGeneric">
<text>Installing Windows Updates</text>
<text>Please do not shut down your computer.</text>
<progress value="{pv}" valueStringOverride="{pvs}" title="" status="{status}"/>
</binding>
</visual>
<audio silent="true"/>
</toast>
"@)
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
$toast.Tag = $script:ProgressTag
$toast.Data = $data
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($AppId).Show($toast)
}
function Update-InstallProgressToast {
param(
[string]$Status,
[int]$Index,
[int]$Total,
[double]$Percent,
[string]$AppId = $script:APP_ID
)
$script:ProgressSeq++
$data = [Windows.UI.Notifications.NotificationData]::new()
$data.Values["pv"] = "$([math]::Round([math]::Max(0, [math]::Min(1, $Percent)), 2))"
$data.Values["pvs"] = if ($Index -gt 0) { "$Index of $Total" } else { "" }
$data.Values["status"] = $Status
$data.SequenceNumber = $script:ProgressSeq
try {
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($AppId).Update(
$data, $script:ProgressTag) | Out-Null
} catch {}
}
function Remove-InstallProgressToast {
param([string]$AppId = $script:APP_ID)
try {
[Windows.UI.Notifications.ToastNotificationManager]::History.Remove(
$script:ProgressTag, "", $AppId)
} catch {}
}
#endregion
Export-ModuleMember -Function Show-UpdateAvailableToast, Show-UpdateInstallingToast, `
Show-UpdateCompleteToast, Show-UpdateFailedToast, Show-RebootRequiredToast, `
Show-BalloonNotification, Show-UpdateDetailsWindow, Show-InstallationProgressWindow, `
Update-InstallationProgress, Update-InstallationProgressBar, `
Show-LoadingSplash, Close-LoadingSplash, Show-UpdatesAvailableNotification, `
Show-InstallProgressToast, Update-InstallProgressToast, Remove-InstallProgressToast