Private/UI/Show-OperationSplash.ps1
function Show-OperationSplash { <# .SYNOPSIS Displays a responsive loading splash screen for PIM operations like activation and refresh. .DESCRIPTION Creates a non-blocking loading splash screen that runs in a separate runspace. Provides smooth progress animation and status updates for long-running operations. Designed for use during role activation and refresh operations. .PARAMETER Title Window title text. Default: "PIM Operation" .PARAMETER InitialMessage Initial status message to display. Default: "Processing..." .PARAMETER ShowProgressBar Whether to show a progress bar. Default: $true .PARAMETER Width Width of the splash window. Default: 450 .PARAMETER Height Height of the splash window. Default: 180 .OUTPUTS PSCustomObject with UpdateStatus() and Close() methods for controlling the splash screen. .EXAMPLE $splash = Show-OperationSplash -Title "Role Activation" -InitialMessage "Preparing role activation..." $splash.UpdateStatus("Activating Global Administrator...", 50) $splash.Close() .EXAMPLE $splash = Show-OperationSplash -Title "Refreshing Roles" -InitialMessage "Fetching role data..." # Do work... $splash.Close() #> [CmdletBinding()] param( [string]$Title = "PIM Operation", [string]$InitialMessage = "Processing...", [bool]$ShowProgressBar = $true, [int]$Width = 450, [int]$Height = 180 ) # Synchronized hashtable for cross-runspace communication $syncHash = [hashtable]::Synchronized(@{ Title = $Title Message = $InitialMessage Progress = 0 TargetProgress = 0 ShouldClose = $false Form = $null StatusLabel = $null ProgressBar = $null IsDisposed = $false ShowProgressBar = $ShowProgressBar Width = $Width Height = $Height }) # 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 = $syncHash.Title Size = [System.Drawing.Size]::new($syncHash.Width, $syncHash.Height) StartPosition = 'CenterScreen' FormBorderStyle = 'FixedDialog' BackColor = [System.Drawing.Color]::White TopMost = $true ShowInTaskbar = $true MaximizeBox = $false MinimizeBox = $false ControlBox = $false # Hide close button during operations } # Add icon (optional - uses default if not found) try { $iconPath = Join-Path $PSScriptRoot "Resources\pim-icon.ico" if (Test-Path $iconPath) { $form.Icon = [System.Drawing.Icon]::new($iconPath) } } catch {} # Header panel with color $headerPanel = New-Object System.Windows.Forms.Panel -Property @{ Location = [System.Drawing.Point]::new(0, 0) Size = [System.Drawing.Size]::new($syncHash.Width, 40) BackColor = [System.Drawing.Color]::FromArgb(0, 120, 212) Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right } $form.Controls.Add($headerPanel) # Title label in header $titleLabel = New-Object System.Windows.Forms.Label -Property @{ Text = $syncHash.Title Font = [System.Drawing.Font]::new("Segoe UI", 11, [System.Drawing.FontStyle]::Bold) ForeColor = [System.Drawing.Color]::White Location = [System.Drawing.Point]::new(15, 10) Size = [System.Drawing.Size]::new($syncHash.Width - 30, 25) BackColor = [System.Drawing.Color]::Transparent } $headerPanel.Controls.Add($titleLabel) # 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(15, 55) Size = [System.Drawing.Size]::new($syncHash.Width - 30, 40) 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 AutoEllipsis = $true } $form.Controls.Add($statusLabel) # Progress bar (optional) if ($syncHash.ShowProgressBar) { $progressBar = New-Object System.Windows.Forms.ProgressBar -Property @{ Location = [System.Drawing.Point]::new(20, 105) Size = [System.Drawing.Size]::new($syncHash.Width - 40, 20) 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) $syncHash.ProgressBar = $progressBar } # Spinner animation (when no progress bar) if (-not $syncHash.ShowProgressBar) { $spinnerLabel = New-Object System.Windows.Forms.Label -Property @{ Text = "⚪⚪⚪" Font = [System.Drawing.Font]::new("Segoe UI", 14) Location = [System.Drawing.Point]::new(($syncHash.Width / 2) - 30, 100) Size = [System.Drawing.Size]::new(60, 30) TextAlign = 'MiddleCenter' ForeColor = [System.Drawing.Color]::FromArgb(0, 120, 212) } $form.Controls.Add($spinnerLabel) # Spinner animation timer $spinnerStates = @("⚫⚪⚪", "⚪⚫⚪", "⚪⚪⚫", "⚪⚫⚪") $spinnerIndex = 0 $spinnerTimer = New-Object System.Windows.Forms.Timer $spinnerTimer.Interval = 200 $spinnerTimer.Add_Tick({ $spinnerLabel.Text = $spinnerStates[$spinnerIndex] $spinnerIndex = ($spinnerIndex + 1) % $spinnerStates.Count }) $spinnerTimer.Start() } # Store UI references $syncHash.Form = $form $syncHash.StatusLabel = $statusLabel $form.Add_FormClosed({ $syncHash.IsDisposed = $true if ($spinnerTimer) { $spinnerTimer.Stop() $spinnerTimer.Dispose() } }) # 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 if exists if ($syncHash.ProgressBar) { $currentValue = $syncHash.ProgressBar.Value $targetValue = [Math]::Min($syncHash.TargetProgress, 100) if ($currentValue -ne $targetValue) { # Smooth animation - move 20% of the way to target each frame $diff = $targetValue - $currentValue $step = [Math]::Max(1, [Math]::Abs($diff * 0.2)) if ($diff -gt 0) { $syncHash.ProgressBar.Value = [Math]::Min($currentValue + $step, $targetValue) } elseif ($diff -lt 0) { $syncHash.ProgressBar.Value = [Math]::Max($currentValue - $step, $targetValue) } } } if ($syncHash.ShouldClose) { $timer.Stop() $form.Close() } }) $timer.Start() [void]$form.ShowDialog() # Cleanup $timer.Stop() $timer.Dispose() $syncHash.IsDisposed = $true }) # Start splash screen $handle = $powershell.BeginInvoke() # Wait for form to be created $maxWait = 50 # 5 seconds max $waited = 0 while (-not $syncHash.Form -and $waited -lt $maxWait) { Start-Sleep -Milliseconds 100 $waited++ } # 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) if (-not $this.IsDisposed) { $this.SyncHash.Message = $Status if ($Progress -ge 0 -and $this.SyncHash.ShowProgressBar) { $this.SyncHash.TargetProgress = [Math]::Min($Progress, 100) } } } $splashControl | Add-Member -MemberType ScriptMethod -Name Close -Value { if (-not $this.IsDisposed) { if ($this.SyncHash.ShowProgressBar) { $this.SyncHash.TargetProgress = 100 Start-Sleep -Milliseconds 300 } $this.SyncHash.ShouldClose = $true Start-Sleep -Milliseconds 200 if ($this.PowerShell) { $this.PowerShell.Stop() $this.PowerShell.Dispose() } if ($this.Runspace) { $this.Runspace.Close() $this.Runspace.Dispose() } } } return $splashControl } |