<# .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 = @" Windows Updates Available $UpdateCount update(s) ready to install $updateList "@ 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 = @" Installing Windows Updates Installing $UpdateCount update(s)... This may take several minutes. Please do not turn off your computer. "@ 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 = @" Updates Installed Successfully $SuccessCount update(s) installed $rebootText "@ if (-not $RebootRequired) { $template = @" Updates Installed Successfully $SuccessCount update(s) installed $rebootText "@ } 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 = @" Update Installation Failed Some updates could not be installed $ErrorMessage "@ 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 = @" Restart Required Your PC will restart in $MinutesUntilReboot minutes Save your work and close all applications "@ 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(@" Installing Windows Updates Please do not shut down your computer. "@) $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