Private/UI/Show-LoadingSplash.ps1
function Show-LoadingSplash { <# .SYNOPSIS Displays a responsive loading splash screen with progress tracking. .DESCRIPTION Creates a non-blocking loading splash screen that runs in a separate runspace. Provides smooth progress animation and status updates without freezing the UI. .PARAMETER Message Initial status message to display. Default: "Initializing..." .PARAMETER Title Window title text. Default: "PIM Activation" .OUTPUTS PSCustomObject with UpdateStatus() and Close() methods for controlling the splash screen. .EXAMPLE $splash = Show-LoadingSplash -Message "Loading configuration..." $splash.UpdateStatus("Processing items...", 50) $splash.Close() #> [CmdletBinding()] param( [string]$Message = "Initializing...", [string]$Title = "PIM Activation" ) # Synchronized hashtable for cross-runspace communication $syncHash = [hashtable]::Synchronized(@{ Message = $Message Progress = 0 TargetProgress = 0 ShouldClose = $false Form = $null StatusLabel = $null ProgressBar = $null IsDisposed = $false }) # Create STA runspace for the UI $runspace = [runspacefactory]::CreateRunspace() $runspace.ApartmentState = "STA" $runspace.ThreadOptions = "ReuseThread" $runspace.Open() $runspace.SessionStateProxy.SetVariable("syncHash", $syncHash) # Create PowerShell instance $powershell = [powershell]::Create() $powershell.Runspace = $runspace # UI creation script [void]$powershell.AddScript({ Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing # Main form $form = New-Object System.Windows.Forms.Form -Property @{ Text = "PIM Activation" Size = [System.Drawing.Size]::new(400, 150) StartPosition = 'CenterScreen' FormBorderStyle = 'FixedDialog' BackColor = [System.Drawing.Color]::White TopMost = $true ShowInTaskbar = $false MaximizeBox = $false MinimizeBox = $false } # Status label $statusLabel = New-Object System.Windows.Forms.Label -Property @{ Text = $syncHash.Message Font = [System.Drawing.Font]::new("Segoe UI", 10) Location = [System.Drawing.Point]::new(10, 20) Size = [System.Drawing.Size]::new(380, 30) TextAlign = 'MiddleCenter' ForeColor = [System.Drawing.Color]::FromArgb(32, 31, 30) Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right } $form.Controls.Add($statusLabel) # Progress bar $progressBar = New-Object System.Windows.Forms.ProgressBar -Property @{ Location = [System.Drawing.Point]::new(20, 60) Size = [System.Drawing.Size]::new(340, 30) Style = [System.Windows.Forms.ProgressBarStyle]::Continuous Minimum = 0 Maximum = 100 Value = 0 ForeColor = [System.Drawing.Color]::FromArgb(0, 103, 184) BackColor = [System.Drawing.Color]::FromArgb(242, 242, 242) Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right } $form.Controls.Add($progressBar) # Store UI references $syncHash.Form = $form $syncHash.StatusLabel = $statusLabel $syncHash.ProgressBar = $progressBar $form.Add_FormClosed({ $syncHash.IsDisposed = $true }) # Update timer for smooth animations $timer = New-Object System.Windows.Forms.Timer $timer.Interval = 20 $timer.Add_Tick({ # Update status text if ($syncHash.StatusLabel.Text -ne $syncHash.Message) { $syncHash.StatusLabel.Text = $syncHash.Message } # Animate progress bar $currentValue = $syncHash.ProgressBar.Value $targetValue = [Math]::Min($syncHash.TargetProgress, 100) if ($currentValue -ne $targetValue) { $diff = $targetValue - $currentValue $step = [Math]::Max(1, [Math]::Abs($diff) / 10) $newValue = if ($diff -gt 0) { [Math]::Min($currentValue + $step, $targetValue) } else { [Math]::Max($currentValue - $step, $targetValue) } $syncHash.ProgressBar.Value = [int]$newValue $syncHash.Progress = [int]$newValue } if ($syncHash.ShouldClose) { $timer.Stop() $timer.Dispose() $form.Hide() $form.Close() $form.Dispose() $syncHash.IsDisposed = $true [System.Windows.Forms.Application]::ExitThread() return } }) $timer.Start() # Show the form and start message pump [void]$form.Show() $form.Activate() $form.BringToFront() $form.TopMost = $true # Use DoEvents loop instead of Application.Run to avoid blocking while (-not $syncHash.ShouldClose -and -not $form.IsDisposed) { [System.Windows.Forms.Application]::DoEvents() Start-Sleep -Milliseconds 50 } # Cleanup when closing if (-not $form.IsDisposed) { $form.Hide() $form.Close() $form.Dispose() } $syncHash.IsDisposed = $true }) # Start splash screen $handle = $powershell.BeginInvoke() # Wait a moment for the form to be created $maxWait = 50 # 5 seconds maximum $waitCount = 0 while (-not $syncHash.Form -and $waitCount -lt $maxWait) { Start-Sleep -Milliseconds 100 $waitCount++ } # Control object $splashControl = [PSCustomObject]@{ SyncHash = $syncHash PowerShell = $powershell Runspace = $runspace Handle = $handle } $splashControl | Add-Member -MemberType ScriptProperty -Name IsDisposed -Value { $this.SyncHash.IsDisposed } $splashControl | Add-Member -MemberType ScriptMethod -Name UpdateStatus -Value { param([string]$Status, [int]$Progress = -1) $this.SyncHash.Message = $Status if ($Progress -ge 0) { $this.SyncHash.TargetProgress = [Math]::Min($Progress, 100) } } $splashControl | Add-Member -MemberType ScriptMethod -Name Close -Value { if (-not $this.IsDisposed) { $this.SyncHash.TargetProgress = 100 Start-Sleep -Milliseconds 200 $this.SyncHash.ShouldClose = $true Start-Sleep -Milliseconds 100 # Wait for the runspace to finish cleanup $maxWait = 20 # 2 seconds max $waitCount = 0 while (-not $this.SyncHash.IsDisposed -and $waitCount -lt $maxWait) { Start-Sleep -Milliseconds 100 $waitCount++ } # Force cleanup if needed if ($this.PowerShell) { try { $this.PowerShell.Stop() } catch {} try { $this.PowerShell.Dispose() } catch {} } if ($this.Runspace) { try { $this.Runspace.Close() } catch {} try { $this.Runspace.Dispose() } catch {} } } } Start-Sleep -Milliseconds 100 return $splashControl } function Update-LoadingStatus { <# .SYNOPSIS Updates the splash screen status and progress. .PARAMETER SplashForm The splash screen control object returned by Show-LoadingSplash. .PARAMETER Status New status message to display. .PARAMETER Progress Progress percentage (0-100). Optional. .EXAMPLE Update-LoadingStatus -SplashForm $splash -Status "Processing..." -Progress 75 #> [CmdletBinding()] param( [Parameter(Mandatory)] [PSCustomObject]$SplashForm, [Parameter(Mandatory)] [string]$Status, [int]$Progress = -1 ) if ($SplashForm -and $SplashForm.SyncHash -and -not $SplashForm.IsDisposed) { $SplashForm.UpdateStatus($Status, $Progress) } } function Close-LoadingSplash { <# .SYNOPSIS Closes the loading splash screen and cleans up resources. .PARAMETER SplashForm The splash screen control object returned by Show-LoadingSplash. .EXAMPLE Close-LoadingSplash -SplashForm $splash #> [CmdletBinding()] param( [Parameter(Mandatory)] [PSCustomObject]$SplashForm ) if ($SplashForm -and -not $SplashForm.IsDisposed) { $SplashForm.Close() } } |