Public/Show-OSDAppUI.ps1
|
function Set-OSDAppUIColumnWidths { [CmdletBinding()] param( [Parameter(Mandatory)][hashtable]$State, [Parameter(Mandatory)][ValidateSet('Staged','Applications','Cache','Logs')][string]$Key ) $list = $State.Lists[$Key] if (-not $list -or $list.Columns.Count -eq 0) { return } # Reserve enough room for Teams' four-part package version, and let # Application / Message absorb remaining space on window resize. $fixed = switch ($Key) { 'Staged' { @(65,0,90,100,125,185,105) } 'Applications' { @(0,75,90,110,175,100,100,90) } 'Cache' { @(0,75,88,175,95,115,74,122) } 'Logs' { @(185,78,135,125,0) } } $flexIndex = if ($Key -in @('Staged','Applications','Cache')) { if ($Key -eq 'Staged') { 1 } else { 0 } } else { 4 } $minimumFlex = if ($Key -eq 'Logs') { 270 } elseif ($Key -in @('Applications','Cache')) { 180 } else { 200 } $reserved = 0 foreach ($width in $fixed) { $reserved += [int]$width } $available = [math]::Max(0,$list.ClientSize.Width - 22) $flexWidth = [math]::Max($minimumFlex,$available - $reserved) for ($i=0; $i -lt $fixed.Count -and $i -lt $list.Columns.Count; $i++) { $width = if ($i -eq $flexIndex) { $flexWidth } else { [int]$fixed[$i] } if ($width -gt 0 -and $list.Columns[$i].Width -ne $width) { $list.Columns[$i].Width = $width } } } function Show-OSDAppUIRows { [CmdletBinding()] param( [Parameter(Mandatory)][hashtable]$State, [Parameter(Mandatory)][ValidateSet('Staged','Applications','Cache','Logs')][string]$Key ) $view = $State.Lists[$Key] $rows = @($State.Rows[$Key]) $needle = [string]$State.Search.Text if (-not [string]::IsNullOrWhiteSpace($needle)) { $rows = @($rows | Where-Object { $searchable = @($_.SubItems | ForEach-Object { [string]$_.Text }) -join ' ' $searchable.IndexOf($needle,[StringComparison]::OrdinalIgnoreCase) -ge 0 }) } $sorting = $State.Sorts[$Key] if ($sorting.Column -ge 0) { $column = [int]$sorting.Column $rows = @($rows | Sort-Object -Property @{ Expression = { $value = [string]$_.SubItems[$column].Text if ($Key -eq 'Staged' -and $column -eq 0) { [int]$value } elseif ($Key -eq 'Cache' -and $column -eq 6) { $parsed = [double]0 $rawValue = $value.Replace(' MB','').Replace(',','.') if ([double]::TryParse($rawValue,[System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture,[ref]$parsed)) { $parsed } else { -1.0 } } else { $value.ToLowerInvariant() } } } -Descending:([bool]$sorting.Descending)) } $view.BeginUpdate() try { $view.Items.Clear() foreach ($item in $rows) { [void]$view.Items.Add($item) } } finally { $view.EndUpdate() } } function Set-OSDAppUIListSort { [CmdletBinding()] param( [Parameter(Mandatory)][hashtable]$State, [Parameter(Mandatory)][ValidateSet('Staged','Applications','Cache','Logs')][string]$Key, [Parameter(Mandatory)][int]$Column ) $previous = $State.Sorts[$Key] $descending = if ($previous.Column -eq $Column) { -not [bool]$previous.Descending } else { $false } $State.Sorts[$Key] = @{ Column=$Column; Descending=$descending } Show-OSDAppUIRows -State $State -Key $Key } function Update-OSDAppUILogViewer { [CmdletBinding()] param([Parameter(Mandatory)][hashtable]$State) $State.Rows['Logs'] = @() $State.LogDetails.Text = '' $selected = $State.LogFiles.SelectedIndex if ($selected -lt 0 -or $selected -ge @($State.LogEntries).Count) { $State.LogInfo.Text = 'Select an available log file.' Show-OSDAppUIRows -State $State -Key Logs return } $path = [string]$State.LogEntries[$selected].Path try { $entries = @(Get-OSDAppUILogRows -Path $path -Tail 500) $items = @( foreach ($entry in $entries) { $item = [System.Windows.Forms.ListViewItem]::new([string]$entry.Timestamp) [void]$item.SubItems.Add([string]$entry.Level) [void]$item.SubItems.Add([string]$entry.Component) [void]$item.SubItems.Add([string]$entry.Event) [void]$item.SubItems.Add([string]$entry.Message) if ($entry.Level -eq 'Error') { $item.ForeColor = [System.Drawing.Color]::Firebrick } elseif ($entry.Level -eq 'Warning') { $item.ForeColor = [System.Drawing.Color]::DarkOrange } $item.Tag = $entry $item.ToolTipText = [string]$entry.Raw $item } ) $State.Rows['Logs'] = $items $State.LogInfo.Text = "$($entries.Count) recent lines | $path" Show-OSDAppUIRows -State $State -Key Logs Set-OSDAppUIColumnWidths -State $State -Key Logs # The log details pane should be useful immediately after opening. # Select the most recent visible entry without changing log order. $visible = $State.Lists['Logs'] if ($visible.Items.Count -gt 0) { $last = $visible.Items[$visible.Items.Count - 1] $last.Selected = $true $entry = $last.Tag $State.LogDetails.Text = "Time: $($entry.Timestamp) | Level: $($entry.Level) | Component: $($entry.Component) | Event: $($entry.Event)" + [Environment]::NewLine + [Environment]::NewLine + [string]$entry.Message } else { $State.LogDetails.Text = 'No log entries match the current search.' } } catch { $State.LogInfo.Text = "Cannot read log: $($_.Exception.Message)" $State.LogDetails.Text = 'Unable to load the selected log file.' Show-OSDAppUIRows -State $State -Key Logs } } function Update-OSDAppUIReadOnlyView { [CmdletBinding()] param([Parameter(Mandatory)][hashtable]$State) $data = Get-OSDAppUIReadOnlySnapshot -WindowsPath $State.Target -Offline:$State.Offline -SkipSignatureVerification:$State.SkipSignatureVerification $State.Snapshot = $data $builtInMetadata = @(Get-OSDAppUIBuiltInMetadata) $stagedItems = @( $number = 0 foreach ($app in @($data.StagedApps)) { $number++ $item = [System.Windows.Forms.ListViewItem]::new([string]$number) $displayInfo = Get-OSDAppUIDisplayInfo -Application $app -CatalogApps $data.CatalogApps -BuiltInMetadata $builtInMetadata [void]$item.SubItems.Add([string]$displayInfo.DisplayName) [void]$item.SubItems.Add($(if ($app.Source -eq 'BuiltIn') { 'Built-in' } else { 'Repository' })) [void]$item.SubItems.Add([string]$displayInfo.Vendor) [void]$item.SubItems.Add((Get-OSDAppUIDisplayVersion -Application $app)) [void]$item.SubItems.Add((Get-OSDAppUICachedVersion -Application $app -CacheEntries $data.CacheEntries)) [void]$item.SubItems.Add([string]$app.Architecture) $item.Tag = [pscustomobject]@{ Application=$app; Vendor=$displayInfo.Vendor } $item } ) $State.Rows['Staged'] = $stagedItems $appItems = @( foreach ($app in @($data.CatalogApps | Sort-Object Source,DisplayName)) { $displayInfo = Get-OSDAppUIDisplayInfo -Application $app -CatalogApps $data.CatalogApps -BuiltInMetadata $builtInMetadata $item = [System.Windows.Forms.ListViewItem]::new([string]$displayInfo.DisplayName) [void]$item.SubItems.Add($(if ($app.Source -eq 'BuiltIn') { 'Built-in' } else { 'Repository' })) [void]$item.SubItems.Add([string]$displayInfo.Vendor) [void]$item.SubItems.Add((Get-OSDAppUIDisplayVersion -Application $app)) [void]$item.SubItems.Add((Get-OSDAppUICachedVersion -Application $app -CacheEntries $data.CacheEntries)) [void]$item.SubItems.Add([string]$app.Architecture) [void]$item.SubItems.Add((Get-OSDAppUIAvailabilityLabel -Availability $app.Availability -Offline:$State.Offline)) [void]$item.SubItems.Add($(if ([string]$app.Id -in @($data.StagedApps.Id)) { 'Staged' } else { '-' })) $item } # Staged apps absent from the current catalog/cache remain visible. foreach ($app in @($data.StagedApps)) { if ([string]$app.Id -in @($data.CatalogApps.Id)) { continue } $displayInfo = Get-OSDAppUIDisplayInfo -Application $app -CatalogApps $data.CatalogApps -BuiltInMetadata $builtInMetadata $item = [System.Windows.Forms.ListViewItem]::new([string]$displayInfo.DisplayName) [void]$item.SubItems.Add($(if ($app.Source -eq 'BuiltIn') { 'Built-in' } else { 'Repository' })) [void]$item.SubItems.Add([string]$displayInfo.Vendor) [void]$item.SubItems.Add((Get-OSDAppUIDisplayVersion -Application $app)) [void]$item.SubItems.Add((Get-OSDAppUICachedVersion -Application $app -CacheEntries $data.CacheEntries)) [void]$item.SubItems.Add([string]$app.Architecture) [void]$item.SubItems.Add('Not in current catalog/cache') [void]$item.SubItems.Add('Staged') $item } ) $State.Rows['Applications'] = $appItems $cacheItems = @( foreach ($entry in @($data.CacheEntries)) { $displayInfo = Get-OSDAppUIDisplayInfo -Application $entry -CatalogApps $data.CatalogApps -BuiltInMetadata $builtInMetadata $item = [System.Windows.Forms.ListViewItem]::new([string]$displayInfo.DisplayName) [void]$item.SubItems.Add($(if ($entry.Source -eq 'BuiltIn') { 'Built-in' } else { 'Repository' })) [void]$item.SubItems.Add([string]$displayInfo.Vendor) $cacheVersion = [string]$entry.Version if ([string]::IsNullOrWhiteSpace($cacheVersion) -or $cacheVersion -eq 'Current') { $cacheVersion = 'Not recorded' } [void]$item.SubItems.Add($cacheVersion) [void]$item.SubItems.Add([string]$entry.Architecture) $integrity = if (-not $entry.Valid) { 'Missing / invalid' } elseif ($entry.Source -eq 'Repository') { 'SHA-256 verified' } else { 'Files present' } [void]$item.SubItems.Add($integrity) [void]$item.SubItems.Add($(if ($null -ne $entry.SizeMB) { "$($entry.SizeMB) MB" } else { '-' })) [void]$item.SubItems.Add((Get-OSDAppUISignatureLabel -Status ([string]$entry.SignatureStatus) -Source ([string]$entry.Source))) if ([string]$entry.Source -eq 'BuiltIn' -and [string]$entry.SignatureStatus -in @('NotSigned','NotTrusted','HashMismatch','PublisherMismatch','Check failed','File missing','UnknownError')) { $item.ForeColor = [System.Drawing.Color]::Firebrick } $item.ToolTipText = @( "Integrity: $integrity" "Signature: $($entry.SignatureStatus)" "Scope: $($entry.SignatureScope)" "Signed file: $($entry.SignatureFile)" "Signer: $($entry.SignatureSigner)" ) -join [Environment]::NewLine $item.Tag = $entry $item } ) $State.Rows['Cache'] = $cacheItems foreach ($key in @('Staged','Applications','Cache')) { Show-OSDAppUIRows -State $State -Key $key Set-OSDAppUIColumnWidths -State $State -Key $key } $State.LogFiles.Items.Clear() $State.LogEntries = @($data.Logs) foreach ($entry in $State.LogEntries) { [void]$State.LogFiles.Items.Add([string]$entry.Label) } if ($State.LogFiles.Items.Count -gt 0) { $State.LogFiles.SelectedIndex = 0 } else { $State.Rows['Logs'] = @() $State.LogInfo.Text = 'No existing OSDApps log files found on the device or USB cache.' $State.LogDetails.Text = '' Show-OSDAppUIRows -State $State -Key Logs } $stagedCount = @($data.StagedApps).Count $builtInCount = @($data.StagedApps | Where-Object Source -eq 'BuiltIn').Count $repositoryCount = @($data.StagedApps | Where-Object Source -eq 'Repository').Count $setupStatus = if ($data.SetupCompleteExists) { 'Present' } else { 'Not found' } $State.StagedInfo.Text = "$stagedCount staged for SetupComplete | $builtInCount built-in, $repositoryCount repository | SetupComplete.cmd: $setupStatus" $State.CacheInfo.Text = if ($data.CachePath) { "USB cache: $($data.CachePath) | $(@($data.CacheEntries).Count) entries | Signatures: $(if ($State.SkipSignatureVerification) { 'skipped' } elseif ($State.WinPE) { 'unavailable in WinPE' } else { 'checked where supported' })" } else { 'USB cache: not connected / not found.' } $usbLabel = if ($data.CachePath) { [string]$data.CachePath } else { 'not connected' } $State.ContextStatus.Text = "Target: $($State.TargetKind) | USB: $usbLabel | $($State.CatalogMode)" $detail = "Target: $($State.TargetDescription)" + [Environment]::NewLine + "USB: $usbLabel" + [Environment]::NewLine + $State.CatalogDescription $State.ContextToolTip.SetToolTip($State.ContextStatus,$detail) # Show a real staged manifest entry by default instead of an empty panel. # This is only a UI selection; it does not change DeviceManifest ordering. if ($State.StagedList.Items.Count -gt 0) { $first = $State.StagedList.Items[0] $first.Selected = $true $selected = $first.Tag $State.Details.Text = Format-OSDAppUIStagedDetails -Application $selected.Application -Vendor $selected.Vendor } else { $State.Details.Text = if ($stagedCount) { 'No staged applications match the current search.' } else { 'No staged applications found. Stage apps using the PowerShell CLI first.' } } $State.Status.Text = "$stagedCount staged | $(@($data.CacheEntries).Count) USB cache entries | $(@($data.Logs).Count) logs" } function Show-OSDAppUI { <# .SYNOPSIS Read-only overview of staged OSDApps, built-in and repository catalog, USB cache, and available log files. All staging and configuration stay CLI-only. .EXAMPLE Show-OSDAppUI -Offline .EXAMPLE Show-OSDAppUI -TestMode -Offline .EXAMPLE Show-OSDAppUI -WindowsPath 'D:\' -Offline #> [CmdletBinding()] param( [string]$WindowsPath, [switch]$Offline, [switch]$PreviewOnly, [switch]$TestMode, [switch]$SkipSignatureVerification ) if ([System.Threading.Thread]::CurrentThread.ApartmentState -ne [System.Threading.ApartmentState]::STA) { throw 'An STA session is required. Run Windows PowerShell with powershell.exe -STA -NoProfile.' } try { Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop Add-Type -AssemblyName System.Drawing -ErrorAction Stop } catch { throw "Windows Forms is unavailable in this Windows/WinPE image: $($_.Exception.Message)" } if ($script:OSDAppUIState) { throw 'OSDApps Viewer is already open.' } if ($TestMode -and $PSBoundParameters.ContainsKey('WindowsPath')) { throw '-TestMode points to the existing isolated TEMP test folder. Do not also specify -WindowsPath.' } $winPE = Test-OSDAppWinPE $target = $null $targetText = 'No Windows target detected' if ($TestMode) { $candidate = Get-OSDAppUITestWindowsPath if (Test-Path -LiteralPath $candidate -PathType Container) { $target = $candidate $targetText = "Test folder: $target" } else { $targetText = "Test folder not present: $candidate" } } elseif ($PSBoundParameters.ContainsKey('WindowsPath')) { $target = Resolve-OSDAppWindowsPath -WindowsPath $WindowsPath $targetText = "Read-only Windows target: $target" } elseif ($winPE) { try { $target = Resolve-OSDAppWindowsPath $targetText = "WinPE target: $target" } catch { $targetText = "WinPE: $($_.Exception.Message)" } } elseif ($env:SystemDrive) { $target = [System.IO.Path]::GetPathRoot((Join-Path $env:SystemDrive '\Windows')) $targetText = "Windows target: $target" } $form = [System.Windows.Forms.Form]::new() $form.Text = 'OSDApps Viewer - Read-only' $form.Size = [System.Drawing.Size]::new(980,710) $form.MinimumSize = [System.Drawing.Size]::new(800,580) $form.StartPosition = 'CenterScreen' $form.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Dpi $form.Font = [System.Drawing.Font]::new('Segoe UI',9) $root = [System.Windows.Forms.TableLayoutPanel]::new() $root.Dock = 'Fill' $root.RowCount = 5 [void]$root.RowStyles.Add([System.Windows.Forms.RowStyle]::new('Absolute',80)) [void]$root.RowStyles.Add([System.Windows.Forms.RowStyle]::new('Absolute',34)) [void]$root.RowStyles.Add([System.Windows.Forms.RowStyle]::new('Absolute',42)) [void]$root.RowStyles.Add([System.Windows.Forms.RowStyle]::new('Percent',100)) [void]$root.RowStyles.Add([System.Windows.Forms.RowStyle]::new('Absolute',45)) $form.Controls.Add($root) $header = [System.Windows.Forms.Panel]::new() $header.Dock = 'Fill' $header.BackColor = [System.Drawing.Color]::FromArgb(35,59,95) [void]$root.Controls.Add($header,0,0) $title = [System.Windows.Forms.Label]::new() $title.Text = 'OSDApps Viewer' $title.Font = [System.Drawing.Font]::new('Segoe UI',17,[System.Drawing.FontStyle]::Bold) $title.ForeColor = [System.Drawing.Color]::White $title.SetBounds(18,9,670,37) $title.Anchor = 'Top,Left,Right' $header.Controls.Add($title) $subtitle = [System.Windows.Forms.Label]::new() $subtitle.Text = 'Deployment overview' $subtitle.ForeColor = [System.Drawing.Color]::Gainsboro $subtitle.SetBounds(20,49,560,23) $header.Controls.Add($subtitle) $readOnly = [System.Windows.Forms.Label]::new() $readOnly.Text = 'Read-only' $readOnly.ForeColor = [System.Drawing.Color]::White $readOnly.TextAlign = [System.Drawing.ContentAlignment]::MiddleRight $readOnly.SetBounds(811,22,110,28) $readOnly.Anchor = 'Top,Right' $header.Controls.Add($readOnly) $context = [System.Windows.Forms.Panel]::new() $context.Dock = 'Fill' $context.BackColor = [System.Drawing.Color]::FromArgb(242,244,247) [void]$root.Controls.Add($context,0,1) $targetKind = if ($TestMode) { 'Test environment' } elseif ($winPE) { 'Offline Windows' } else { 'Windows' } $catalogUri = (Get-OSDAppConfiguration).CatalogUri $catalogMode = if ($Offline) { 'Offline' } elseif ($catalogUri) { 'Online' } else { 'No catalog' } $catalogDescription = if ($Offline) { 'Catalog lookup disabled (-Offline).' } elseif ($catalogUri) { "Catalog: $($catalogUri.AbsoluteUri)" } else { 'Repository not configured in this PowerShell session.' } $contextStatus = [System.Windows.Forms.Label]::new() $contextStatus.Dock = 'Fill' $contextStatus.Padding = [System.Windows.Forms.Padding]::new(14,0,8,0) $contextStatus.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft $contextStatus.AutoEllipsis = $true $contextStatus.Text = "Target: $targetKind | USB: detecting... | $catalogMode" [void]$context.Controls.Add($contextStatus) $toolTip = [System.Windows.Forms.ToolTip]::new() $contextToolTip = "Target: $targetText" + [Environment]::NewLine + $catalogDescription $toolTip.SetToolTip($contextStatus,$contextToolTip) $searchPanel = [System.Windows.Forms.TableLayoutPanel]::new() $searchPanel.Dock = 'Fill' $searchPanel.ColumnCount = 2 $searchPanel.Padding = [System.Windows.Forms.Padding]::new(8,5,8,5) [void]$searchPanel.ColumnStyles.Add([System.Windows.Forms.ColumnStyle]::new('Absolute',73)) [void]$searchPanel.ColumnStyles.Add([System.Windows.Forms.ColumnStyle]::new('Percent',100)) [void]$root.Controls.Add($searchPanel,0,2) $searchLabel = [System.Windows.Forms.Label]::new() $searchLabel.Text = 'Search' $searchLabel.Dock = 'Fill' $searchLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft [void]$searchPanel.Controls.Add($searchLabel,0,0) $searchBox = [System.Windows.Forms.TextBox]::new() $searchBox.Dock = 'Fill' [void]$searchPanel.Controls.Add($searchBox,1,0) $tabs = [System.Windows.Forms.TabControl]::new() $tabs.Dock = 'Fill' [void]$root.Controls.Add($tabs,0,3) $stagedTab = [System.Windows.Forms.TabPage]::new('Staged for device') $catalogTab = [System.Windows.Forms.TabPage]::new('Apps') $cacheTab = [System.Windows.Forms.TabPage]::new('Cache') $logsTab = [System.Windows.Forms.TabPage]::new('Logs') foreach ($tab in @($stagedTab,$catalogTab,$cacheTab,$logsTab)) { [void]$tabs.TabPages.Add($tab) } $stagedLayout = [System.Windows.Forms.TableLayoutPanel]::new() $stagedLayout.Dock = 'Fill' $stagedLayout.RowCount = 3 [void]$stagedLayout.RowStyles.Add([System.Windows.Forms.RowStyle]::new('Absolute',38)) [void]$stagedLayout.RowStyles.Add([System.Windows.Forms.RowStyle]::new('Percent',100)) [void]$stagedLayout.RowStyles.Add([System.Windows.Forms.RowStyle]::new('Absolute',160)) $stagedTab.Controls.Add($stagedLayout) $stagedInfo = [System.Windows.Forms.Label]::new() $stagedInfo.Dock = 'Fill' $stagedInfo.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft [void]$stagedLayout.Controls.Add($stagedInfo,0,0) $stagedList = [System.Windows.Forms.ListView]::new() $stagedList.Dock = 'Fill' $stagedList.View = 'Details' $stagedList.FullRowSelect = $true $stagedList.GridLines = $true $stagedList.MultiSelect = $false [void]$stagedList.Columns.Add('Order',65) [void]$stagedList.Columns.Add('Display Name',270) [void]$stagedList.Columns.Add('Source',90) [void]$stagedList.Columns.Add('Vendor',100) [void]$stagedList.Columns.Add('Version / policy',125) [void]$stagedList.Columns.Add('USB version',185) [void]$stagedList.Columns.Add('Architecture',105) [void]$stagedLayout.Controls.Add($stagedList,0,1) $details = [System.Windows.Forms.TextBox]::new() $details.Multiline = $true $details.ReadOnly = $true $details.ScrollBars = [System.Windows.Forms.ScrollBars]::Vertical $details.Dock = 'Fill' $details.BackColor = [System.Drawing.Color]::White [void]$stagedLayout.Controls.Add($details,0,2) $catalogLayout = [System.Windows.Forms.TableLayoutPanel]::new() $catalogLayout.Dock = 'Fill' $catalogLayout.RowCount = 2 [void]$catalogLayout.RowStyles.Add([System.Windows.Forms.RowStyle]::new('Absolute',38)) [void]$catalogLayout.RowStyles.Add([System.Windows.Forms.RowStyle]::new('Percent',100)) $catalogTab.Controls.Add($catalogLayout) $catalogInfo = [System.Windows.Forms.Label]::new() $catalogInfo.Text = 'Built-in and repository apps | Configure and stage using PowerShell CLI' $catalogInfo.Dock = 'Fill' $catalogInfo.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft [void]$catalogLayout.Controls.Add($catalogInfo,0,0) $catalogList = [System.Windows.Forms.ListView]::new() $catalogList.Dock = 'Fill' $catalogList.View = 'Details' $catalogList.FullRowSelect = $true $catalogList.GridLines = $true [void]$catalogList.Columns.Add('Display Name',260) [void]$catalogList.Columns.Add('Source',90) [void]$catalogList.Columns.Add('Vendor',100) [void]$catalogList.Columns.Add('Version / policy',110) [void]$catalogList.Columns.Add('USB version',115) [void]$catalogList.Columns.Add('Architecture',95) [void]$catalogList.Columns.Add('Availability',160) [void]$catalogList.Columns.Add('Deployment',95) [void]$catalogLayout.Controls.Add($catalogList,0,1) $cacheLayout = [System.Windows.Forms.TableLayoutPanel]::new() $cacheLayout.Dock = 'Fill' $cacheLayout.RowCount = 2 [void]$cacheLayout.RowStyles.Add([System.Windows.Forms.RowStyle]::new('Absolute',48)) [void]$cacheLayout.RowStyles.Add([System.Windows.Forms.RowStyle]::new('Percent',100)) $cacheTab.Controls.Add($cacheLayout) $cacheInfo = [System.Windows.Forms.Label]::new() $cacheInfo.Dock = 'Fill' $cacheInfo.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft $cacheInfo.AutoEllipsis = $true [void]$cacheLayout.Controls.Add($cacheInfo,0,0) $cacheList = [System.Windows.Forms.ListView]::new() $cacheList.Dock = 'Fill' $cacheList.View = 'Details' $cacheList.FullRowSelect = $true $cacheList.GridLines = $true $cacheList.ShowItemToolTips = $true [void]$cacheList.Columns.Add('Display Name',265) [void]$cacheList.Columns.Add('Source',75) [void]$cacheList.Columns.Add('Vendor',88) [void]$cacheList.Columns.Add('USB version',175) [void]$cacheList.Columns.Add('Architecture',105) [void]$cacheList.Columns.Add('Integrity',145) [void]$cacheList.Columns.Add('Size',100) [void]$cacheList.Columns.Add('Signature',145) [void]$cacheLayout.Controls.Add($cacheList,0,1) $logsLayout = [System.Windows.Forms.TableLayoutPanel]::new() $logsLayout.Dock = 'Fill' $logsLayout.RowCount = 4 [void]$logsLayout.RowStyles.Add([System.Windows.Forms.RowStyle]::new('Absolute',43)) [void]$logsLayout.RowStyles.Add([System.Windows.Forms.RowStyle]::new('Absolute',30)) [void]$logsLayout.RowStyles.Add([System.Windows.Forms.RowStyle]::new('Percent',100)) [void]$logsLayout.RowStyles.Add([System.Windows.Forms.RowStyle]::new('Absolute',110)) $logsTab.Controls.Add($logsLayout) $logFiles = [System.Windows.Forms.ComboBox]::new() $logFiles.Dock = 'Fill' $logFiles.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList $logFiles.Margin = [System.Windows.Forms.Padding]::new(7,8,7,4) [void]$logsLayout.Controls.Add($logFiles,0,0) $logInfo = [System.Windows.Forms.Label]::new() $logInfo.Text = 'Select a log entry to view its full details below.' $logInfo.Dock = 'Fill' $logInfo.AutoEllipsis = $true [void]$logsLayout.Controls.Add($logInfo,0,1) $logList = [System.Windows.Forms.ListView]::new() $logList.View = 'Details' $logList.Dock = 'Fill' $logList.FullRowSelect = $true $logList.MultiSelect = $false $logList.GridLines = $true $logList.ShowItemToolTips = $true [void]$logList.Columns.Add('Timestamp',185) [void]$logList.Columns.Add('Level',78) [void]$logList.Columns.Add('Component',135) [void]$logList.Columns.Add('Event',125) [void]$logList.Columns.Add('Message',420) [void]$logsLayout.Controls.Add($logList,0,2) $logDetails = [System.Windows.Forms.TextBox]::new() $logDetails.Multiline = $true $logDetails.ReadOnly = $true $logDetails.ScrollBars = [System.Windows.Forms.ScrollBars]::Vertical $logDetails.Dock = 'Fill' $logDetails.BackColor = [System.Drawing.Color]::White $logDetails.Font = [System.Drawing.Font]::new('Consolas',9) [void]$logsLayout.Controls.Add($logDetails,0,3) $footer = [System.Windows.Forms.TableLayoutPanel]::new() $footer.Dock = 'Fill' $footer.ColumnCount = 3 [void]$footer.ColumnStyles.Add([System.Windows.Forms.ColumnStyle]::new('Percent',100)) [void]$footer.ColumnStyles.Add([System.Windows.Forms.ColumnStyle]::new('Absolute',120)) [void]$footer.ColumnStyles.Add([System.Windows.Forms.ColumnStyle]::new('Absolute',100)) [void]$root.Controls.Add($footer,0,4) $status = [System.Windows.Forms.Label]::new() $status.Text = 'Loading inventory...' $status.Dock = 'Fill' $status.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft [void]$footer.Controls.Add($status,0,0) $refresh = [System.Windows.Forms.Button]::new() $refresh.Text = 'Refresh' $refresh.Dock = 'Fill' [void]$footer.Controls.Add($refresh,1,0) $close = [System.Windows.Forms.Button]::new() $close.Text = 'Close' $close.Dock = 'Fill' [void]$footer.Controls.Add($close,2,0) $script:OSDAppUIState = @{ Form=$form; Target=$target; Offline=[bool]$Offline; Search=$searchBox SkipSignatureVerification=[bool]$SkipSignatureVerification; WinPE=[bool]$winPE ContextStatus=$contextStatus; ContextToolTip=$toolTip TargetKind=$targetKind; TargetDescription=$targetText CatalogMode=$catalogMode; CatalogDescription=$catalogDescription StagedList=$stagedList; CatalogList=$catalogList; CacheList=$cacheList StagedInfo=$stagedInfo; CacheInfo=$cacheInfo; Details=$details LogFiles=$logFiles; LogList=$logList; LogDetails=$logDetails; LogInfo=$logInfo LogEntries=@(); Status=$status; Snapshot=$null Lists=@{ Staged=$stagedList; Applications=$catalogList; Cache=$cacheList; Logs=$logList } Rows=@{ Staged=@(); Applications=@(); Cache=@(); Logs=@() } Sorts=@{ Staged=@{Column=-1;Descending=$false} Applications=@{Column=-1;Descending=$false} Cache=@{Column=-1;Descending=$false} Logs=@{Column=-1;Descending=$false} } } $stagedList.Add_SelectedIndexChanged({ $state=$script:OSDAppUIState if (-not $state -or $state.StagedList.SelectedItems.Count -eq 0) { return } $selected = $state.StagedList.SelectedItems[0].Tag $state.Details.Text = Format-OSDAppUIStagedDetails -Application $selected.Application -Vendor $selected.Vendor }) $logFiles.Add_SelectedIndexChanged({ $state=$script:OSDAppUIState if ($state) { Update-OSDAppUILogViewer -State $state } }) $logList.Add_SelectedIndexChanged({ $state=$script:OSDAppUIState if (-not $state -or $state.LogList.SelectedItems.Count -eq 0) { return } $entry=$state.LogList.SelectedItems[0].Tag $state.LogDetails.Text = "Time: $($entry.Timestamp) | Level: $($entry.Level) | Component: $($entry.Component) | Event: $($entry.Event)" + [Environment]::NewLine + [Environment]::NewLine + [string]$entry.Message }) $searchBox.Add_TextChanged({ $state=$script:OSDAppUIState if (-not $state) { return } foreach ($key in @('Staged','Applications','Cache','Logs')) { Show-OSDAppUIRows -State $state -Key $key } }) $stagedList.Add_ColumnClick({ $state=$script:OSDAppUIState if ($state) { Set-OSDAppUIListSort -State $state -Key Staged -Column $_.Column } }) $catalogList.Add_ColumnClick({ $state=$script:OSDAppUIState if ($state) { Set-OSDAppUIListSort -State $state -Key Applications -Column $_.Column } }) $cacheList.Add_ColumnClick({ $state=$script:OSDAppUIState if ($state) { Set-OSDAppUIListSort -State $state -Key Cache -Column $_.Column } }) $logList.Add_ColumnClick({ $state=$script:OSDAppUIState if ($state) { Set-OSDAppUIListSort -State $state -Key Logs -Column $_.Column } }) $stagedList.Add_ClientSizeChanged({ $state = $script:OSDAppUIState if ($state) { Set-OSDAppUIColumnWidths -State $state -Key Staged } }) $catalogList.Add_ClientSizeChanged({ $state = $script:OSDAppUIState if ($state) { Set-OSDAppUIColumnWidths -State $state -Key Applications } }) $cacheList.Add_ClientSizeChanged({ $state = $script:OSDAppUIState if ($state) { Set-OSDAppUIColumnWidths -State $state -Key Cache } }) $logList.Add_ClientSizeChanged({ $state = $script:OSDAppUIState if ($state) { Set-OSDAppUIColumnWidths -State $state -Key Logs } }) $refresh.Add_Click({ $state = $script:OSDAppUIState if (-not $state) { return } $state.Status.Text = 'Refreshing inventory...' $state.Form.Refresh() try { Update-OSDAppUIReadOnlyView -State $state } catch { $state.Status.Text = "Refresh failed: $($_.Exception.Message)" [void][System.Windows.Forms.MessageBox]::Show($_.Exception.Message,'OSDApps - Inventory','OK','Error') } }) $close.Add_Click({ $script:OSDAppUIState.Form.Close() }) try { try { Update-OSDAppUIReadOnlyView -State $script:OSDAppUIState } catch { $script:OSDAppUIState.Status.Text = "Load failed: $($_.Exception.Message)" Write-Warning "OSDApps UI: $($_.Exception.Message)" } [void]$form.ShowDialog() } finally { $script:OSDAppUIState = $null $form.Dispose() } } |