[CmdletBinding()] [System.STAThread()] param() # Check PowerShell Version if ($PSVersionTable.PSVersion.Major -lt 7) { Write-Error "PowerShell 7 or later is required to run this script." exit 1 } # -------------------------------------------------------------------------- # SECTION: Variables & Constants # -------------------------------------------------------------------------- # $FFUDevelopmentPath = $PSScriptRoot $FFUDevelopmentPath = 'C:\FFUDevelopment' # hard coded for testing $AppsPath = "$FFUDevelopmentPath\Apps" $AppListJsonPath = "$AppsPath\AppList.json" $UserAppListJsonPath = "$AppsPath\UserAppList.json" # Define path for UserAppList.json #Microsoft sites will intermittently fail on downloads. These headers are to help with that. $Headers = @{ "Accept" = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" "Accept-Encoding" = "gzip, deflate, br, zstd" "Accept-Language" = "en-US,en;q=0.9" "Priority" = "u=0, i" "Sec-Ch-Ua" = "`"Microsoft Edge`";v=`"125`", `"Chromium`";v=`"125`", `"Not.A/Brand`";v=`"24`"" "Sec-Ch-Ua-Mobile" = "?0" "Sec-Ch-Ua-Platform" = "`"Windows`"" "Sec-Fetch-Dest" = "document" "Sec-Fetch-Mode" = "navigate" "Sec-Fetch-Site" = "none" "Sec-Fetch-User" = "?1" "Upgrade-Insecure-Requests" = "1" } $UserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0' # --- NEW: Central State Object --- $script:uiState = [PSCustomObject]@{ Window = $null; Controls = @{ featureCheckBoxes = @{}; # Moved from script scope UpdateInstallAppsBasedOnUpdates = $null # Placeholder for the scriptblock }; Data = @{ allDriverModels = [System.Collections.Generic.List[PSCustomObject]]::new(); appsScriptVariablesDataList = [System.Collections.Generic.List[PSCustomObject]]::new(); versionData = $null; # Will be initialized later vmSwitchMap = @{} # To store Hyper-V switch to IP mapping }; Flags = @{ installAppsForcedByUpdates = $false; prevInstallAppsStateBeforeUpdates = $null; installAppsCheckedByOffice = $false; lastSortProperty = $null; lastSortAscending = $true }; Defaults = @{}; LogFilePath = "$FFUDevelopmentPath\FFUDevelopment_UI.log" } # Remove any existing modules to avoid conflicts if (Get-Module -Name 'FFU.Common.Core' -ErrorAction SilentlyContinue) { Remove-Module -Name 'FFU.Common.Core' -Force } if (Get-Module -Name 'FFUUI.Core' -ErrorAction SilentlyContinue) { Remove-Module -Name 'FFUUI.Core' -Force } # Import the common core module first for logging Import-Module "$PSScriptRoot\common\FFU.Common.Core.psm1" # Import the Core UI Logic Module Import-Module "$PSScriptRoot\FFUUI.Core\FFUUI.Core.psm1" # Set the log path for the common logger (for UI operations) Set-CommonCoreLogPath -Path $script:uiState.LogFilePath # Setting long path support - this prevents issues where some applications have deep directory structures # and driver extraction fails due to long paths. $script:uiState.Flags.originalLongPathsValue = $null # Store original value try { $script:uiState.Flags.originalLongPathsValue = Get-ItemPropertyValue -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -ErrorAction SilentlyContinue } catch { # Key or value might not exist, which is fine. WriteLog "Could not read initial LongPathsEnabled value (may not exist)." } # Enable long paths if not already enabled if ($script:uiState.Flags.originalLongPathsValue -ne 1) { try { WriteLog 'LongPathsEnabled is not set to 1. Setting it to 1 for the duration of this script.' Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1 -Force WriteLog 'LongPathsEnabled set to 1.' } catch { WriteLog "Error setting LongPathsEnabled registry key: $($_.Exception.Message). Long path issues might persist." # Optionally show a warning to the user if this fails? # [System.Windows.MessageBox]::Show("Could not enable long path support. Some operations might fail.", "Warning", "OK", "Warning") } } else { WriteLog "LongPathsEnabled is already set to 1." } # ---------------------------------------------------------------------------- # SECTION: LOAD UI # ---------------------------------------------------------------------------- # Helper function to safely set UI properties from config and log the process function Set-UIValue { param( [string]$ControlName, [string]$PropertyName, [object]$ConfigObject, [string]$ConfigKey, [scriptblock]$TransformValue = $null, # Optional scriptblock to transform the value from config [psobject]$State # Pass the $State object ) $control = $State.Controls[$ControlName] if ($null -eq $control) { WriteLog "LoadConfig Error: Control '$ControlName' not found in the state object." return } # Robust check for property existence. $keyExists = $false if ($ConfigObject -is [System.Management.Automation.PSCustomObject] -and $null -ne $ConfigObject.PSObject.Properties) { # Use the Match() method, which returns a collection of matching properties. # If the count is greater than 0, the key exists. try { if (($ConfigObject.PSObject.Properties.Match($ConfigKey)).Count -gt 0) { $keyExists = $true } } catch { WriteLog "ERROR: Exception while trying to Match key '$ConfigKey' on ConfigObject.PSObject.Properties. Error: $($_.Exception.Message)" # $keyExists remains false } } if (-not $keyExists) { WriteLog "LoadConfig Info: Key '$ConfigKey' not found in configuration object. Skipping '$ControlName.$PropertyName'." return } $valueFromConfig = $ConfigObject.$ConfigKey WriteLog "LoadConfig: Preparing to set '$ControlName.$PropertyName'. Config key: '$ConfigKey', Raw value: '$valueFromConfig'." $finalValue = $valueFromConfig if ($null -ne $TransformValue) { try { $finalValue = Invoke-Command -ScriptBlock $TransformValue -ArgumentList $valueFromConfig WriteLog "LoadConfig: Transformed value for '$ControlName.$PropertyName' (from key '$ConfigKey') is: '$finalValue'." } catch { WriteLog "LoadConfig Error: Failed to transform value for '$ControlName.$PropertyName' from key '$ConfigKey'. Error: $($_.Exception.Message)" return } } try { # Handle ComboBox SelectedItem specifically if ($control -is [System.Windows.Controls.ComboBox] -and $PropertyName -eq 'SelectedItem') { $itemToSelect = $null # Iterate through the Items collection of the ComboBox foreach ($item in $control.Items) { $itemValue = $null if ($item -is [System.Windows.Controls.ComboBoxItem]) { $itemValue = $item.Content } elseif ($item -is [pscustomobject] -and $item.PSObject.Properties['Value']) { $itemValue = $item.Value } elseif ($item -is [pscustomobject] -and $item.PSObject.Properties['Display']) { # Assuming 'Display' might be used if 'Value' isn't $itemValue = $item.Display } else { $itemValue = $item # For simple string items or direct object comparison } # Compare, ensuring types are compatible or converting $finalValue if necessary if (($null -ne $itemValue -and $itemValue.ToString() -eq $finalValue.ToString()) -or ($item -eq $finalValue)) { $itemToSelect = $item break } } if ($null -ne $itemToSelect) { $control.SelectedItem = $itemToSelect WriteLog "LoadConfig: Successfully set '$ControlName.SelectedItem' by finding matching item for value '$finalValue'." } elseif ($control.IsEditable -and ($finalValue -is [string] -or $finalValue -is [int] -or $finalValue -is [long])) { $control.Text = $finalValue.ToString() WriteLog "LoadConfig: Set '$ControlName.Text' to '$($finalValue.ToString())' as SelectedItem match failed (editable ComboBox)." } else { $itemsString = "" try { # Safer way to get item strings $itemStrings = @() foreach ($cbItem in $control.Items) { if ($null -ne $cbItem) { $itemStrings += $cbItem.ToString() } else { $itemStrings += "[NULL_ITEM]" } } $itemsString = $itemStrings -join "; " } catch { $itemsString = "Error retrieving item strings." } WriteLog "LoadConfig Warning: Could not find or set item matching value '$finalValue' for '$ControlName.SelectedItem'. Current items: [$itemsString]" } } else { # For other properties or controls $control.$PropertyName = $finalValue WriteLog "LoadConfig: Successfully set '$ControlName.$PropertyName' to '$finalValue'." } } catch { WriteLog "LoadConfig Error: Failed to set '$ControlName.$PropertyName' to '$finalValue'. Error: $($_.Exception.Message)" } } # -------------------------------------------------------------------------- # SECTION: Driver Download Functions # -------------------------------------------------------------------------- # Helper function to convert raw driver objects to a standardized format function ConvertTo-StandardizedDriverModel { param( [Parameter(Mandatory = $true)] [PSCustomObject]$RawDriverObject, [Parameter(Mandatory = $true)] [string]$Make ) $modelDisplay = $RawDriverObject.Model # Default $id = $RawDriverObject.Model # Default $link = $null $productName = $null $machineType = $null if ($RawDriverObject.PSObject.Properties['Link']) { $link = $RawDriverObject.Link } # Lenovo specific handling if ($Make -eq 'Lenovo') { # RawDriverObject.Model is "ProductName (MachineType)" from Get-LenovoDriversModelList # RawDriverObject.ProductName is "ProductName" # RawDriverObject.MachineType is "MachineType" $modelDisplay = $RawDriverObject.Model # This is already "ProductName (MachineType)" $productName = $RawDriverObject.ProductName $machineType = $RawDriverObject.MachineType $id = $RawDriverObject.MachineType # Use MachineType as a more specific ID for Lenovo backend operations if needed } return [PSCustomObject]@{ IsSelected = $false Make = $Make Model = $modelDisplay # Primary display string, used as identifier in ListView Link = $link Id = $id # Technical/unique identifier (e.g., MachineType for Lenovo) ProductName = $productName # Specific for Lenovo MachineType = $machineType # Specific for Lenovo Version = "" # Placeholder Type = "" # Placeholder Size = "" # Placeholder Arch = "" # Placeholder DownloadStatus = "" # Initial download status } } # Helper function to get models for a selected Make and standardize them function Get-ModelsForMake { param( [Parameter(Mandatory = $true)] [string]$SelectedMake, [Parameter(Mandatory = $true)] [psobject]$State ) $standardizedModels = [System.Collections.Generic.List[PSCustomObject]]::new() $rawModels = @() # Get necessary values from UI or script scope $localDriversFolder = $State.Controls.txtDriversFolder.Text $localWindowsRelease = $null if ($null -ne $State.Controls.cmbWindowsRelease.SelectedItem) { $localWindowsRelease = $State.Controls.cmbWindowsRelease.SelectedItem.Value } # $Headers and $UserAgent are available from script scope if (-not $localWindowsRelease -and ($SelectedMake -eq 'Dell' -or $SelectedMake -eq 'Lenovo')) { [System.Windows.MessageBox]::Show("Please select a Windows Release first for $SelectedMake.", "Missing Information", "OK", "Warning") throw "Windows Release not selected for $SelectedMake." } switch ($SelectedMake) { 'Microsoft' { $rawModels = Get-MicrosoftDriversModelList -Headers $Headers -UserAgent $UserAgent } 'Dell' { $rawModels = Get-DellDriversModelList -WindowsRelease $localWindowsRelease -DriversFolder $localDriversFolder -Make $SelectedMake } 'HP' { $rawModels = Get-HPDriversModelList -DriversFolder $localDriversFolder -Make $SelectedMake } 'Lenovo' { $modelSearchTerm = [Microsoft.VisualBasic.Interaction]::InputBox("Enter Lenovo Model Name or Machine Type (e.g., T480 or 20L5):", "Lenovo Model Search", "") if ([string]::IsNullOrWhiteSpace($modelSearchTerm)) { # User cancelled or entered nothing return @() } $State.Controls.txtStatus.Text = "Searching Lenovo models for '$modelSearchTerm'..." $rawModels = Get-LenovoDriversModelList -ModelSearchTerm $modelSearchTerm -Headers $Headers -UserAgent $UserAgent } default { [System.Windows.MessageBox]::Show("Selected Make '$SelectedMake' is not supported for automatic model retrieval.", "Unsupported Make", "OK", "Warning") return @() } } if ($null -ne $rawModels) { foreach ($rawModel in $rawModels) { # Filter out Chromebooks for Lenovo before standardization if ($SelectedMake -eq 'Lenovo' -and $rawModel.Model -match 'Chromebook') { WriteLog "Get-ModelsForMake: Skipping Chromebook model: $($rawModel.Model)" continue } $standardizedModels.Add((ConvertTo-StandardizedDriverModel -RawDriverObject $rawModel -Make $SelectedMake)) } } return $standardizedModels.ToArray() } # Function to filter the driver model list based on text input function Filter-DriverModels { param( [string]$filterText ) # Check if UI elements and the full list are available if ($null -eq $script:uiState.Controls.lstDriverModels -or $null -eq $script:uiState.Data.allDriverModels) { WriteLog "Filter-DriverModels: ListView or full model list not available." return } WriteLog "Filtering models with text: '$filterText'" # Filter the full list based on the Model property (case-insensitive) # Use -match for potentially better performance or stick with -like # Ensure the result is always an array, even if only one item matches $filteredModels = @($script:uiState.Data.allDriverModels | Where-Object { $_.Model -like "*$filterText*" }) # Update the ListView's ItemsSource with the filtered list # Setting ItemsSource directly should work for simple scenarios $script:uiState.Controls.lstDriverModels.ItemsSource = $filteredModels # Explicitly refresh the ListView's view to reflect the changes in the bound source if ($null -ne $script:uiState.Controls.lstDriverModels.ItemsSource -and $script:uiState.Controls.lstDriverModels.Items -is [System.ComponentModel.ICollectionView]) { $script:uiState.Controls.lstDriverModels.Items.Refresh() } elseif ($null -ne $script:uiState.Controls.lstDriverModels.ItemsSource) { # Fallback refresh if not using ICollectionView (less common for direct ItemsSource binding) $script:uiState.Controls.lstDriverModels.Items.Refresh() } WriteLog "Filtered list contains $($filteredModels.Count) models." } # Function to save selected driver models to a JSON file function Save-DriversJson { WriteLog "Save-DriversJson function called." $selectedDrivers = @($script:uiState.Controls.lstDriverModels.Items | Where-Object { $_.IsSelected }) if (-not $selectedDrivers) { [System.Windows.MessageBox]::Show("No drivers selected to save.", "Save Drivers", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information) WriteLog "No drivers selected to save." return } $outputJson = @{} # Use a Hashtable for the desired structure $selectedDrivers | Group-Object -Property Make | ForEach-Object { $makeName = $_.Name $modelsForThisMake = @() # Initialize an array to hold model objects foreach ($driverItem in $_.Group) { $modelObject = $null switch ($makeName) { 'Microsoft' { $modelObject = @{ Name = $driverItem.Model # Model is the display name Link = $driverItem.Link } } 'Dell' { $modelObject = @{ Name = $driverItem.Model } } 'HP' { $modelObject = @{ Name = $driverItem.Model } } 'Lenovo' { $modelObject = @{ Name = $driverItem.Model # This is "ProductName (MachineType)" ProductName = $driverItem.ProductName # This is "ProductName" MachineType = $driverItem.MachineType # This is "MachineType" } } default { WriteLog "Save-DriversJson: Unknown Make '$makeName' encountered for model '$($driverItem.Model)'. Skipping." } } if ($null -ne $modelObject) { $modelsForThisMake += $modelObject } } if ($modelsForThisMake.Count -gt 0) { # Store the array of model objects under a "Models" key $outputJson[$makeName] = @{ "Models" = $modelsForThisMake } } } $sfd = New-Object System.Windows.Forms.SaveFileDialog $sfd.Filter = "JSON files (*.json)|*.json|All files (*.*)|*.*" $sfd.Title = "Save Selected Drivers" $sfd.FileName = "Drivers.json" $sfd.InitialDirectory = $FFUDevelopmentPath if ($sfd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { try { $outputJson | ConvertTo-Json -Depth 5 | Set-Content -Path $sfd.FileName -Encoding UTF8 [System.Windows.MessageBox]::Show("Selected drivers saved to $($sfd.FileName)", "Save Successful", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information) WriteLog "Selected drivers saved to $($sfd.FileName)" } catch { [System.Windows.MessageBox]::Show("Error saving drivers file: $($_.Exception.Message)", "Save Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error) WriteLog "Error saving drivers file to $($sfd.FileName): $($_.Exception.Message)" } } else { WriteLog "Save drivers operation cancelled by user." } } # Function to import driver models from a JSON file function Import-DriversJson { WriteLog "Import-DriversJson function called." $ofd = New-Object System.Windows.Forms.OpenFileDialog $ofd.Filter = "JSON files (*.json)|*.json|All files (*.*)|*.*" $ofd.Title = "Import Drivers" $ofd.InitialDirectory = $FFUDevelopmentPath if ($ofd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { try { $importedData = Get-Content -Path $ofd.FileName -Raw | ConvertFrom-Json if ($null -eq $importedData -or $importedData -isnot [System.Management.Automation.PSCustomObject]) { [System.Windows.MessageBox]::Show("Invalid JSON file format. Expected a JSON object with Makes as keys.", "Import Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error) WriteLog "Import-DriversJson: Invalid JSON format in $($ofd.FileName). Expected an object." return } $newModelsAdded = 0 $existingModelsUpdated = 0 if ($null -eq $script:uiState.Data.allDriverModels) { $script:uiState.Data.allDriverModels = @() } $importedData.PSObject.Properties | ForEach-Object { $makeName = $_.Name $makeData = $_.Value # This is the object containing "Models" array # Check if $makeData is null, not a PSCustomObject, or does not have a 'Models' property if ($null -eq $makeData -or $makeData -isnot [System.Management.Automation.PSCustomObject] -or -not ($makeData.PSObject.Properties | Where-Object { $_.Name -eq 'Models' })) { WriteLog "Import-DriversJson: Skipping Make '$makeName' due to invalid structure or missing 'Models' key." return # Corresponds to 'continue' in ForEach-Object script block } $modelObjectArray = $makeData.Models # This is now an array of objects if ($null -eq $modelObjectArray -or $modelObjectArray -isnot [array]) { WriteLog "Import-DriversJson: Skipping Make '$makeName' because 'Models' value is not an array." return } foreach ($importedModelObject in $modelObjectArray) { if ($null -eq $importedModelObject -or -not $importedModelObject.PSObject.Properties['Name']) { WriteLog "Import-DriversJson: Skipping model for Make '$makeName' due to missing 'Name' property or null object." continue } $importedModelNameFromObject = $importedModelObject.Name if ([string]::IsNullOrWhiteSpace($importedModelNameFromObject)) { WriteLog "Import-DriversJson: Skipping empty model name for Make '$makeName'." continue } $existingModel = $script:uiState.Data.allDriverModels | Where-Object { $_.Make -eq $makeName -and $_.Model -eq $importedModelNameFromObject } | Select-Object -First 1 if ($null -ne $existingModel) { $existingModel.IsSelected = $true $existingModel.DownloadStatus = "Imported" if ($makeName -eq 'Microsoft' -and $importedModelObject.PSObject.Properties['Link']) { if ($existingModel.Link -ne $importedModelObject.Link) { $existingModel.Link = $importedModelObject.Link WriteLog "Import-DriversJson: Updated Link for existing Microsoft model '$($existingModel.Model)'." } } elseif ($makeName -eq 'Lenovo') { $updateExistingLenovo = $false if ($importedModelObject.PSObject.Properties['ProductName'] -and $existingModel.PSObject.Properties['ProductName'] -and $existingModel.ProductName -ne $importedModelObject.ProductName) { $existingModel.ProductName = $importedModelObject.ProductName $updateExistingLenovo = $true } if ($importedModelObject.PSObject.Properties['MachineType'] -and $existingModel.PSObject.Properties['MachineType'] -and $existingModel.MachineType -ne $importedModelObject.MachineType) { $existingModel.MachineType = $importedModelObject.MachineType $existingModel.Id = $importedModelObject.MachineType # Update Id as well $updateExistingLenovo = $true } if ($updateExistingLenovo) { WriteLog "Import-DriversJson: Updated ProductName/MachineType/Id for existing Lenovo model '$($existingModel.Model)'." } } $existingModelsUpdated++ WriteLog "Import-DriversJson: Marked existing model '$($existingModel.Make) - $($existingModel.Model)' as imported." } else { # Model does not exist, create a new one $importedLink = if ($makeName -eq 'Microsoft' -and $importedModelObject.PSObject.Properties['Link']) { $importedModelObject.Link } else { $null } $importedId = $importedModelNameFromObject # Default Id $importedProductName = $null $importedMachineType = $null if ($makeName -eq 'Lenovo') { $importedProductName = if ($importedModelObject.PSObject.Properties['ProductName']) { $importedModelObject.ProductName } else { $null } $importedMachineType = if ($importedModelObject.PSObject.Properties['MachineType']) { $importedModelObject.MachineType } else { $null } if ($null -ne $importedMachineType) { $importedId = $importedMachineType # Override Id for Lenovo } # Fallback parsing if ProductName/MachineType are missing from JSON but Name has the pattern if (($null -eq $importedProductName -or $null -eq $importedMachineType) -and $importedModelNameFromObject -match '(.+?)\s*\((.+?)\)$') { WriteLog "Import-DriversJson: Lenovo model '$importedModelNameFromObject' missing ProductName or MachineType in JSON. Attempting to parse from Name." if ($null -eq $importedProductName) { $importedProductName = $matches[1].Trim() } if ($null -eq $importedMachineType) { $importedMachineType = $matches[2].Trim() $importedId = $importedMachineType # Update Id if MachineType was parsed here } } if ($null -eq $importedProductName -or $null -eq $importedMachineType) { WriteLog "Import-DriversJson: Warning - Lenovo model '$importedModelNameFromObject' is missing ProductName or MachineType after parsing. ID might be based on full name." } } $newDriverModel = [PSCustomObject]@{ IsSelected = $true Make = $makeName Model = $importedModelNameFromObject # Full display name Link = $importedLink Id = $importedId ProductName = $importedProductName MachineType = $importedMachineType Version = "" Type = "" Size = "" Arch = "" DownloadStatus = "Imported" } $script:uiState.Data.allDriverModels += $newDriverModel $newModelsAdded++ WriteLog "Import-DriversJson: Added new model '$($newDriverModel.Make) - $($newDriverModel.Model)' from import. ID: $($newDriverModel.Id), Link: $($newDriverModel.Link)" } } } $script:uiState.Data.allDriverModels = $script:uiState.Data.allDriverModels | Sort-Object @{Expression = { $_.IsSelected }; Descending = $true }, Make, Model Filter-DriverModels -filterText $script:uiState.Controls.txtModelFilter.Text $message = "Driver import complete.`nNew models added: $newModelsAdded`nExisting models updated: $existingModelsUpdated" [System.Windows.MessageBox]::Show($message, "Import Successful", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information) WriteLog $message } catch { [System.Windows.MessageBox]::Show("Error importing drivers file: $($_.Exception.Message)", "Import Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error) WriteLog "Error importing drivers file from $($ofd.FileName): $($_.Exception.Message)" } } else { WriteLog "Import drivers operation cancelled by user." } } # Some default values $defaultFFUPrefix = "_FFU" # -------------------------------------------------------------------------- #Remove old log file if found if (Test-Path -Path $script:uiState.LogFilePath) { Remove-item -Path $script:uiState.LogFilePath -Force } # Function to refresh the Windows Release ComboBox based on ISO path function Update-WindowsReleaseCombo { param([string]$isoPath) if (-not $script:uiState.Controls.cmbWindowsRelease) { return } # Ensure combo exists $oldSelectedItemValue = $null if ($null -ne $script:uiState.Controls.cmbWindowsRelease.SelectedItem) { $oldSelectedItemValue = $script:uiState.Controls.cmbWindowsRelease.SelectedItem.Value } # Get the appropriate list of releases from the helper module $availableReleases = Get-AvailableWindowsReleases -IsoPath $isoPath # Update the ComboBox ItemsSource $script:uiState.Controls.cmbWindowsRelease.ItemsSource = $availableReleases $script:uiState.Controls.cmbWindowsRelease.DisplayMemberPath = 'Display' $script:uiState.Controls.cmbWindowsRelease.SelectedValuePath = 'Value' # Try to re-select the previously selected item, or default $itemToSelect = $availableReleases | Where-Object { $_.Value -eq $oldSelectedItemValue } | Select-Object -First 1 if ($null -ne $itemToSelect) { $script:uiState.Controls.cmbWindowsRelease.SelectedItem = $itemToSelect } elseif ($availableReleases.Count -gt 0) { # Default to Windows 11 if available, otherwise the first item $defaultItem = $availableReleases | Where-Object { $_.Value -eq 11 } | Select-Object -First 1 if ($null -eq $defaultItem) { $defaultItem = $availableReleases[0] } $script:uiState.Controls.cmbWindowsRelease.SelectedItem = $defaultItem } else { # No items available (should not happen with current logic) $script:uiState.Controls.cmbWindowsRelease.SelectedIndex = -1 } } # Function to refresh the Windows Version ComboBox based on selected release and ISO path function Update-WindowsVersionCombo { param( [int]$selectedRelease, [string]$isoPath ) $combo = $script:uiState.Controls.cmbWindowsVersion # Use script-scoped variable if (-not $combo) { return } # Ensure combo exists # Get available versions and default from the helper module $versionData = Get-AvailableWindowsVersions -SelectedRelease $selectedRelease -IsoPath $isoPath # Update the ComboBox ItemsSource and IsEnabled state $combo.ItemsSource = $versionData.Versions $combo.IsEnabled = $versionData.IsEnabled # Set the selected item if ($null -ne $versionData.DefaultVersion -and $versionData.Versions -contains $versionData.DefaultVersion) { $combo.SelectedItem = $versionData.DefaultVersion } elseif ($versionData.Versions.Count -gt 0) { $combo.SelectedIndex = 0 # Fallback to first item if default isn't valid } else { $combo.SelectedIndex = -1 # No items available } } # Function to refresh the Windows SKU ComboBox based on selected release function Update-WindowsSkuCombo { # This function no longer takes parameters. # It derives the selected release value and display name from the cmbWindowsRelease ComboBox. $skuCombo = $script:uiState.Controls.cmbWindowsSKU if (-not $skuCombo) { WriteLog "Update-WindowsSkuCombo: SKU ComboBox not found." return } $releaseCombo = $script:uiState.Controls.cmbWindowsRelease if (-not $releaseCombo -or $null -eq $releaseCombo.SelectedItem) { WriteLog "Update-WindowsSkuCombo: Windows Release ComboBox not found or no item selected. Cannot update SKUs." $skuCombo.ItemsSource = @() # Clear SKUs $skuCombo.SelectedIndex = -1 return } $selectedReleaseItem = $releaseCombo.SelectedItem $selectedReleaseValue = $selectedReleaseItem.Value $selectedReleaseDisplayName = $selectedReleaseItem.Display $previousSelectedSku = $null if ($null -ne $skuCombo.SelectedItem) { $previousSelectedSku = $skuCombo.SelectedItem } WriteLog "Update-WindowsSkuCombo: Updating SKUs for Release Value '$selectedReleaseValue' (Display: '$selectedReleaseDisplayName')." # Call Get-AvailableSkusForRelease with both Value and DisplayName $availableSkus = Get-AvailableSkusForRelease -SelectedReleaseValue $selectedReleaseValue -SelectedReleaseDisplayName $selectedReleaseDisplayName $skuCombo.ItemsSource = $availableSkus WriteLog "Update-WindowsSkuCombo: Set ItemsSource with $($availableSkus.Count) SKUs." # Attempt to re-select the previous SKU, or "Pro", or the first available if ($null -ne $previousSelectedSku -and $availableSkus -contains $previousSelectedSku) { $skuCombo.SelectedItem = $previousSelectedSku WriteLog "Update-WindowsSkuCombo: Re-selected previous SKU '$previousSelectedSku'." } elseif ($availableSkus -contains "Pro") { $skuCombo.SelectedItem = "Pro" WriteLog "Update-WindowsSkuCombo: Selected default SKU 'Pro'." } elseif ($availableSkus.Count -gt 0) { $skuCombo.SelectedIndex = 0 WriteLog "Update-WindowsSkuCombo: Selected first available SKU '$($skuCombo.SelectedItem)'." } else { $skuCombo.SelectedIndex = -1 # No SKUs available WriteLog "Update-WindowsSkuCombo: No SKUs available for Release '$selectedReleaseValue' (Display: '$selectedReleaseDisplayName')." } } # Combined function to refresh both Release and Version combos function Refresh-WindowsSettingsCombos { param([string]$isoPath) # Update Release combo first Update-WindowsReleaseCombo -isoPath $isoPath # Get the newly selected release value $selectedReleaseValue = 11 # Default to 11 if selection is null if ($null -ne $script:uiState.Controls.cmbWindowsRelease.SelectedItem) { $selectedReleaseValue = $script:uiState.Controls.cmbWindowsRelease.SelectedItem.Value } # Update Version combo based on the selected release Update-WindowsVersionCombo -selectedRelease $selectedReleaseValue -isoPath $isoPath # Update SKU combo based on the selected release (now derives values internally) Update-WindowsSkuCombo } Add-Type -AssemblyName WindowsBase Add-Type -AssemblyName PresentationCore, PresentationFramework Add-Type -AssemblyName System.Windows.Forms # Load XAML $xamlPath = Join-Path $PSScriptRoot "BuildFFUVM_UI.xaml" if (-not (Test-Path $xamlPath)) { Write-Error "XAML file not found: $xamlPath" return } $xamlString = Get-Content $xamlPath -Raw $reader = New-Object System.IO.StringReader($xamlString) $xmlReader = [System.Xml.XmlReader]::Create($reader) $window = [Windows.Markup.XamlReader]::Load($xmlReader) # Dynamic checkboxes for optional features in Windows Settings tab function UpdateOptionalFeaturesString { param( [psobject]$State ) $checkedFeatures = @() foreach ($entry in $State.Controls.featureCheckBoxes.GetEnumerator()) { if ($entry.Value.IsChecked) { $checkedFeatures += $entry.Key } } $State.Controls.txtOptionalFeatures.Text = $checkedFeatures -join ";" } function BuildFeaturesGrid { param ( [Parameter(Mandatory)] [System.Windows.FrameworkElement]$parent, [Parameter(Mandatory)] [array]$allowedFeatures # Pass the list of features explicitly ) $parent.Children.Clear() $script:uiState.Controls.featureCheckBoxes.Clear() # Clear the tracking hashtable $sortedFeatures = $allowedFeatures | Sort-Object $rows = 10 # Define number of rows for layout $columns = [math]::Ceiling($sortedFeatures.Count / $rows) $featuresGrid = New-Object System.Windows.Controls.Grid $featuresGrid.Margin = "0,5,0,5" $featuresGrid.ShowGridLines = $false # Define grid rows for ($r = 0; $r -lt $rows; $r++) { $rowDef = New-Object System.Windows.Controls.RowDefinition $rowDef.Height = [System.Windows.GridLength]::Auto $featuresGrid.RowDefinitions.Add($rowDef) | Out-Null } # Define grid columns for ($c = 0; $c -lt $columns; $c++) { $colDef = New-Object System.Windows.Controls.ColumnDefinition $colDef.Width = [System.Windows.GridLength]::Auto $featuresGrid.ColumnDefinitions.Add($colDef) | Out-Null } # Populate grid with checkboxes for ($i = 0; $i -lt $sortedFeatures.Count; $i++) { $featureName = $sortedFeatures[$i] $colIndex = [int]([math]::Floor($i / $rows)) $rowIndex = $i % $rows $chk = New-Object System.Windows.Controls.CheckBox $chk.Content = $featureName $chk.Margin = "5" $chk.Add_Checked({ UpdateOptionalFeaturesString -State $script:uiState }) $chk.Add_Unchecked({ UpdateOptionalFeaturesString -State $script:uiState }) $script:uiState.Controls.featureCheckBoxes[$featureName] = $chk # Track the checkbox [System.Windows.Controls.Grid]::SetRow($chk, $rowIndex) [System.Windows.Controls.Grid]::SetColumn($chk, $colIndex) $featuresGrid.Children.Add($chk) | Out-Null } $parent.Children.Add($featuresGrid) | Out-Null } # ----------------------------------------------------------------------------- # SECTION: Winget UI # ----------------------------------------------------------------------------- # Create data context class for version binding $script:uiState.Data.versionData = [PSCustomObject]@{ WingetVersion = "Not checked" ModuleVersion = "Not checked" } # Add observable property support $script:uiState.Data.versionData | Add-Member -MemberType ScriptMethod -Name NotifyPropertyChanged -Value { param($PropertyName) if ($this.PropertyChanged) { $this.PropertyChanged.Invoke($this, [System.ComponentModel.PropertyChangedEventArgs]::new($PropertyName)) } } $script:uiState.Data.versionData | Add-Member -MemberType NoteProperty -Name PropertyChanged -Value $null $script:uiState.Data.versionData | Add-Member -TypeName "System.ComponentModel.INotifyPropertyChanged" function Update-WingetVersionFields { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$wingetText, [Parameter(Mandatory)] [string]$moduleText ) # Force UI update on the UI thread $window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Normal, [Action] { $script:uiState.Controls.txtWingetVersion.Text = $wingetText $script:uiState.Controls.txtWingetModuleVersion.Text = $moduleText # Force immediate UI refresh [System.Windows.Forms.Application]::DoEvents() }) } # Add a function to create a sortable list view for Winget search results function Add-SortableColumn { param( [System.Windows.Controls.GridView]$gridView, [string]$header, [string]$binding, [int]$width = 'Auto', [bool]$isCheckbox = $false, [System.Windows.HorizontalAlignment]$headerHorizontalAlignment = [System.Windows.HorizontalAlignment]::Stretch ) $column = New-Object System.Windows.Controls.GridViewColumn $commonPadding = New-Object System.Windows.Thickness(5, 2, 5, 2) $headerControl = New-Object System.Windows.Controls.GridViewColumnHeader $headerControl.Tag = $binding # Used for sorting if ($isCheckbox) { # Cell template for a column of checkboxes $cellTemplate = New-Object System.Windows.DataTemplate $gridFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.Grid]) $checkBoxFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.CheckBox]) $checkBoxFactory.SetBinding([System.Windows.Controls.CheckBox]::IsCheckedProperty, (New-Object System.Windows.Data.Binding("IsSelected"))) $checkBoxFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Center) $checkBoxFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center) $checkBoxFactory.AddHandler([System.Windows.Controls.CheckBox]::ClickEvent, [System.Windows.RoutedEventHandler] { param($eventSourceLocal, $eventArgsLocal) # Sync logic would be needed here if this column had a header checkbox }) $gridFactory.AppendChild($checkBoxFactory) $cellTemplate.VisualTree = $gridFactory $column.CellTemplate = $cellTemplate # $column.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center # REMOVED } else { # For regular text columns $headerControl.HorizontalContentAlignment = $headerHorizontalAlignment $headerControl.Content = $header $headerTextElementFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBlock]) $headerTextElementFactory.SetValue([System.Windows.Controls.TextBlock]::TextProperty, $header) $headerTextBlockPadding = New-Object System.Windows.Thickness($commonPadding.Left, $commonPadding.Top, $commonPadding.Right, $commonPadding.Bottom) $headerTextElementFactory.SetValue([System.Windows.Controls.TextBlock]::PaddingProperty, $headerTextBlockPadding) $headerTextElementFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center) $headerDataTemplate = New-Object System.Windows.DataTemplate $headerDataTemplate.VisualTree = $headerTextElementFactory $headerControl.ContentTemplate = $headerDataTemplate $cellTemplate = New-Object System.Windows.DataTemplate $textBlockFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBlock]) $textBlockFactory.SetBinding([System.Windows.Controls.TextBlock]::TextProperty, (New-Object System.Windows.Data.Binding($binding))) # Adjust left padding to 0 for cell text to align with header text $cellTextBlockPadding = New-Object System.Windows.Thickness(0, $commonPadding.Top, $commonPadding.Right, $commonPadding.Bottom) $textBlockFactory.SetValue([System.Windows.Controls.TextBlock]::PaddingProperty, $cellTextBlockPadding) $textBlockFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Left) $textBlockFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center) $cellTemplate.VisualTree = $textBlockFactory $column.CellTemplate = $cellTemplate # $column.HorizontalContentAlignment = $headerHorizontalAlignment # REMOVED } $column.Header = $headerControl if ($width -ne 'Auto') { $column.Width = $width } $gridView.Columns.Add($column) } # Function to sort ListView items function Invoke-ListViewSort { param( [System.Windows.Controls.ListView]$listView, [string]$property ) # Toggle sort direction if clicking the same column if ($script:uiState.Flags.lastSortProperty -eq $property) { $script:uiState.Flags.lastSortAscending = -not $script:uiState.Flags.lastSortAscending } else { $script:uiState.Flags.lastSortAscending = $true } $script:uiState.Flags.lastSortProperty = $property # Get items from ItemsSource or Items collection $currentItemsSource = $listView.ItemsSource $itemsToSort = @() if ($null -ne $currentItemsSource) { $itemsToSort = @($currentItemsSource) } else { $itemsToSort = @($listView.Items) } if ($itemsToSort.Count -eq 0) { return } $selectedItems = @($itemsToSort | Where-Object { $_.IsSelected }) $unselectedItems = @($itemsToSort | Where-Object { -not $_.IsSelected }) # Define the primary sort criterion $primarySortDefinition = @{ Expression = { $val = $_.$property if ($null -eq $val) { '' } else { $val } } Ascending = $script:uiState.Flags.lastSortAscending } $sortCriteria = [System.Collections.Generic.List[hashtable]]::new() $sortCriteria.Add($primarySortDefinition) # Determine secondary sort property based on the ListView $secondarySortPropertyName = $null if ($listView.Name -eq 'lstDriverModels') { $secondarySortPropertyName = "Model" } elseif ($listView.Name -eq 'lstWingetResults') { $secondarySortPropertyName = "Name" } elseif ($listView.Name -eq 'lstAppsScriptVariables') { if ($property -eq "Key") { $secondarySortPropertyName = "Value" } elseif ($property -eq "Value") { $secondarySortPropertyName = "Key" } else { # Default secondary sort for IsSelected or other properties $secondarySortPropertyName = "Key" } } if ($null -ne $secondarySortPropertyName -and $property -ne $secondarySortPropertyName) { $itemsHaveSecondaryProperty = $false if ($unselectedItems.Count -gt 0) { if ($null -ne $unselectedItems[0].PSObject.Properties[$secondarySortPropertyName]) { $itemsHaveSecondaryProperty = $true } } elseif ($selectedItems.Count -gt 0) { if ($null -ne $selectedItems[0].PSObject.Properties[$secondarySortPropertyName]) { $itemsHaveSecondaryProperty = $true } } if ($itemsHaveSecondaryProperty) { # Create a scriptblock for the secondary sort expression dynamically $expressionScriptBlock = [scriptblock]::Create("`$_.$secondarySortPropertyName") $secondarySortDefinition = @{ Expression = { $val = Invoke-Command -ScriptBlock $expressionScriptBlock -ArgumentList $_ if ($null -eq $val) { '' } else { $val } } Ascending = $true # Secondary sort always ascending } $sortCriteria.Add($secondarySortDefinition) } } $sortedUnselected = $unselectedItems | Sort-Object -Property $sortCriteria.ToArray() # Ensure $sortedUnselected is not null before attempting to add its range if ($null -eq $sortedUnselected) { $sortedUnselected = @() } # Combine sorted items: selected items first, then sorted unselected items $newSortedList = [System.Collections.Generic.List[object]]::new() $newSortedList.AddRange($selectedItems) $newSortedList.AddRange($sortedUnselected) # Set the new sorted list as the ItemsSource # Try nulling out ItemsSource first to force a more complete refresh $listView.ItemsSource = $null $listView.ItemsSource = $newSortedList.ToArray() } # Function to add a selectable GridViewColumn with a "Select All" header CheckBox function Add-SelectableGridViewColumn { [CmdletBinding()] param( [Parameter(Mandatory)] [System.Windows.Controls.ListView]$ListView, [Parameter(Mandatory)] [string]$HeaderCheckBoxScriptVariableName, [Parameter(Mandatory)] [double]$ColumnWidth, [string]$IsSelectedPropertyName = "IsSelected" ) # Ensure the ListView has a GridView if ($null -eq $ListView.View -or -not ($ListView.View -is [System.Windows.Controls.GridView])) { WriteLog "Add-SelectableGridViewColumn: ListView '$($ListView.Name)' does not have a GridView or View is null. Cannot add column." # Optionally, create a new GridView if one doesn't exist, though XAML usually defines it. # $ListView.View = New-Object System.Windows.Controls.GridView return } $gridView = $ListView.View # Create the "Select All" CheckBox for the header $headerCheckBox = New-Object System.Windows.Controls.CheckBox $headerCheckBox.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Center # Store an object containing the IsSelectedPropertyName and the ListView's Name in the Tag $headerTagObject = [PSCustomObject]@{ PropertyName = $IsSelectedPropertyName ListViewName = $ListView.Name } $headerCheckBox.Tag = $headerTagObject # Removed debug WriteLog for storing tag data $headerCheckBox.Add_Checked({ param($senderCheckBoxLocal, $eventArgsCheckedLocal) $tagData = $senderCheckBoxLocal.Tag if ($null -eq $tagData -or -not $tagData.PSObject.Properties['PropertyName'] -or -not $tagData.PSObject.Properties['ListViewName']) { WriteLog "Add-SelectableGridViewColumn: CRITICAL - Tag data on header checkbox is missing, null, or malformed. Aborting HeaderChecked event." return } $localPropertyName = $tagData.PropertyName $localListViewName = $tagData.ListViewName # Removed debug WriteLog for HeaderChecked event fired if ([string]::IsNullOrEmpty($localPropertyName)) { WriteLog "Add-SelectableGridViewColumn: CRITICAL - PropertyName from Tag is null or empty in HeaderChecked event for ListView '$localListViewName'. Aborting." return } if ([string]::IsNullOrEmpty($localListViewName)) { WriteLog "Add-SelectableGridViewColumn: CRITICAL - ListViewName from Tag is null or empty in HeaderChecked event. Aborting." return } $actualListView = $script:uiState.Controls[$localListViewName] if ($null -eq $actualListView) { WriteLog "Add-SelectableGridViewColumn: CRITICAL - ListView control '$localListViewName' not found in window during HeaderChecked event. Aborting." return } # Removed debug WriteLog for successfully finding ListView in HeaderChecked $collectionToUpdate = $null if ($null -ne $actualListView.ItemsSource) { $collectionToUpdate = $actualListView.ItemsSource } elseif ($actualListView.HasItems) { $collectionToUpdate = $actualListView.Items } if ($null -ne $collectionToUpdate) { foreach ($item in $collectionToUpdate) { try { $item.($localPropertyName) = $true } catch { WriteLog "Error setting '$localPropertyName' to true for item in $($actualListView.Name): $($_.Exception.Message)" } } $actualListView.Items.Refresh() WriteLog "Header checkbox for $($actualListView.Name) checked. All items' '$localPropertyName' set to true." } }) $headerCheckBox.Add_Unchecked({ param($senderCheckBoxLocal, $eventArgsUncheckedLocal) $tagData = $senderCheckBoxLocal.Tag if ($null -eq $tagData -or -not $tagData.PSObject.Properties['PropertyName'] -or -not $tagData.PSObject.Properties['ListViewName']) { WriteLog "Add-SelectableGridViewColumn: CRITICAL - Tag data on header checkbox is missing, null, or malformed. Aborting HeaderUnchecked event." return } $localPropertyName = $tagData.PropertyName $localListViewName = $tagData.ListViewName # Removed debug WriteLog for HeaderUnchecked event fired if ([string]::IsNullOrEmpty($localPropertyName)) { WriteLog "Add-SelectableGridViewColumn: CRITICAL - PropertyName from Tag is null or empty in HeaderUnchecked event for ListView '$localListViewName'. Aborting." return } if ([string]::IsNullOrEmpty($localListViewName)) { WriteLog "Add-SelectableGridViewColumn: CRITICAL - ListViewName from Tag is null or empty in HeaderUnchecked event. Aborting." return } $actualListView = $script:uiState.Controls[$localListViewName] if ($null -eq $actualListView) { WriteLog "Add-SelectableGridViewColumn: CRITICAL - ListView control '$localListViewName' not found in window during HeaderUnchecked event. Aborting." return } # Removed debug WriteLog for successfully finding ListView in HeaderUnchecked # Only proceed if the uncheck was initiated by the user (IsChecked is explicitly false) if ($senderCheckBoxLocal.IsChecked -eq $false) { $collectionToUpdate = $null if ($null -ne $actualListView.ItemsSource) { $collectionToUpdate = $actualListView.ItemsSource } elseif ($actualListView.HasItems) { $collectionToUpdate = $actualListView.Items } if ($null -ne $collectionToUpdate) { foreach ($item in $collectionToUpdate) { try { $item.($localPropertyName) = $false } catch { WriteLog "Error setting '$localPropertyName' to false for item in $($actualListView.Name): $($_.Exception.Message)" } } $actualListView.Items.Refresh() WriteLog "Header checkbox for $($actualListView.Name) unchecked by user. All items' '$localPropertyName' set to false." } } }) # Store the header checkbox in a script-scoped variable Set-Variable -Name $HeaderCheckBoxScriptVariableName -Value $headerCheckBox -Scope Script -Force WriteLog "Add-SelectableGridViewColumn: Stored header checkbox in script variable '$HeaderCheckBoxScriptVariableName'." # Create the GridViewColumn $selectableColumn = New-Object System.Windows.Controls.GridViewColumn $selectableColumn.Header = $headerCheckBox $selectableColumn.Width = $ColumnWidth # Create the CellTemplate for item CheckBoxes $cellTemplate = New-Object System.Windows.DataTemplate # Use a Border to ensure CheckBox centers and stretches $borderFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.Border]) $borderFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch) $borderFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Stretch) $checkBoxFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.CheckBox]) $checkBoxFactory.SetBinding([System.Windows.Controls.CheckBox]::IsCheckedProperty, (New-Object System.Windows.Data.Binding($IsSelectedPropertyName))) $checkBoxFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Center) $checkBoxFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center) # Create an object to store both the header checkbox name and the ListView name $tagObject = [PSCustomObject]@{ HeaderCheckboxName = $HeaderCheckBoxScriptVariableName ListViewName = $ListView.Name # Store the name of the ListView } # Store this object in the Tag of each item checkbox $checkBoxFactory.SetValue([System.Windows.FrameworkElement]::TagProperty, $tagObject) # Add handler to update the header checkbox state when an item checkbox is clicked $checkBoxFactory.AddHandler([System.Windows.Controls.CheckBox]::ClickEvent, [System.Windows.RoutedEventHandler] { param($eventSourceLocal, $eventArgsLocal) $itemCheckBox = $eventSourceLocal -as [System.Windows.Controls.CheckBox] if ($null -eq $itemCheckBox) { WriteLog "Add-SelectableGridViewColumn: CRITICAL - Event source in item checkbox click handler is not a CheckBox." return } $tagData = $itemCheckBox.Tag if ($null -eq $tagData -or -not $tagData.PSObject.Properties['HeaderCheckboxName'] -or -not $tagData.PSObject.Properties['ListViewName']) { WriteLog "Add-SelectableGridViewColumn: Error - Tag data on itemCheckBox is missing or malformed." return } $headerCheckboxNameFromTag = $tagData.HeaderCheckboxName $listViewNameFromTag = $tagData.ListViewName WriteLog "Add-SelectableGridViewColumn: Item Click. ListView: '$listViewNameFromTag', HeaderChkName: '$headerCheckboxNameFromTag'" if ([string]::IsNullOrEmpty($headerCheckboxNameFromTag)) { WriteLog "Add-SelectableGridViewColumn: Error - Header checkbox name from Tag is null or empty for ListView '$listViewNameFromTag'." return } if ([string]::IsNullOrEmpty($listViewNameFromTag)) { WriteLog "Add-SelectableGridViewColumn: Error - ListView name from Tag is null or empty." return } # Retrieve the actual ListView control using its name stored in the Tag $targetListView = $script:uiState.Controls[$listViewNameFromTag] if ($null -eq $targetListView) { WriteLog "Add-SelectableGridViewColumn: Error - Could not find ListView control named '$listViewNameFromTag'." return } $headerChk = Get-Variable -Name $headerCheckboxNameFromTag -Scope Script -ValueOnly -ErrorAction SilentlyContinue if ($null -ne $headerChk) { Update-SelectAllHeaderCheckBoxState -ListView $targetListView -HeaderCheckBox $headerChk } else { WriteLog "Add-SelectableGridViewColumn: Error - Could not retrieve script variable for header checkbox named '$headerCheckboxNameFromTag' for ListView '$listViewNameFromTag'." } }) $borderFactory.AppendChild($checkBoxFactory) $cellTemplate.VisualTree = $borderFactory $selectableColumn.CellTemplate = $cellTemplate # Insert the new column at the beginning of the GridView $gridView.Columns.Insert(0, $selectableColumn) WriteLog "Add-SelectableGridViewColumn: Successfully added selectable column to '$($ListView.Name)'." } # Function to update the IsChecked state of a "Select All" header CheckBox function Update-SelectAllHeaderCheckBoxState { param( [Parameter(Mandatory)] [System.Windows.Controls.ListView]$ListView, [Parameter(Mandatory)] [System.Windows.Controls.CheckBox]$HeaderCheckBox ) $collectionToInspect = $null if ($null -ne $ListView.ItemsSource) { $collectionToInspect = @($ListView.ItemsSource) } elseif ($ListView.HasItems) { # Check if Items collection has items and ItemsSource is null $collectionToInspect = @($ListView.Items) } # If no items to inspect (either ItemsSource was null and Items was empty, or ItemsSource was empty) if ($null -eq $collectionToInspect -or $collectionToInspect.Count -eq 0) { $HeaderCheckBox.IsChecked = $false return } $selectedCount = ($collectionToInspect | Where-Object { $_.IsSelected }).Count $totalItemCount = $collectionToInspect.Count # Get the total count from the collection being inspected if ($totalItemCount -eq 0) { # Handle empty list case specifically $HeaderCheckBox.IsChecked = $false } elseif ($selectedCount -eq $totalItemCount) { $HeaderCheckBox.IsChecked = $true } elseif ($selectedCount -eq 0) { $HeaderCheckBox.IsChecked = $false } else { # Indeterminate state $HeaderCheckBox.IsChecked = $null } } # Function to update priorities sequentially in a ListView function Update-ListViewPriorities { param( [Parameter(Mandatory)] [System.Windows.Controls.ListView]$ListView ) $currentPriority = 1 foreach ($item in $ListView.Items) { if ($null -ne $item -and $item.PSObject.Properties['Priority']) { $item.Priority = $currentPriority $currentPriority++ } } $ListView.Items.Refresh() } # Function to move selected item to the top function Move-ListViewItemTop { param( [Parameter(Mandatory)] [System.Windows.Controls.ListView]$ListView ) $selectedItem = $ListView.SelectedItem if ($null -eq $selectedItem) { return } $currentIndex = $ListView.Items.IndexOf($selectedItem) if ($currentIndex -gt 0) { $ListView.Items.RemoveAt($currentIndex) $ListView.Items.Insert(0, $selectedItem) $ListView.SelectedItem = $selectedItem Update-ListViewPriorities -ListView $ListView } } # Function to move selected item up one position function Move-ListViewItemUp { param( [Parameter(Mandatory)] [System.Windows.Controls.ListView]$ListView ) $selectedItem = $ListView.SelectedItem if ($null -eq $selectedItem) { return } $currentIndex = $ListView.Items.IndexOf($selectedItem) if ($currentIndex -gt 0) { $ListView.Items.RemoveAt($currentIndex) $ListView.Items.Insert($currentIndex - 1, $selectedItem) $ListView.SelectedItem = $selectedItem Update-ListViewPriorities -ListView $ListView } } # Function to move selected item down one position function Move-ListViewItemDown { param( [Parameter(Mandatory)] [System.Windows.Controls.ListView]$ListView ) $selectedItem = $ListView.SelectedItem if ($null -eq $selectedItem) { return } $currentIndex = $ListView.Items.IndexOf($selectedItem) if ($currentIndex -lt ($ListView.Items.Count - 1)) { $ListView.Items.RemoveAt($currentIndex) $ListView.Items.Insert($currentIndex + 1, $selectedItem) $ListView.SelectedItem = $selectedItem Update-ListViewPriorities -ListView $ListView } } # Function to move selected item to the bottom function Move-ListViewItemBottom { param( [Parameter(Mandatory)] [System.Windows.Controls.ListView]$ListView ) $selectedItem = $ListView.SelectedItem if ($null -eq $selectedItem) { return } $currentIndex = $ListView.Items.IndexOf($selectedItem) if ($currentIndex -lt ($ListView.Items.Count - 1)) { $ListView.Items.RemoveAt($currentIndex) $ListView.Items.Add($selectedItem) $ListView.SelectedItem = $selectedItem Update-ListViewPriorities -ListView $ListView } } # Function to update the enabled state of the Copy Apps button function Update-CopyButtonState { param( [psobject]$State ) $listView = $State.Controls.lstApplications $copyButton = $State.Controls.btnCopyBYOApps if ($listView -and $copyButton) { $hasSource = $false foreach ($item in $listView.Items) { if ($null -ne $item -and $item.PSObject.Properties['Source'] -and -not [string]::IsNullOrWhiteSpace($item.Source)) { $hasSource = $true break } } $copyButton.IsEnabled = $hasSource } } # -------------------------------------------------------------------------- # SECTION: Parallel Processing # -------------------------------------------------------------------------- # -------------------------------------------------------------------------- $window.Add_Loaded({ # Assign UI elements to script variables $script:uiState.Window = $window $script:uiState.Controls.cmbWindowsRelease = $window.FindName('cmbWindowsRelease') $script:uiState.Controls.cmbWindowsVersion = $window.FindName('cmbWindowsVersion') $script:uiState.Controls.txtISOPath = $window.FindName('txtISOPath') $script:uiState.Controls.btnBrowseISO = $window.FindName('btnBrowseISO') $script:uiState.Controls.cmbWindowsArch = $window.FindName('cmbWindowsArch') $script:uiState.Controls.cmbWindowsLang = $window.FindName('cmbWindowsLang') $script:uiState.Controls.cmbWindowsSKU = $window.FindName('cmbWindowsSKU') $script:uiState.Controls.cmbMediaType = $window.FindName('cmbMediaType') $script:uiState.Controls.txtOptionalFeatures = $window.FindName('txtOptionalFeatures') $script:uiState.Controls.featuresPanel = $window.FindName('stackFeaturesContainer') $script:uiState.Controls.chkDownloadDrivers = $window.FindName('chkDownloadDrivers') $script:uiState.Controls.cmbMake = $window.FindName('cmbMake') # $script:uiState.Controls.cmbModel = $window.FindName('cmbModel') # cmbModel TextBox removed from XAML $script:uiState.Controls.spMakeSection = $window.FindName('spMakeSection') # Updated StackPanel name $script:uiState.Controls.btnGetModels = $window.FindName('btnGetModels') $script:uiState.Controls.spModelFilterSection = $window.FindName('spModelFilterSection') # New StackPanel for filter $script:uiState.Controls.txtModelFilter = $window.FindName('txtModelFilter') # New TextBox for filter $script:uiState.Controls.lstDriverModels = $window.FindName('lstDriverModels') # Set ListViewItem style to stretch content horizontally so cell templates fill the cell $itemStyleDriverModels = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem]) $itemStyleDriverModels.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch))) $script:uiState.Controls.lstDriverModels.ItemContainerStyle = $itemStyleDriverModels # Driver Models ListView setup $driverModelsGridView = New-Object System.Windows.Controls.GridView $script:uiState.Controls.lstDriverModels.View = $driverModelsGridView # Assign GridView to ListView first # Add the selectable column using the new function Add-SelectableGridViewColumn -ListView $script:uiState.Controls.lstDriverModels -HeaderCheckBoxScriptVariableName "chkSelectAllDriverModels" -ColumnWidth 70 # Add other sortable columns with left-aligned headers Add-SortableColumn -gridView $driverModelsGridView -header "Make" -binding "Make" -width 100 -headerHorizontalAlignment Left Add-SortableColumn -gridView $driverModelsGridView -header "Model" -binding "Model" -width 200 -headerHorizontalAlignment Left Add-SortableColumn -gridView $driverModelsGridView -header "Download Status" -binding "DownloadStatus" -width 150 -headerHorizontalAlignment Left $script:uiState.Controls.lstDriverModels.AddHandler( [System.Windows.Controls.GridViewColumnHeader]::ClickEvent, [System.Windows.RoutedEventHandler] { param($eventSource, $e) $header = $e.OriginalSource if ($header -is [System.Windows.Controls.GridViewColumnHeader] -and $header.Tag) { Invoke-ListViewSort -listView $script:uiState.Controls.lstDriverModels -property $header.Tag } } ) $script:uiState.Controls.spDriverActionButtons = $window.FindName('spDriverActionButtons') $script:uiState.Controls.btnSaveDriversJson = $window.FindName('btnSaveDriversJson') $script:uiState.Controls.btnImportDriversJson = $window.FindName('btnImportDriversJson') $script:uiState.Controls.btnDownloadSelectedDrivers = $window.FindName('btnDownloadSelectedDrivers') $script:uiState.Controls.btnClearDriverList = $window.FindName('btnClearDriverList') # New button $script:uiState.Controls.chkInstallOffice = $window.FindName('chkInstallOffice') $script:uiState.Controls.chkInstallApps = $window.FindName('chkInstallApps') $script:uiState.Controls.OfficePathStackPanel = $window.FindName('OfficePathStackPanel') $script:uiState.Controls.OfficePathGrid = $window.FindName('OfficePathGrid') $script:uiState.Controls.CopyOfficeConfigXMLStackPanel = $window.FindName('CopyOfficeConfigXMLStackPanel') $script:uiState.Controls.OfficeConfigurationXMLFileStackPanel = $window.FindName('OfficeConfigurationXMLFileStackPanel') $script:uiState.Controls.OfficeConfigurationXMLFileGrid = $window.FindName('OfficeConfigurationXMLFileGrid') $script:uiState.Controls.chkCopyOfficeConfigXML = $window.FindName('chkCopyOfficeConfigXML') $script:uiState.Controls.chkLatestCU = $window.FindName('chkUpdateLatestCU') $script:uiState.Controls.chkPreviewCU = $window.FindName('chkUpdatePreviewCU') $script:uiState.Controls.btnCheckUSBDrives = $window.FindName('btnCheckUSBDrives') $script:uiState.Controls.lstUSBDrives = $window.FindName('lstUSBDrives') $script:uiState.Controls.chkSelectAllUSBDrives = $window.FindName('chkSelectAllUSBDrives') $script:uiState.Controls.chkBuildUSBDriveEnable = $window.FindName('chkBuildUSBDriveEnable') $script:uiState.Controls.usbSection = $window.FindName('usbDriveSection') $script:uiState.Controls.chkSelectSpecificUSBDrives = $window.FindName('chkSelectSpecificUSBDrives') $script:uiState.Controls.usbSelectionPanel = $window.FindName('usbDriveSelectionPanel') $script:uiState.Controls.chkAllowExternalHardDiskMedia = $window.FindName('chkAllowExternalHardDiskMedia') $script:uiState.Controls.chkPromptExternalHardDiskMedia = $window.FindName('chkPromptExternalHardDiskMedia') $script:uiState.Controls.chkInstallWingetApps = $window.FindName('chkInstallWingetApps') $script:uiState.Controls.wingetPanel = $window.FindName('wingetPanel') $script:uiState.Controls.btnCheckWingetModule = $window.FindName('btnCheckWingetModule') $script:uiState.Controls.txtWingetVersion = $window.FindName('txtWingetVersion') $script:uiState.Controls.txtWingetModuleVersion = $window.FindName('txtWingetModuleVersion') $script:uiState.Controls.applicationPathPanel = $window.FindName('applicationPathPanel') $script:uiState.Controls.appListJsonPathPanel = $window.FindName('appListJsonPathPanel') $script:uiState.Controls.btnBrowseApplicationPath = $window.FindName('btnBrowseApplicationPath') $script:uiState.Controls.btnBrowseAppListJsonPath = $window.FindName('btnBrowseAppListJsonPath') $script:uiState.Controls.chkBringYourOwnApps = $window.FindName('chkBringYourOwnApps') $script:uiState.Controls.byoApplicationPanel = $window.FindName('byoApplicationPanel') $script:uiState.Controls.wingetSearchPanel = $window.FindName('wingetSearchPanel') $script:uiState.Controls.txtWingetSearch = $window.FindName('txtWingetSearch') $script:uiState.Controls.btnWingetSearch = $window.FindName('btnWingetSearch') $script:uiState.Controls.lstWingetResults = $window.FindName('lstWingetResults') # Set ListViewItem style to stretch content horizontally so cell templates fill the cell $itemStyleWingetResults = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem]) $itemStyleWingetResults.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch))) $script:uiState.Controls.lstWingetResults.ItemContainerStyle = $itemStyleWingetResults $script:uiState.Controls.btnSaveWingetList = $window.FindName('btnSaveWingetList') $script:uiState.Controls.btnImportWingetList = $window.FindName('btnImportWingetList') $script:uiState.Controls.btnClearWingetList = $window.FindName('btnClearWingetList') $script:uiState.Controls.btnDownloadSelected = $window.FindName('btnDownloadSelected') $script:uiState.Controls.btnBrowseAppSource = $window.FindName('btnBrowseAppSource') $script:uiState.Controls.btnBrowseFFUDevPath = $window.FindName('btnBrowseFFUDevPath') $script:uiState.Controls.btnBrowseFFUCaptureLocation = $window.FindName('btnBrowseFFUCaptureLocation') $script:uiState.Controls.btnBrowseOfficePath = $window.FindName('btnBrowseOfficePath') $script:uiState.Controls.btnBrowseDriversFolder = $window.FindName('btnBrowseDriversFolder') $script:uiState.Controls.btnBrowsePEDriversFolder = $window.FindName('btnBrowsePEDriversFolder') $script:uiState.Controls.btnAddApplication = $window.FindName('btnAddApplication') $script:uiState.Controls.btnSaveBYOApplications = $window.FindName('btnSaveBYOApplications') $script:uiState.Controls.btnLoadBYOApplications = $window.FindName('btnLoadBYOApplications') $script:uiState.Controls.btnClearBYOApplications = $window.FindName('btnClearBYOApplications') $script:uiState.Controls.btnCopyBYOApps = $window.FindName('btnCopyBYOApps') $script:uiState.Controls.lstApplications = $window.FindName('lstApplications') $script:uiState.Controls.btnMoveTop = $window.FindName('btnMoveTop') $script:uiState.Controls.btnMoveUp = $window.FindName('btnMoveUp') $script:uiState.Controls.btnMoveDown = $window.FindName('btnMoveDown') $script:uiState.Controls.btnMoveBottom = $window.FindName('btnMoveBottom') $script:uiState.Controls.txtStatus = $window.FindName('txtStatus') # Assign txtStatus control # Assign Progress Bar and Overall Status Text controls to script variables $script:uiState.Controls.pbOverallProgress = $window.FindName('progressBar') # Use the correct x:Name from XAML $script:uiState.Controls.txtOverallStatus = $window.FindName('txtStatus') # Use the correct x:Name from XAML (assuming it's txtStatus) $script:uiState.Controls.cmbVMSwitchName = $window.FindName('cmbVMSwitchName') $script:uiState.Controls.txtVMHostIPAddress = $window.FindName('txtVMHostIPAddress') $script:uiState.Controls.txtCustomVMSwitchName = $window.FindName('txtCustomVMSwitchName') $script:uiState.Controls.txtFFUDevPath = $window.FindName('txtFFUDevPath') $script:uiState.Controls.txtCustomFFUNameTemplate = $window.FindName('txtCustomFFUNameTemplate') $script:uiState.Controls.txtFFUCaptureLocation = $window.FindName('txtFFUCaptureLocation') $script:uiState.Controls.txtShareName = $window.FindName('txtShareName') $script:uiState.Controls.txtUsername = $window.FindName('txtUsername') $script:uiState.Controls.chkCompactOS = $window.FindName('chkCompactOS') $script:uiState.Controls.chkOptimize = $window.FindName('chkOptimize') $script:uiState.Controls.chkAllowVHDXCaching = $window.FindName('chkAllowVHDXCaching') $script:uiState.Controls.chkCreateCaptureMedia = $window.FindName('chkCreateCaptureMedia') $script:uiState.Controls.chkCreateDeploymentMedia = $window.FindName('chkCreateDeploymentMedia') $script:uiState.Controls.chkCopyAutopilot = $window.FindName('chkCopyAutopilot') $script:uiState.Controls.chkCopyUnattend = $window.FindName('chkCopyUnattend') $script:uiState.Controls.chkCopyPPKG = $window.FindName('chkCopyPPKG') $script:uiState.Controls.chkCleanupAppsISO = $window.FindName('chkCleanupAppsISO') $script:uiState.Controls.chkCleanupCaptureISO = $window.FindName('chkCleanupCaptureISO') $script:uiState.Controls.chkCleanupDeployISO = $window.FindName('chkCleanupDeployISO') $script:uiState.Controls.chkCleanupDrivers = $window.FindName('chkCleanupDrivers') $script:uiState.Controls.chkRemoveFFU = $window.FindName('chkRemoveFFU') $script:uiState.Controls.txtDiskSize = $window.FindName('txtDiskSize') $script:uiState.Controls.txtMemory = $window.FindName('txtMemory') $script:uiState.Controls.txtProcessors = $window.FindName('txtProcessors') $script:uiState.Controls.txtVMLocation = $window.FindName('txtVMLocation') $script:uiState.Controls.txtVMNamePrefix = $window.FindName('txtVMNamePrefix') $script:uiState.Controls.cmbLogicalSectorSize = $window.FindName('cmbLogicalSectorSize') $script:uiState.Controls.txtProductKey = $window.FindName('txtProductKey') $script:uiState.Controls.txtOfficePath = $window.FindName('txtOfficePath') $script:uiState.Controls.txtOfficeConfigXMLFilePath = $window.FindName('txtOfficeConfigXMLFilePath') $script:uiState.Controls.txtDriversFolder = $window.FindName('txtDriversFolder') $script:uiState.Controls.txtPEDriversFolder = $window.FindName('txtPEDriversFolder') $script:uiState.Controls.chkCopyPEDrivers = $window.FindName('chkCopyPEDrivers') $script:uiState.Controls.chkUpdateLatestCU = $window.FindName('chkUpdateLatestCU') $script:uiState.Controls.chkUpdateLatestNet = $window.FindName('chkUpdateLatestNet') $script:uiState.Controls.chkUpdateLatestDefender = $window.FindName('chkUpdateLatestDefender') $script:uiState.Controls.chkUpdateEdge = $window.FindName('chkUpdateEdge') $script:uiState.Controls.chkUpdateOneDrive = $window.FindName('chkUpdateOneDrive') $script:uiState.Controls.chkUpdateLatestMSRT = $window.FindName('chkUpdateLatestMSRT') $script:uiState.Controls.chkUpdatePreviewCU = $window.FindName('chkUpdatePreviewCU') $script:uiState.Controls.txtApplicationPath = $window.FindName('txtApplicationPath') $script:uiState.Controls.txtAppListJsonPath = $window.FindName('txtAppListJsonPath') # Assign Driver Checkboxes $script:uiState.Controls.chkInstallDrivers = $window.FindName('chkInstallDrivers') $script:uiState.Controls.chkCopyDrivers = $window.FindName('chkCopyDrivers') $script:uiState.Controls.chkCompressDriversToWIM = $window.FindName('chkCompressDriversToWIM') $script:uiState.Controls.chkRemoveApps = $window.FindName('chkRemoveApps') $script:uiState.Controls.chkRemoveUpdates = $window.FindName('chkRemoveUpdates') $script:uiState.Controls.chkUpdateLatestMicrocode = $window.FindName('chkUpdateLatestMicrocode') # AppsScriptVariables Controls $script:uiState.Controls.chkDefineAppsScriptVariables = $window.FindName('chkDefineAppsScriptVariables') $script:uiState.Controls.appsScriptVariablesPanel = $window.FindName('appsScriptVariablesPanel') $script:uiState.Controls.txtAppsScriptKey = $window.FindName('txtAppsScriptKey') $script:uiState.Controls.txtAppsScriptValue = $window.FindName('txtAppsScriptValue') $script:uiState.Controls.btnAddAppsScriptVariable = $window.FindName('btnAddAppsScriptVariable') $script:uiState.Controls.lstAppsScriptVariables = $window.FindName('lstAppsScriptVariables') # Bind ItemsSource to the data list $script:uiState.Controls.lstAppsScriptVariables.ItemsSource = $script:uiState.Data.appsScriptVariablesDataList.ToArray() # Set ListViewItem style to stretch content horizontally so cell templates fill the cell $itemStyleAppsScriptVars = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem]) $itemStyleAppsScriptVars.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch))) $script:uiState.Controls.lstAppsScriptVariables.ItemContainerStyle = $itemStyleAppsScriptVars # The GridView for lstAppsScriptVariables is defined in XAML. We need to get it and add the column. if ($script:uiState.Controls.lstAppsScriptVariables.View -is [System.Windows.Controls.GridView]) { Add-SelectableGridViewColumn -ListView $script:uiState.Controls.lstAppsScriptVariables -HeaderCheckBoxScriptVariableName "chkSelectAllAppsScriptVariables" -ColumnWidth 60 # Make Key and Value columns sortable $appsScriptVarsGridView = $script:uiState.Controls.lstAppsScriptVariables.View # Key Column (should be at index 1 after selectable column is inserted at 0) if ($appsScriptVarsGridView.Columns.Count -gt 1) { $keyColumn = $appsScriptVarsGridView.Columns[1] $keyHeader = New-Object System.Windows.Controls.GridViewColumnHeader $keyHeader.Content = "Key" $keyHeader.Tag = "Key" # Property to sort by $keyHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left $keyColumn.Header = $keyHeader } # Value Column (should be at index 2) if ($appsScriptVarsGridView.Columns.Count -gt 2) { $valueColumn = $appsScriptVarsGridView.Columns[2] $valueHeader = New-Object System.Windows.Controls.GridViewColumnHeader $valueHeader.Content = "Value" $valueHeader.Tag = "Value" # Property to sort by $valueHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left $valueColumn.Header = $valueHeader } # Add Click event handler for sorting $script:uiState.Controls.lstAppsScriptVariables.AddHandler( [System.Windows.Controls.GridViewColumnHeader]::ClickEvent, [System.Windows.RoutedEventHandler] { param($eventSource, $e) $header = $e.OriginalSource if ($header -is [System.Windows.Controls.GridViewColumnHeader] -and $header.Tag) { Invoke-ListViewSort -listView $script:uiState.Controls.lstAppsScriptVariables -property $header.Tag } } ) } else { WriteLog "Warning: lstAppsScriptVariables.View is not a GridView. Selectable column not added, and sorting cannot be enabled." } $script:uiState.Controls.btnRemoveSelectedAppsScriptVariables = $window.FindName('btnRemoveSelectedAppsScriptVariables') # Updated variable name $script:uiState.Controls.btnClearAppsScriptVariables = $window.FindName('btnClearAppsScriptVariables') # Get Windows Settings defaults and lists from helper module $script:uiState.Defaults.windowsSettingsDefaults = Get-WindowsSettingsDefaults # Get General defaults from helper module $script:uiState.Defaults.generalDefaults = Get-GeneralDefaults -FFUDevelopmentPath $FFUDevelopmentPath # Initialize Windows Settings UI using data from helper module Refresh-WindowsSettingsCombos -isoPath $script:uiState.Defaults.windowsSettingsDefaults.DefaultISOPath # Use combined refresh function $script:uiState.Controls.txtISOPath.Add_TextChanged({ Refresh-WindowsSettingsCombos -isoPath $script:uiState.Controls.txtISOPath.Text }) $script:uiState.Controls.cmbWindowsRelease.Add_SelectionChanged({ $selectedReleaseValue = 11 # Default if null if ($null -ne $script:uiState.Controls.cmbWindowsRelease.SelectedItem) { $selectedReleaseValue = $script:uiState.Controls.cmbWindowsRelease.SelectedItem.Value } # Only need to update the Version combo when Release changes Update-WindowsVersionCombo -selectedRelease $selectedReleaseValue -isoPath $script:uiState.Controls.txtISOPath.Text # Also update the SKU combo (now derives values internally) Update-WindowsSkuCombo }) $script:uiState.Controls.btnBrowseISO.Add_Click({ $ofd = New-Object System.Windows.Forms.OpenFileDialog $ofd.Filter = "ISO files (*.iso)|*.iso" $ofd.Title = "Select Windows ISO File" if ($ofd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $script:uiState.Controls.txtISOPath.Text = $ofd.FileName } }) # Populate static combos from defaults object $script:uiState.Controls.cmbWindowsArch.ItemsSource = $script:uiState.Defaults.windowsSettingsDefaults.AllowedArchitectures $script:uiState.Controls.cmbWindowsArch.SelectedItem = $script:uiState.Defaults.windowsSettingsDefaults.DefaultWindowsArch $script:uiState.Controls.cmbWindowsLang.ItemsSource = $script:uiState.Defaults.windowsSettingsDefaults.AllowedLanguages $script:uiState.Controls.cmbWindowsLang.SelectedItem = $script:uiState.Defaults.windowsSettingsDefaults.DefaultWindowsLang # $script:uiState.Controls.cmbWindowsSKU.ItemsSource is now populated by Update-WindowsSkuCombo $script:uiState.Controls.cmbWindowsSKU.SelectedItem = $script:uiState.Defaults.windowsSettingsDefaults.DefaultWindowsSKU # Attempt to set default $script:uiState.Controls.cmbMediaType.ItemsSource = $script:uiState.Defaults.windowsSettingsDefaults.AllowedMediaTypes $script:uiState.Controls.cmbMediaType.SelectedItem = $script:uiState.Defaults.windowsSettingsDefaults.DefaultMediaType # Set default text values for Windows Settings $script:uiState.Controls.txtOptionalFeatures.Text = $script:uiState.Defaults.windowsSettingsDefaults.DefaultOptionalFeatures $window.FindName('txtProductKey').Text = $script:uiState.Defaults.windowsSettingsDefaults.DefaultProductKey # Build tab defaults from General Defaults $window.FindName('txtFFUDevPath').Text = $FFUDevelopmentPath # Keep this as it's the base path $window.FindName('txtCustomFFUNameTemplate').Text = $script:uiState.Defaults.generalDefaults.CustomFFUNameTemplate $window.FindName('txtFFUCaptureLocation').Text = $script:uiState.Defaults.generalDefaults.FFUCaptureLocation $window.FindName('txtShareName').Text = $script:uiState.Defaults.generalDefaults.ShareName $window.FindName('txtUsername').Text = $script:uiState.Defaults.generalDefaults.Username $window.FindName('chkBuildUSBDriveEnable').IsChecked = $script:uiState.Defaults.generalDefaults.BuildUSBDriveEnable $window.FindName('chkCompactOS').IsChecked = $script:uiState.Defaults.generalDefaults.CompactOS $script:uiState.Controls.chkUpdateADK = $window.FindName('chkUpdateADK') # Assign chkUpdateADK $script:uiState.Controls.chkUpdateADK.IsChecked = $script:uiState.Defaults.generalDefaults.UpdateADK # Set default for chkUpdateADK $window.FindName('chkOptimize').IsChecked = $script:uiState.Defaults.generalDefaults.Optimize $window.FindName('chkAllowVHDXCaching').IsChecked = $script:uiState.Defaults.generalDefaults.AllowVHDXCaching $window.FindName('chkCreateCaptureMedia').IsChecked = $script:uiState.Defaults.generalDefaults.CreateCaptureMedia $window.FindName('chkCreateDeploymentMedia').IsChecked = $script:uiState.Defaults.generalDefaults.CreateDeploymentMedia $window.FindName('chkAllowExternalHardDiskMedia').IsChecked = $script:uiState.Defaults.generalDefaults.AllowExternalHardDiskMedia $window.FindName('chkPromptExternalHardDiskMedia').IsChecked = $script:uiState.Defaults.generalDefaults.PromptExternalHardDiskMedia $window.FindName('chkSelectSpecificUSBDrives').IsChecked = $script:uiState.Defaults.generalDefaults.SelectSpecificUSBDrives $window.FindName('chkCopyAutopilot').IsChecked = $script:uiState.Defaults.generalDefaults.CopyAutopilot $window.FindName('chkCopyUnattend').IsChecked = $script:uiState.Defaults.generalDefaults.CopyUnattend $window.FindName('chkCopyPPKG').IsChecked = $script:uiState.Defaults.generalDefaults.CopyPPKG $window.FindName('chkCleanupAppsISO').IsChecked = $script:uiState.Defaults.generalDefaults.CleanupAppsISO $window.FindName('chkCleanupCaptureISO').IsChecked = $script:uiState.Defaults.generalDefaults.CleanupCaptureISO $window.FindName('chkCleanupDeployISO').IsChecked = $script:uiState.Defaults.generalDefaults.CleanupDeployISO $window.FindName('chkCleanupDrivers').IsChecked = $script:uiState.Defaults.generalDefaults.CleanupDrivers $window.FindName('chkRemoveFFU').IsChecked = $script:uiState.Defaults.generalDefaults.RemoveFFU $script:uiState.Controls.chkRemoveApps.IsChecked = $script:uiState.Defaults.generalDefaults.RemoveApps $script:uiState.Controls.chkRemoveUpdates.IsChecked = $script:uiState.Defaults.generalDefaults.RemoveUpdates # Hyper-V Settings defaults from General Defaults $window.FindName('txtDiskSize').Text = $script:uiState.Defaults.generalDefaults.DiskSizeGB $window.FindName('txtMemory').Text = $script:uiState.Defaults.generalDefaults.MemoryGB $window.FindName('txtProcessors').Text = $script:uiState.Defaults.generalDefaults.Processors $window.FindName('txtVMLocation').Text = $script:uiState.Defaults.generalDefaults.VMLocation $window.FindName('txtVMNamePrefix').Text = $script:uiState.Defaults.generalDefaults.VMNamePrefix $window.FindName('cmbLogicalSectorSize').SelectedItem = ($window.FindName('cmbLogicalSectorSize').Items | Where-Object { $_.Content -eq $script:uiState.Defaults.generalDefaults.LogicalSectorSize.ToString() }) # Hyper-V Settings: Populate VM Switch ComboBox (Keep existing logic) $vmSwitchData = Get-VMSwitchData $script:uiState.Data.vmSwitchMap = $vmSwitchData.SwitchMap $script:uiState.Controls.cmbVMSwitchName.Items.Clear() foreach ($switchName in $vmSwitchData.SwitchNames) { $script:uiState.Controls.cmbVMSwitchName.Items.Add($switchName) | Out-Null } $script:uiState.Controls.cmbVMSwitchName.Items.Add('Other') | Out-Null if ($script:uiState.Controls.cmbVMSwitchName.Items.Count -gt 1) { $script:uiState.Controls.cmbVMSwitchName.SelectedIndex = 0 $firstSwitch = $script:uiState.Controls.cmbVMSwitchName.SelectedItem if ($script:uiState.Data.vmSwitchMap.ContainsKey($firstSwitch)) { $script:uiState.Controls.txtVMHostIPAddress.Text = $script:uiState.Data.vmSwitchMap[$firstSwitch] } else { $script:uiState.Controls.txtVMHostIPAddress.Text = $script:uiState.Defaults.generalDefaults.VMHostIPAddress # Use default if IP not found } $script:uiState.Controls.txtCustomVMSwitchName.Visibility = 'Collapsed' } else { $script:uiState.Controls.cmbVMSwitchName.SelectedItem = 'Other' $script:uiState.Controls.txtCustomVMSwitchName.Visibility = 'Visible' $script:uiState.Controls.txtVMHostIPAddress.Text = $script:uiState.Defaults.generalDefaults.VMHostIPAddress # Use default } $script:uiState.Controls.cmbVMSwitchName.Add_SelectionChanged({ param($eventSource, $selectionChangedEventArgs) $selectedItem = $eventSource.SelectedItem if ($selectedItem -eq 'Other') { $script:uiState.Controls.txtCustomVMSwitchName.Visibility = 'Visible' $script:uiState.Controls.txtVMHostIPAddress.Text = '' # Clear IP for custom } else { $script:uiState.Controls.txtCustomVMSwitchName.Visibility = 'Collapsed' if ($script:uiState.Data.vmSwitchMap.ContainsKey($selectedItem)) { $script:uiState.Controls.txtVMHostIPAddress.Text = $script:uiState.Data.vmSwitchMap[$selectedItem] } else { $script:uiState.Controls.txtVMHostIPAddress.Text = '' # Clear IP if not found in map } } }) # Updates tab defaults from General Defaults $window.FindName('chkUpdateLatestCU').IsChecked = $script:uiState.Defaults.generalDefaults.UpdateLatestCU $window.FindName('chkUpdateLatestNet').IsChecked = $script:uiState.Defaults.generalDefaults.UpdateLatestNet $window.FindName('chkUpdateLatestDefender').IsChecked = $script:uiState.Defaults.generalDefaults.UpdateLatestDefender $window.FindName('chkUpdateEdge').IsChecked = $script:uiState.Defaults.generalDefaults.UpdateEdge $window.FindName('chkUpdateOneDrive').IsChecked = $script:uiState.Defaults.generalDefaults.UpdateOneDrive $window.FindName('chkUpdateLatestMSRT').IsChecked = $script:uiState.Defaults.generalDefaults.UpdateLatestMSRT $script:uiState.Controls.chkUpdateLatestMicrocode.IsChecked = $script:uiState.Defaults.generalDefaults.UpdateLatestMicrocode # Added for UpdateLatestMicrocode $window.FindName('chkUpdatePreviewCU').IsChecked = $script:uiState.Defaults.generalDefaults.UpdatePreviewCU # Applications tab defaults from General Defaults $window.FindName('chkInstallApps').IsChecked = $script:uiState.Defaults.generalDefaults.InstallApps $window.FindName('txtApplicationPath').Text = $script:uiState.Defaults.generalDefaults.ApplicationPath $window.FindName('txtAppListJsonPath').Text = $script:uiState.Defaults.generalDefaults.AppListJsonPath $window.FindName('chkInstallWingetApps').IsChecked = $script:uiState.Defaults.generalDefaults.InstallWingetApps $window.FindName('chkBringYourOwnApps').IsChecked = $script:uiState.Defaults.generalDefaults.BringYourOwnApps # M365 Apps/Office tab defaults from General Defaults $window.FindName('chkInstallOffice').IsChecked = $script:uiState.Defaults.generalDefaults.InstallOffice $window.FindName('txtOfficePath').Text = $script:uiState.Defaults.generalDefaults.OfficePath $window.FindName('chkCopyOfficeConfigXML').IsChecked = $script:uiState.Defaults.generalDefaults.CopyOfficeConfigXML $window.FindName('txtOfficeConfigXMLFilePath').Text = $script:uiState.Defaults.generalDefaults.OfficeConfigXMLFilePath # Drivers tab defaults from General Defaults $window.FindName('txtDriversFolder').Text = $script:uiState.Defaults.generalDefaults.DriversFolder $window.FindName('txtPEDriversFolder').Text = $script:uiState.Defaults.generalDefaults.PEDriversFolder $script:uiState.Controls.txtDriversJsonPath = $window.FindName('txtDriversJsonPath') # Assign new TextBox $script:uiState.Controls.txtDriversJsonPath.Text = $script:uiState.Defaults.generalDefaults.DriversJsonPath # Set default text $script:uiState.Controls.btnBrowseDriversJsonPath = $window.FindName('btnBrowseDriversJsonPath') # Assign new Button $window.FindName('chkDownloadDrivers').IsChecked = $script:uiState.Defaults.generalDefaults.DownloadDrivers $window.FindName('chkInstallDrivers').IsChecked = $script:uiState.Defaults.generalDefaults.InstallDrivers $window.FindName('chkCopyDrivers').IsChecked = $script:uiState.Defaults.generalDefaults.CopyDrivers $window.FindName('chkCopyPEDrivers').IsChecked = $script:uiState.Defaults.generalDefaults.CopyPEDrivers # Drivers tab UI logic (Keep existing logic) $makeList = @('Microsoft', 'Dell', 'HP', 'Lenovo') # Added Lenovo foreach ($m in $makeList) { [void]$script:uiState.Controls.cmbMake.Items.Add($m) } if ($script:uiState.Controls.cmbMake.Items.Count -gt 0) { $script:uiState.Controls.cmbMake.SelectedIndex = 0 } $script:uiState.Controls.chkDownloadDrivers.Add_Checked({ $script:uiState.Controls.cmbMake.Visibility = 'Visible' $script:uiState.Controls.btnGetModels.Visibility = 'Visible' $script:uiState.Controls.spMakeSection.Visibility = 'Visible' # Make the model filter, list, and action buttons visible immediately # This allows users to import a Drivers.json without first clicking "Get Models" $script:uiState.Controls.spModelFilterSection.Visibility = 'Visible' $script:uiState.Controls.lstDriverModels.Visibility = 'Visible' $script:uiState.Controls.spDriverActionButtons.Visibility = 'Visible' }) $script:uiState.Controls.chkDownloadDrivers.Add_Unchecked({ $script:uiState.Controls.cmbMake.Visibility = 'Collapsed' $script:uiState.Controls.btnGetModels.Visibility = 'Collapsed' $script:uiState.Controls.spMakeSection.Visibility = 'Collapsed' $script:uiState.Controls.spModelFilterSection.Visibility = 'Collapsed' $script:uiState.Controls.lstDriverModels.Visibility = 'Collapsed' $script:uiState.Controls.spDriverActionButtons.Visibility = 'Collapsed' $script:uiState.Controls.lstDriverModels.ItemsSource = $null $script:uiState.Data.allDriverModels = @() $script:uiState.Controls.txtModelFilter.Text = "" }) $script:uiState.Controls.spMakeSection.Visibility = if ($script:uiState.Controls.chkDownloadDrivers.IsChecked) { 'Visible' } else { 'Collapsed' } $script:uiState.Controls.btnGetModels.Visibility = if ($script:uiState.Controls.chkDownloadDrivers.IsChecked) { 'Visible' } else { 'Collapsed' } $script:uiState.Controls.spModelFilterSection.Visibility = 'Collapsed' $script:uiState.Controls.lstDriverModels.Visibility = 'Collapsed' $script:uiState.Controls.spDriverActionButtons.Visibility = 'Collapsed' $script:uiState.Controls.btnGetModels.Add_Click({ $selectedMake = $script:uiState.Controls.cmbMake.SelectedItem $script:uiState.Controls.txtStatus.Text = "Getting models for $selectedMake..." $window.Cursor = [System.Windows.Input.Cursors]::Wait $this.IsEnabled = $false # Disable the button try { # Get previously selected models from the master list ($script:uiState.Data.allDriverModels) # This ensures all selected items are captured, regardless of any active filter. $previouslySelectedModels = @($script:uiState.Data.allDriverModels | Where-Object { $_.IsSelected }) # Get newly fetched models for the current make (already standardized) $newlyFetchedStandardizedModels = Get-ModelsForMake -SelectedMake $selectedMake -State $script:uiState $combinedModelsList = [System.Collections.Generic.List[PSCustomObject]]::new() $modelIdentifiersInCombinedList = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) # Add previously selected models first to preserve their selection state and order (if any) foreach ($item in $previouslySelectedModels) { $combinedModelsList.Add($item) # Use a composite key of Make and Model for uniqueness tracking $modelIdentifiersInCombinedList.Add("$($item.Make)::$($item.Model)") | Out-Null } # Add newly fetched models if they are not already in the combined list (based on Make::Model identifier) $addedNewCount = 0 foreach ($item in $newlyFetchedStandardizedModels) { if (-not $modelIdentifiersInCombinedList.Contains("$($item.Make)::$($item.Model)")) { $combinedModelsList.Add($item) # Add to HashSet to prevent duplicates if the new list itself has them (though Get-ModelsForMake should try to avoid this) $modelIdentifiersInCombinedList.Add("$($item.Make)::$($item.Model)") | Out-Null $addedNewCount++ } } $script:uiState.Data.allDriverModels = $combinedModelsList.ToArray() | Sort-Object @{Expression = { $_.IsSelected }; Descending = $true }, Make, Model # Sort by selection status, then Make, then Model $script:uiState.Controls.lstDriverModels.ItemsSource = $script:uiState.Data.allDriverModels $script:uiState.Controls.txtModelFilter.Text = "" # Clear any existing filter if ($script:uiState.Data.allDriverModels.Count -gt 0) { $script:uiState.Controls.spModelFilterSection.Visibility = 'Visible' $script:uiState.Controls.lstDriverModels.Visibility = 'Visible' $script:uiState.Controls.spDriverActionButtons.Visibility = 'Visible' $statusText = "Displaying $($script:uiState.Data.allDriverModels.Count) models." if ($newlyFetchedStandardizedModels.Count -gt 0 -and $addedNewCount -eq 0 -and $previouslySelectedModels.Count -gt 0) { # This case means new models were fetched, but all were already present in the selected list. $statusText = "Fetched $($newlyFetchedStandardizedModels.Count) models for $selectedMake; all were already in the selected list. Displaying $($script:uiState.Data.allDriverModels.Count) total selected models." } elseif ($addedNewCount -gt 0) { $statusText = "Added $addedNewCount new models for $selectedMake. Displaying $($script:uiState.Data.allDriverModels.Count) total models." } elseif ($newlyFetchedStandardizedModels.Count -eq 0 -and $selectedMake -eq 'Lenovo' ) { # Handled Lenovo specific no new models found message inside Get-ModelsForMake or if user cancelled prompt $statusText = if ($previouslySelectedModels.Count -gt 0) { "No new models found for $selectedMake. Displaying $($previouslySelectedModels.Count) previously selected models." } else { "No models found for $selectedMake." } } elseif ($newlyFetchedStandardizedModels.Count -eq 0) { $statusText = "No new models found for $selectedMake. Displaying $($script:uiState.Data.allDriverModels.Count) previously selected models." } $script:uiState.Controls.txtStatus.Text = $statusText } else { $script:uiState.Controls.spModelFilterSection.Visibility = 'Collapsed' $script:uiState.Controls.lstDriverModels.Visibility = 'Collapsed' $script:uiState.Controls.spDriverActionButtons.Visibility = 'Collapsed' $script:uiState.Controls.txtStatus.Text = "No models to display for $selectedMake." } } # End Try catch { $script:uiState.Controls.txtStatus.Text = "Error getting models: $($_.Exception.Message)" [System.Windows.MessageBox]::Show("Error getting models: $($_.Exception.Message)", "Error", "OK", "Error") # Minimal UI reset on error, keep previously selected if any if ($null -eq $script:uiState.Data.allDriverModels -or $script:uiState.Data.allDriverModels.Count -eq 0) { $script:uiState.Controls.spModelFilterSection.Visibility = 'Collapsed' $script:uiState.Controls.lstDriverModels.Visibility = 'Collapsed' $script:uiState.Controls.spDriverActionButtons.Visibility = 'Collapsed' $script:uiState.Controls.lstDriverModels.ItemsSource = $null $script:uiState.Controls.txtModelFilter.Text = "" } } # End Catch finally { $window.Cursor = $null $this.IsEnabled = $true # Re-enable the button } # End Finally }) $script:uiState.Controls.txtModelFilter.Add_TextChanged({ param($sourceObject, $textChangedEventArgs) Filter-DriverModels -filterText $script:uiState.Controls.txtModelFilter.Text }) $script:uiState.Controls.btnDownloadSelectedDrivers.Add_Click({ param($buttonSender, $clickEventArgs) $selectedDrivers = @($script:uiState.Controls.lstDriverModels.Items | Where-Object { $_.IsSelected }) if (-not $selectedDrivers) { [System.Windows.MessageBox]::Show("No drivers selected to download.", "Download Drivers", "OK", "Information") return } $buttonSender.IsEnabled = $false $script:uiState.Controls.pbOverallProgress.Visibility = 'Visible' $script:uiState.Controls.pbOverallProgress.Value = 0 $script:uiState.Controls.txtStatus.Text = "Preparing driver downloads..." # Define common necessary task-specific variables locally $localDriversFolder = $window.FindName('txtDriversFolder').Text $localWindowsRelease = $window.FindName('cmbWindowsRelease').SelectedItem.Value $localWindowsArch = $window.FindName('cmbWindowsArch').SelectedItem $localHeaders = $Headers # Use script-level variable $localUserAgent = $UserAgent # Use script-level variable $compressDrivers = $script:uiState.Controls.chkCompressDriversToWIM.IsChecked # Define common necessary task-specific variables locally # Ensure required selections are made if ($null -eq $window.FindName('cmbWindowsRelease').SelectedItem) { [System.Windows.MessageBox]::Show("Please select a Windows Release.", "Missing Information", "OK", "Warning") $buttonSender.IsEnabled = $true $script:uiState.Controls.pbOverallProgress.Visibility = 'Collapsed' $script:uiState.Controls.txtStatus.Text = "Driver download cancelled." return } if ($null -eq $window.FindName('cmbWindowsArch').SelectedItem) { [System.Windows.MessageBox]::Show("Please select a Windows Architecture.", "Missing Information", "OK", "Warning") $buttonSender.IsEnabled = $true $script:uiState.Controls.pbOverallProgress.Visibility = 'Collapsed' $script:uiState.Controls.txtStatus.Text = "Driver download cancelled." return } if (($selectedDrivers | Where-Object { $_.Make -eq 'HP' }) -and $null -eq $window.FindName('cmbWindowsVersion').SelectedItem) { [System.Windows.MessageBox]::Show("HP drivers are selected. Please select a Windows Version.", "Missing Information", "OK", "Warning") $buttonSender.IsEnabled = $true $script:uiState.Controls.pbOverallProgress.Visibility = 'Collapsed' $script:uiState.Controls.txtStatus.Text = "Driver download cancelled." return } $localDriversFolder = $window.FindName('txtDriversFolder').Text $localWindowsRelease = $window.FindName('cmbWindowsRelease').SelectedItem.Value $localWindowsArch = $window.FindName('cmbWindowsArch').SelectedItem $localWindowsVersion = if ($null -ne $window.FindName('cmbWindowsVersion').SelectedItem) { $window.FindName('cmbWindowsVersion').SelectedItem } else { $null } $localHeaders = $Headers # Use script-level variable $localUserAgent = $UserAgent # Use script-level variable $compressDrivers = $script:uiState.Controls.chkCompressDriversToWIM.IsChecked # --- Dell Catalog Handling (once, if Dell drivers are selected) --- $dellCatalogXmlPath = $null # This will be the path passed to the background task if ($selectedDrivers | Where-Object { $_.Make -eq 'Dell' }) { $script:uiState.Controls.txtStatus.Text = "Checking Dell Catalog..." WriteLog "Dell drivers selected. Preparing Dell catalog..." $dellDriversFolderUi = Join-Path -Path $localDriversFolder -ChildPath "Dell" $catalogBaseName = if ($localWindowsRelease -le 11) { "CatalogPC" } else { "Catalog" } $dellCabFileUi = Join-Path -Path $dellDriversFolderUi -ChildPath "$($catalogBaseName).cab" # This $dellCatalogXmlPath is the one we ensure exists and is up-to-date for the Save-DellDriversTask $dellCatalogXmlPath = Join-Path -Path $dellDriversFolderUi -ChildPath "$($catalogBaseName).xml" $catalogUrl = if ($localWindowsRelease -le 11) { "http://downloads.dell.com/catalog/CatalogPC.cab" } else { "https://downloads.dell.com/catalog/Catalog.cab" } $downloadDellCatalog = $true if (Test-Path -Path $dellCatalogXmlPath -PathType Leaf) { if (((Get-Date) - (Get-Item $dellCatalogXmlPath).LastWriteTime).TotalDays -lt 7) { WriteLog "Using existing Dell Catalog XML (less than 7 days old) for download task: $dellCatalogXmlPath" $downloadDellCatalog = $false $script:uiState.Controls.txtStatus.Text = "Dell Catalog ready." } else { WriteLog "Existing Dell Catalog XML '$dellCatalogXmlPath' is older than 7 days." } } else { WriteLog "Dell Catalog XML '$dellCatalogXmlPath' not found." } if ($downloadDellCatalog) { WriteLog "Dell Catalog XML '$dellCatalogXmlPath' needs to be downloaded/updated for driver download task." $script:uiState.Controls.txtStatus.Text = "Downloading Dell Catalog..." try { # Ensure Dell drivers folder exists if (-not (Test-Path -Path $dellDriversFolderUi -PathType Container)) { WriteLog "Creating Dell drivers folder: $dellDriversFolderUi" New-Item -Path $dellDriversFolderUi -ItemType Directory -Force | Out-Null } if (Test-Path $dellCabFileUi) { Remove-Item $dellCabFileUi -Force -ErrorAction SilentlyContinue } if (Test-Path $dellCatalogXmlPath) { Remove-Item $dellCatalogXmlPath -Force -ErrorAction SilentlyContinue } # Using Start-BitsTransferWithRetry and Invoke-Process (available from FFUUI.Core.psm1) Start-BitsTransferWithRetry -Source $catalogUrl -Destination $dellCabFileUi WriteLog "Dell Catalog CAB downloaded to $dellCabFileUi" Invoke-Process -FilePath "Expand.exe" -ArgumentList """$dellCabFileUi"" ""$dellCatalogXmlPath""" | Out-Null WriteLog "Dell Catalog XML extracted to $dellCatalogXmlPath" Remove-Item -Path $dellCabFileUi -Force -ErrorAction SilentlyContinue WriteLog "Dell Catalog CAB file $dellCabFileUi deleted." $script:uiState.Controls.txtStatus.Text = "Dell Catalog ready." } catch { $errMsg = "Failed to download/extract Dell Catalog for driver download task: $($_.Exception.Message)" WriteLog $errMsg; [System.Windows.MessageBox]::Show($errMsg, "Dell Catalog Error", "OK", "Error") $dellCatalogXmlPath = $null # Ensure it's null if failed, Save-DellDriversTask will handle this $script:uiState.Controls.txtStatus.Text = "Dell Catalog download failed. Dell drivers may not download." } } # If $downloadDellCatalog was false, $dellCatalogXmlPath is already set to the existing valid XML. } # --- End Dell Catalog Handling --- $script:uiState.Controls.txtStatus.Text = "Processing all selected drivers..." WriteLog "Processing all selected drivers: $($selectedDrivers.Model -join ', ')" $taskArguments = @{ DriversFolder = $localDriversFolder WindowsRelease = $localWindowsRelease WindowsArch = $localWindowsArch WindowsVersion = $localWindowsVersion # Will be null if not applicable (e.g., not HP) Headers = $localHeaders UserAgent = $localUserAgent CompressToWim = $compressDrivers DellCatalogXmlPath = $dellCatalogXmlPath # Will be null if not Dell or if Dell catalog prep failed } Invoke-ParallelProcessing -ItemsToProcess $selectedDrivers ` -ListViewControl $script:uiState.Controls.lstDriverModels ` -IdentifierProperty 'Model' ` -StatusProperty 'DownloadStatus' ` -TaskType 'DownloadDriverByMake' ` -TaskArguments $taskArguments ` -CompletedStatusText 'Completed' ` -ErrorStatusPrefix 'Error: ' ` -WindowObject $window ` -MainThreadLogPath $script:uiState.LogFilePath $overallSuccess = $true # Check if any item has an error status after processing # We iterate over $script:lstDriverModels.Items because their DownloadStatus property was updated by Invoke-ParallelProcessing foreach ($item in ($script:uiState.Controls.lstDriverModels.Items | Where-Object { $_.IsSelected })) { # Check only originally selected items if ($item.DownloadStatus -like 'Error:*') { $overallSuccess = $false WriteLog "Error detected for model $($item.Model) (Make: $($item.Make)): $($item.DownloadStatus)" # No break here, log all errors } } $script:uiState.Controls.pbOverallProgress.Visibility = 'Collapsed' $buttonSender.IsEnabled = $true if ($overallSuccess) { $script:uiState.Controls.txtStatus.Text = "All selected driver downloads processed." [System.Windows.MessageBox]::Show("All selected driver downloads processed. Check status column for details.", "Download Process Finished", "OK", "Information") } else { $script:uiState.Controls.txtStatus.Text = "Driver downloads processed with some errors. Check status column and log." [System.Windows.MessageBox]::Show("Driver downloads processed, but some errors occurred. Please check the status column for each driver and the log file for details.", "Download Process Finished with Errors", "OK", "Warning") } }) $script:uiState.Controls.btnClearDriverList.Add_Click({ $script:uiState.Controls.lstDriverModels.ItemsSource = $null $script:uiState.Data.allDriverModels = @() $script:uiState.Controls.txtModelFilter.Text = "" $script:uiState.Controls.txtStatus.Text = "Driver list cleared." }) $script:uiState.Controls.btnSaveDriversJson.Add_Click({ Save-DriversJson }) $script:uiState.Controls.btnImportDriversJson.Add_Click({ Import-DriversJson }) # Office interplay (Keep existing logic) $script:uiState.Flags.installAppsCheckedByOffice = $false if ($script:uiState.Controls.chkInstallOffice.IsChecked) { $script:uiState.Controls.OfficePathStackPanel.Visibility = 'Visible' $script:uiState.Controls.OfficePathGrid.Visibility = 'Visible' $script:uiState.Controls.CopyOfficeConfigXMLStackPanel.Visibility = 'Visible' # Show/hide XML file path based on checkbox state $script:uiState.Controls.OfficeConfigurationXMLFileStackPanel.Visibility = if ($script:uiState.Controls.chkCopyOfficeConfigXML.IsChecked) { 'Visible' } else { 'Collapsed' } $script:uiState.Controls.OfficeConfigurationXMLFileGrid.Visibility = if ($script:uiState.Controls.chkCopyOfficeConfigXML.IsChecked) { 'Visible' } else { 'Collapsed' } } else { $script:uiState.Controls.OfficePathStackPanel.Visibility = 'Collapsed' $script:uiState.Controls.OfficePathGrid.Visibility = 'Collapsed' $script:uiState.Controls.CopyOfficeConfigXMLStackPanel.Visibility = 'Collapsed' $script:uiState.Controls.OfficeConfigurationXMLFileStackPanel.Visibility = 'Collapsed' $script:uiState.Controls.OfficeConfigurationXMLFileGrid.Visibility = 'Collapsed' } $script:uiState.Controls.chkInstallOffice.Add_Checked({ if (-not $script:uiState.Controls.chkInstallApps.IsChecked) { $script:uiState.Controls.chkInstallApps.IsChecked = $true $script:uiState.Flags.installAppsCheckedByOffice = $true } $script:uiState.Controls.chkInstallApps.IsEnabled = $false $script:uiState.Controls.OfficePathStackPanel.Visibility = 'Visible' $script:uiState.Controls.OfficePathGrid.Visibility = 'Visible' $script:uiState.Controls.CopyOfficeConfigXMLStackPanel.Visibility = 'Visible' # Show/hide XML file path based on checkbox state $script:uiState.Controls.OfficeConfigurationXMLFileStackPanel.Visibility = if ($script:uiState.Controls.chkCopyOfficeConfigXML.IsChecked) { 'Visible' } else { 'Collapsed' } $script:uiState.Controls.OfficeConfigurationXMLFileGrid.Visibility = if ($script:uiState.Controls.chkCopyOfficeConfigXML.IsChecked) { 'Visible' } else { 'Collapsed' } }) $script:uiState.Controls.chkInstallOffice.Add_Unchecked({ if ($script:uiState.Flags.installAppsCheckedByOffice) { $script:uiState.Controls.chkInstallApps.IsChecked = $false $script:uiState.Flags.installAppsCheckedByOffice = $false } # Only re-enable InstallApps if not forced by Updates if (-not $script:uiState.Flags.installAppsForcedByUpdates) { $script:uiState.Controls.chkInstallApps.IsEnabled = $true } $script:uiState.Controls.OfficePathStackPanel.Visibility = 'Collapsed' $script:uiState.Controls.OfficePathGrid.Visibility = 'Collapsed' $script:uiState.Controls.CopyOfficeConfigXMLStackPanel.Visibility = 'Collapsed' $script:uiState.Controls.OfficeConfigurationXMLFileStackPanel.Visibility = 'Collapsed' $script:uiState.Controls.OfficeConfigurationXMLFileGrid.Visibility = 'Collapsed' }) $script:uiState.Controls.chkCopyOfficeConfigXML.Add_Checked({ $script:uiState.Controls.OfficeConfigurationXMLFileStackPanel.Visibility = 'Visible' $script:uiState.Controls.OfficeConfigurationXMLFileGrid.Visibility = 'Visible' }) $script:uiState.Controls.chkCopyOfficeConfigXML.Add_Unchecked({ $script:uiState.Controls.OfficeConfigurationXMLFileStackPanel.Visibility = 'Collapsed' $script:uiState.Controls.OfficeConfigurationXMLFileGrid.Visibility = 'Collapsed' }) # Build dynamic multi-column checkboxes for optional features (Keep existing logic) if ($script:featuresPanel) { BuildFeaturesGrid -parent $script:featuresPanel -allowedFeatures $script:windowsSettingsDefaults.AllowedFeatures } # Updates/InstallApps interplay (Keep existing logic) $script:uiState.Flags.installAppsForcedByUpdates = $false $script:uiState.Flags.prevInstallAppsStateBeforeUpdates = $null # Define the scriptblock within the Loaded event and assign it to the state object $script:uiState.Controls.UpdateInstallAppsBasedOnUpdates = { param($State) # Pass state object to avoid using $script: scope inside $anyUpdateChecked = $State.Controls.chkUpdateLatestDefender.IsChecked -or $State.Controls.chkUpdateEdge.IsChecked -or $State.Controls.chkUpdateOneDrive.IsChecked -or $State.Controls.chkUpdateLatestMSRT.IsChecked if ($anyUpdateChecked) { if (-not $State.Flags.installAppsForcedByUpdates) { $State.Flags.prevInstallAppsStateBeforeUpdates = $State.Controls.chkInstallApps.IsChecked $State.Flags.installAppsForcedByUpdates = $true } $State.Controls.chkInstallApps.IsChecked = $true $State.Controls.chkInstallApps.IsEnabled = $false } else { if ($State.Flags.installAppsForcedByUpdates) { $State.Controls.chkInstallApps.IsChecked = $State.Flags.prevInstallAppsStateBeforeUpdates $State.Flags.installAppsForcedByUpdates = $false $State.Flags.prevInstallAppsStateBeforeUpdates = $null } # Only re-enable InstallApps if not forced by Office if (-not $State.Controls.chkInstallOffice.IsChecked) { $State.Controls.chkInstallApps.IsEnabled = $true } } } $window.FindName('chkUpdateLatestDefender').Add_Checked({ & $script:uiState.Controls.UpdateInstallAppsBasedOnUpdates -State $script:uiState }) $window.FindName('chkUpdateLatestDefender').Add_Unchecked({ & $script:uiState.Controls.UpdateInstallAppsBasedOnUpdates -State $script:uiState }) $window.FindName('chkUpdateEdge').Add_Checked({ & $script:uiState.Controls.UpdateInstallAppsBasedOnUpdates -State $script:uiState }) $window.FindName('chkUpdateEdge').Add_Unchecked({ & $script:uiState.Controls.UpdateInstallAppsBasedOnUpdates -State $script:uiState }) $window.FindName('chkUpdateOneDrive').Add_Checked({ & $script:uiState.Controls.UpdateInstallAppsBasedOnUpdates -State $script:uiState }) $window.FindName('chkUpdateOneDrive').Add_Unchecked({ & $script:uiState.Controls.UpdateInstallAppsBasedOnUpdates -State $script:uiState }) $window.FindName('chkUpdateLatestMSRT').Add_Checked({ & $script:uiState.Controls.UpdateInstallAppsBasedOnUpdates -State $script:uiState }) $window.FindName('chkUpdateLatestMSRT').Add_Unchecked({ & $script:uiState.Controls.UpdateInstallAppsBasedOnUpdates -State $script:uiState }) # Initial check for Updates/InstallApps state & $script:uiState.Controls.UpdateInstallAppsBasedOnUpdates -State $script:uiState # CU interplay (Keep existing logic) $script:uiState.Controls.chkLatestCU.Add_Checked({ $script:uiState.Controls.chkPreviewCU.IsEnabled = $false }) $script:uiState.Controls.chkLatestCU.Add_Unchecked({ $script:uiState.Controls.chkPreviewCU.IsEnabled = $true }) $script:uiState.Controls.chkPreviewCU.Add_Checked({ $script:uiState.Controls.chkLatestCU.IsEnabled = $false }) $script:uiState.Controls.chkPreviewCU.Add_Unchecked({ $script:uiState.Controls.chkLatestCU.IsEnabled = $true }) # Set initial state based on defaults $script:uiState.Controls.chkPreviewCU.IsEnabled = -not $script:uiState.Controls.chkLatestCU.IsChecked $script:uiState.Controls.chkLatestCU.IsEnabled = -not $script:uiState.Controls.chkPreviewCU.IsChecked # USB Drive Detection/Selection logic (Keep existing logic) $script:uiState.Controls.btnCheckUSBDrives.Add_Click({ $script:uiState.Controls.lstUSBDrives.Items.Clear() $usbDrives = Get-USBDrives foreach ($drive in $usbDrives) { $script:uiState.Controls.lstUSBDrives.Items.Add([PSCustomObject]$drive) } if ($script:uiState.Controls.lstUSBDrives.Items.Count -gt 0) { $script:uiState.Controls.lstUSBDrives.SelectedIndex = 0 } }) $script:uiState.Controls.chkSelectAllUSBDrives.Add_Checked({ foreach ($item in $script:uiState.Controls.lstUSBDrives.Items) { $item.IsSelected = $true } $script:uiState.Controls.lstUSBDrives.Items.Refresh() }) $script:uiState.Controls.chkSelectAllUSBDrives.Add_Unchecked({ foreach ($item in $script:uiState.Controls.lstUSBDrives.Items) { $item.IsSelected = $false } $script:uiState.Controls.lstUSBDrives.Items.Refresh() }) $script:uiState.Controls.lstUSBDrives.Add_KeyDown({ param($eventSource, $keyEvent) if ($keyEvent.Key -eq 'Space') { $selectedItem = $script:uiState.Controls.lstUSBDrives.SelectedItem if ($selectedItem) { $selectedItem.IsSelected = !$selectedItem.IsSelected $script:uiState.Controls.lstUSBDrives.Items.Refresh() $allSelected = -not ($script:uiState.Controls.lstUSBDrives.Items | Where-Object { -not $_.IsSelected }) $script:uiState.Controls.chkSelectAllUSBDrives.IsChecked = $allSelected } } }) $script:uiState.Controls.lstUSBDrives.Add_SelectionChanged({ param($eventSource, $selChangeEvent) $allSelected = -not ($script:uiState.Controls.lstUSBDrives.Items | Where-Object { -not $_.IsSelected }) $script:uiState.Controls.chkSelectAllUSBDrives.IsChecked = $allSelected }) $script:uiState.Controls.usbSection.Visibility = if ($script:uiState.Controls.chkBuildUSBDriveEnable.IsChecked) { 'Visible' } else { 'Collapsed' } $script:uiState.Controls.usbSelectionPanel.Visibility = if ($script:uiState.Controls.chkSelectSpecificUSBDrives.IsChecked) { 'Visible' } else { 'Collapsed' } $script:uiState.Controls.chkBuildUSBDriveEnable.Add_Checked({ $script:uiState.Controls.usbSection.Visibility = 'Visible' $script:uiState.Controls.chkSelectSpecificUSBDrives.IsEnabled = $true }) $script:uiState.Controls.chkBuildUSBDriveEnable.Add_Unchecked({ $script:uiState.Controls.usbSection.Visibility = 'Collapsed' $script:uiState.Controls.chkSelectSpecificUSBDrives.IsEnabled = $false $script:uiState.Controls.chkSelectSpecificUSBDrives.IsChecked = $false $script:uiState.Controls.lstUSBDrives.Items.Clear() $script:uiState.Controls.chkSelectAllUSBDrives.IsChecked = $false }) $script:uiState.Controls.chkSelectSpecificUSBDrives.Add_Checked({ $script:uiState.Controls.usbSelectionPanel.Visibility = 'Visible' }) $script:uiState.Controls.chkSelectSpecificUSBDrives.Add_Unchecked({ $script:uiState.Controls.usbSelectionPanel.Visibility = 'Collapsed' $script:uiState.Controls.lstUSBDrives.Items.Clear() $script:uiState.Controls.chkSelectAllUSBDrives.IsChecked = $false }) $script:uiState.Controls.chkSelectSpecificUSBDrives.IsEnabled = $script:uiState.Controls.chkBuildUSBDriveEnable.IsChecked $script:uiState.Controls.chkAllowExternalHardDiskMedia.Add_Checked({ $script:uiState.Controls.chkPromptExternalHardDiskMedia.IsEnabled = $true }) $script:uiState.Controls.chkAllowExternalHardDiskMedia.Add_Unchecked({ $script:uiState.Controls.chkPromptExternalHardDiskMedia.IsEnabled = $false $script:uiState.Controls.chkPromptExternalHardDiskMedia.IsChecked = $false }) # Set initial state based on defaults $script:uiState.Controls.chkPromptExternalHardDiskMedia.IsEnabled = $script:uiState.Controls.chkAllowExternalHardDiskMedia.IsChecked # APPLICATIONS tab UI logic (Keep existing logic) $script:uiState.Controls.chkInstallWingetApps.Visibility = if ($script:uiState.Controls.chkInstallApps.IsChecked) { 'Visible' } else { 'Collapsed' } $script:uiState.Controls.applicationPathPanel.Visibility = if ($script:uiState.Controls.chkInstallApps.IsChecked) { 'Visible' } else { 'Collapsed' } $script:uiState.Controls.appListJsonPathPanel.Visibility = if ($script:uiState.Controls.chkInstallApps.IsChecked) { 'Visible' } else { 'Collapsed' } $script:uiState.Controls.chkBringYourOwnApps.Visibility = if ($script:uiState.Controls.chkInstallApps.IsChecked) { 'Visible' } else { 'Collapsed' } $script:uiState.Controls.byoApplicationPanel.Visibility = if ($script:uiState.Controls.chkBringYourOwnApps.IsChecked) { 'Visible' } else { 'Collapsed' } $script:uiState.Controls.wingetPanel.Visibility = if ($script:uiState.Controls.chkInstallWingetApps.IsChecked) { 'Visible' } else { 'Collapsed' } $script:uiState.Controls.wingetSearchPanel.Visibility = 'Collapsed' # Keep search hidden initially $script:uiState.Controls.chkInstallApps.Add_Checked({ $script:uiState.Controls.chkInstallWingetApps.Visibility = 'Visible' $script:uiState.Controls.applicationPathPanel.Visibility = 'Visible' $script:uiState.Controls.appListJsonPathPanel.Visibility = 'Visible' $script:uiState.Controls.chkBringYourOwnApps.Visibility = 'Visible' # New logic for AppsScriptVariables $script:uiState.Controls.chkDefineAppsScriptVariables.Visibility = 'Visible' }) $script:uiState.Controls.chkInstallApps.Add_Unchecked({ $script:uiState.Controls.chkInstallWingetApps.IsChecked = $false # Uncheck children when parent is unchecked $script:uiState.Controls.chkBringYourOwnApps.IsChecked = $false $script:uiState.Controls.chkInstallWingetApps.Visibility = 'Collapsed' $script:uiState.Controls.applicationPathPanel.Visibility = 'Collapsed' $script:uiState.Controls.appListJsonPathPanel.Visibility = 'Collapsed' $script:uiState.Controls.chkBringYourOwnApps.Visibility = 'Collapsed' $script:uiState.Controls.wingetPanel.Visibility = 'Collapsed' $script:uiState.Controls.wingetSearchPanel.Visibility = 'Collapsed' $script:uiState.Controls.byoApplicationPanel.Visibility = 'Collapsed' # New logic for AppsScriptVariables $script:uiState.Controls.chkDefineAppsScriptVariables.IsChecked = $false # Also uncheck it $script:uiState.Controls.chkDefineAppsScriptVariables.Visibility = 'Collapsed' $script:uiState.Controls.appsScriptVariablesPanel.Visibility = 'Collapsed' # Ensure panel is hidden }) $script:uiState.Controls.btnBrowseApplicationPath.Add_Click({ $selectedPath = Show-ModernFolderPicker -Title "Select Application Path Folder" if ($selectedPath) { $window.FindName('txtApplicationPath').Text = $selectedPath } }) $script:uiState.Controls.btnBrowseAppListJsonPath.Add_Click({ $ofd = New-Object System.Windows.Forms.OpenFileDialog $ofd.Filter = "JSON files (*.json)|*.json" $ofd.Title = "Select AppList.json File" $ofd.CheckFileExists = $false if ($ofd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $window.FindName('txtAppListJsonPath').Text = $ofd.FileName } }) $script:uiState.Controls.chkBringYourOwnApps.Add_Checked({ $script:uiState.Controls.byoApplicationPanel.Visibility = 'Visible' }) $script:uiState.Controls.chkBringYourOwnApps.Add_Unchecked({ $script:uiState.Controls.byoApplicationPanel.Visibility = 'Collapsed' # Clear fields when hiding $window.FindName('txtAppName').Text = '' $window.FindName('txtAppCommandLine').Text = '' $window.FindName('txtAppArguments').Text = '' $window.FindName('txtAppSource').Text = '' }) $script:uiState.Controls.chkInstallWingetApps.Add_Checked({ $script:uiState.Controls.wingetPanel.Visibility = 'Visible' }) $script:uiState.Controls.chkInstallWingetApps.Add_Unchecked({ $script:uiState.Controls.wingetPanel.Visibility = 'Collapsed' $script:uiState.Controls.wingetSearchPanel.Visibility = 'Collapsed' # Hide search when unchecked }) $script:uiState.Controls.btnCheckWingetModule.Add_Click({ param($buttonSender, $clickEventArgs) $buttonSender.IsEnabled = $false $window.Cursor = [System.Windows.Input.Cursors]::Wait # Initial UI update before calling the core function Update-WingetVersionFields -wingetText "Checking..." -moduleText "Checking..." $statusResult = $null try { # Call the Core function to perform checks and potential install/update # Pass the UI update function as a callback $statusResult = Confirm-WingetInstallationUI -UiUpdateCallback { param($wingetText, $moduleText) Update-WingetVersionFields -wingetText $wingetText -moduleText $moduleText } # Display appropriate message based on the result if ($statusResult.Success -and $statusResult.UpdateAttempted) { # Update attempted and successful [System.Windows.MessageBox]::Show("Winget components installed/updated successfully.", "Winget Installation Complete", "OK", "Information") } elseif (-not $statusResult.Success) { # Error occurred $errorMessage = if (-not [string]::IsNullOrWhiteSpace($statusResult.Message)) { $statusResult.Message } else { "An unknown error occurred during Winget check/install." } [System.Windows.MessageBox]::Show($errorMessage, "Winget Error", "OK", "Error") } # If Winget components were already up-to-date ($statusResult.Success -eq $true -and $statusResult.UpdateAttempted -eq $false), no message box is shown. # Show search panel only if the final status is successful and checkbox is still checked if ($statusResult.Success -and $script:uiState.Controls.chkInstallWingetApps.IsChecked) { $script:uiState.Controls.wingetSearchPanel.Visibility = 'Visible' } else { $script:uiState.Controls.wingetSearchPanel.Visibility = 'Collapsed' # Hide if not successful or unchecked } } catch { # Catch errors from the Confirm-WingetInstallationUI call itself (less likely now) Update-WingetVersionFields -wingetText "Error" -moduleText "Error" [System.Windows.MessageBox]::Show("Unexpected error checking/installing Winget components: $($_.Exception.Message)", "Error", "OK", "Error") $script:uiState.Controls.wingetSearchPanel.Visibility = 'Collapsed' # Ensure search is hidden on error } finally { $buttonSender.IsEnabled = $true $window.Cursor = $null } }) # Winget Search ListView setup $wingetGridView = New-Object System.Windows.Controls.GridView $script:uiState.Controls.lstWingetResults.View = $wingetGridView # Assign GridView to ListView first # Add the selectable column using the new function Add-SelectableGridViewColumn -ListView $script:uiState.Controls.lstWingetResults -HeaderCheckBoxScriptVariableName "chkSelectAllWingetResults" -ColumnWidth 60 # Add other sortable columns with left-aligned headers Add-SortableColumn -gridView $wingetGridView -header "Name" -binding "Name" -width 200 -headerHorizontalAlignment Left Add-SortableColumn -gridView $wingetGridView -header "Id" -binding "Id" -width 200 -headerHorizontalAlignment Left Add-SortableColumn -gridView $wingetGridView -header "Version" -binding "Version" -width 100 -headerHorizontalAlignment Left Add-SortableColumn -gridView $wingetGridView -header "Source" -binding "Source" -width 100 -headerHorizontalAlignment Left Add-SortableColumn -gridView $wingetGridView -header "Download Status" -binding "DownloadStatus" -width 150 -headerHorizontalAlignment Left $script:uiState.Controls.lstWingetResults.AddHandler( [System.Windows.Controls.GridViewColumnHeader]::ClickEvent, [System.Windows.RoutedEventHandler] { param($eventSource, $e) $header = $e.OriginalSource if ($header -is [System.Windows.Controls.GridViewColumnHeader] -and $header.Tag) { Invoke-ListViewSort -listView $script:uiState.Controls.lstWingetResults -property $header.Tag } } ) $script:uiState.Controls.btnWingetSearch.Add_Click({ Search-WingetApps }) $script:uiState.Controls.txtWingetSearch.Add_KeyDown({ param($eventSrc, $keyEvent) if ($keyEvent.Key -eq 'Return') { Search-WingetApps; $keyEvent.Handled = $true } }) $script:uiState.Controls.btnSaveWingetList.Add_Click({ Save-WingetList }) $script:uiState.Controls.btnImportWingetList.Add_Click({ Import-WingetList }) $script:uiState.Controls.btnClearWingetList.Add_Click({ $script:uiState.Controls.lstWingetResults.ItemsSource = @() # Set ItemsSource to an empty array $script:uiState.Controls.txtWingetSearch.Text = "" if ($script:uiState.Controls.txtStatus) { $script:uiState.Controls.txtStatus.Text = "Cleared all applications from the list" } }) # -------------------------------------------------------------------------- # SECTION: Background Task Management (Using ForEach-Object -Parallel) # -------------------------------------------------------------------------- # Modules (UI_Helpers, BackgroundTasks) and Scripts (WingetFunctions) are imported/dot-sourced # directly into the main script scope. ForEach-Object -Parallel automatically handles # module/variable availability in the parallel threads. # UI updates are handled by calling helper functions directly on the main UI thread # after the parallel processing completes. # -------------------------------------------------------------------------- $script:uiState.Controls.btnDownloadSelected.Add_Click({ param($buttonSender, $clickEventArgs) $selectedApps = $script:uiState.Controls.lstWingetResults.Items | Where-Object { $_.IsSelected } if (-not $selectedApps) { [System.Windows.MessageBox]::Show("No applications selected to download.", "Download Winget Apps", "OK", "Information") return } $buttonSender.IsEnabled = $false $script:uiState.Controls.pbOverallProgress.Visibility = 'Visible' $script:uiState.Controls.pbOverallProgress.Value = 0 $script:uiState.Controls.txtStatus.Text = "Starting Winget app downloads..." # Define necessary task-specific variables locally $localAppsPath = $window.FindName('txtApplicationPath').Text $localAppListJsonPath = $window.FindName('txtAppListJsonPath').Text $localWindowsArch = $window.FindName('cmbWindowsArch').SelectedItem $localOrchestrationPath = Join-Path -Path $window.FindName('txtApplicationPath').Text -ChildPath "Orchestration" # Create hashtable for task-specific arguments to pass to Invoke-ParallelProcessing $taskArguments = @{ AppsPath = $localAppsPath AppListJsonPath = $localAppListJsonPath WindowsArch = $localWindowsArch OrchestrationPath = $localOrchestrationPath } # Select only necessary properties before passing to Invoke-ParallelProcessing $itemsToProcess = $selectedApps | Select-Object Name, Id, Source, Version # Include Version if needed # Invoke the centralized parallel processing function # Pass task type and task-specific arguments Invoke-ParallelProcessing -ItemsToProcess $itemsToProcess ` -ListViewControl $script:uiState.Controls.lstWingetResults ` -IdentifierProperty 'Id' ` -StatusProperty 'DownloadStatus' ` -TaskType 'WingetDownload' ` -TaskArguments $taskArguments ` -CompletedStatusText "Completed" ` -ErrorStatusPrefix "Error: " ` -WindowObject $window ` -MainThreadLogPath $script:uiState.LogFilePath # Final status update (handled by Invoke-ParallelProcessing) $script:uiState.Controls.pbOverallProgress.Visibility = 'Collapsed' $buttonSender.IsEnabled = $true }) # BYO Apps UI logic (Keep existing logic) $script:uiState.Controls.btnBrowseAppSource.Add_Click({ $selectedPath = Show-ModernFolderPicker -Title "Select Application Source Folder" if ($selectedPath) { $window.FindName('txtAppSource').Text = $selectedPath } }) $script:uiState.Controls.btnAddApplication.Add_Click({ $name = $window.FindName('txtAppName').Text $commandLine = $window.FindName('txtAppCommandLine').Text $arguments = $window.FindName('txtAppArguments').Text $source = $window.FindName('txtAppSource').Text if ([string]::IsNullOrWhiteSpace($name) -or [string]::IsNullOrWhiteSpace($commandLine) -or [string]::IsNullOrWhiteSpace($arguments)) { [System.Windows.MessageBox]::Show("Please fill in all fields (Name, Command Line, and Arguments)", "Missing Information", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning) return } $listView = $window.FindName('lstApplications') $priority = 1 if ($listView.Items.Count -gt 0) { $priority = ($listView.Items | Measure-Object -Property Priority -Maximum).Maximum + 1 } $application = [PSCustomObject]@{ Priority = $priority; Name = $name; CommandLine = $commandLine; Arguments = $arguments; Source = $source; CopyStatus = "" } $listView.Items.Add($application) $window.FindName('txtAppName').Text = "" $window.FindName('txtAppCommandLine').Text = "" $window.FindName('txtAppArguments').Text = "" $window.FindName('txtAppSource').Text = "" Update-CopyButtonState -State $script:uiState }) $script:uiState.Controls.btnSaveBYOApplications.Add_Click({ $saveDialog = New-Object Microsoft.Win32.SaveFileDialog $saveDialog.Filter = "JSON files (*.json)|*.json|All files (*.*)|*.*" $saveDialog.DefaultExt = ".json" $saveDialog.Title = "Save Application List" $initialDir = $window.FindName('txtApplicationPath').Text if ([string]::IsNullOrWhiteSpace($initialDir) -or -not (Test-Path $initialDir)) { $initialDir = $PSScriptRoot } $saveDialog.InitialDirectory = $initialDir $saveDialog.FileName = "UserAppList.json" if ($saveDialog.ShowDialog()) { Save-BYOApplicationList -Path $saveDialog.FileName } }) $script:uiState.Controls.btnLoadBYOApplications.Add_Click({ $openDialog = New-Object Microsoft.Win32.OpenFileDialog $openDialog.Filter = "JSON files (*.json)|*.json|All files (*.*)|*.*" $openDialog.Title = "Import Application List" $initialDir = $window.FindName('txtApplicationPath').Text if ([string]::IsNullOrWhiteSpace($initialDir) -or -not (Test-Path $initialDir)) { $initialDir = $PSScriptRoot } $openDialog.InitialDirectory = $initialDir if ($openDialog.ShowDialog()) { Import-BYOApplicationList -Path $openDialog.FileName; Update-CopyButtonState -State $script:uiState } }) $script:uiState.Controls.btnClearBYOApplications.Add_Click({ $result = [System.Windows.MessageBox]::Show("Are you sure you want to clear all applications?", "Clear Applications", [System.Windows.MessageBoxButton]::YesNo, [System.Windows.MessageBoxImage]::Question) if ($result -eq [System.Windows.MessageBoxResult]::Yes) { $window.FindName('lstApplications').Items.Clear(); Update-CopyButtonState -State $script:uiState } }) $script:uiState.Controls.btnCopyBYOApps.Add_Click({ param($buttonSender, $clickEventArgs) $appsToCopy = $script:uiState.Controls.lstApplications.Items | Where-Object { -not [string]::IsNullOrWhiteSpace($_.Source) } if (-not $appsToCopy) { [System.Windows.MessageBox]::Show("No applications with a source path specified.", "Copy BYO Apps", "OK", "Information") return } $buttonSender.IsEnabled = $false $script:uiState.Controls.pbOverallProgress.Visibility = 'Visible' $script:uiState.Controls.pbOverallProgress.Value = 0 $script:uiState.Controls.txtStatus.Text = "Starting BYO app copy..." # Define necessary task-specific variables locally $localAppsPath = $window.FindName('txtApplicationPath').Text # Create hashtable for task-specific arguments $taskArguments = @{ AppsPath = $localAppsPath } # Select only necessary properties before passing $itemsToProcess = $appsToCopy | Select-Object Priority, Name, CommandLine, Arguments, Source # Invoke the centralized parallel processing function # Pass task type and task-specific arguments Invoke-ParallelProcessing -ItemsToProcess $itemsToProcess ` -ListViewControl $script:uiState.Controls.lstApplications ` -IdentifierProperty 'Name' ` -StatusProperty 'CopyStatus' ` -TaskType 'CopyBYO' ` -TaskArguments $taskArguments ` -CompletedStatusText "Copied" ` -ErrorStatusPrefix "Error: " ` -WindowObject $window ` -MainThreadLogPath $script:uiState.LogFilePath # Final status update (handled by Invoke-ParallelProcessing) $script:uiState.Controls.pbOverallProgress.Visibility = 'Collapsed' $buttonSender.IsEnabled = $true }) $script:uiState.Controls.btnMoveTop.Add_Click({ Move-ListViewItemTop -ListView $script:uiState.Controls.lstApplications }) $script:uiState.Controls.btnMoveUp.Add_Click({ Move-ListViewItemUp -ListView $script:uiState.Controls.lstApplications }) $script:uiState.Controls.btnMoveDown.Add_Click({ Move-ListViewItemDown -ListView $script:uiState.Controls.lstApplications }) $script:uiState.Controls.btnMoveBottom.Add_Click({ Move-ListViewItemBottom -ListView $script:uiState.Controls.lstApplications }) # BYO Apps ListView setup (Keep existing logic, ensure CopyStatus column is handled) $byoGridView = $script:uiState.Controls.lstApplications.View if ($byoGridView -is [System.Windows.Controls.GridView]) { $copyStatusColumnExists = $false foreach ($col in $byoGridView.Columns) { if ($col.Header -eq "Copy Status") { $copyStatusColumnExists = $true; break } } if (-not $copyStatusColumnExists) { $actionColumnIndex = -1 for ($i = 0; $i -lt $byoGridView.Columns.Count; $i++) { if ($byoGridView.Columns[$i].Header -eq "Action") { $actionColumnIndex = $i; break } } $copyStatusColumn = New-Object System.Windows.Controls.GridViewColumn $copyStatusColumn.Header = "Copy Status"; $copyStatusColumn.DisplayMemberBinding = New-Object System.Windows.Data.Binding("CopyStatus"); $copyStatusColumn.Width = 150 if ($actionColumnIndex -ge 0) { $byoGridView.Columns.Insert($actionColumnIndex, $copyStatusColumn) } else { $byoGridView.Columns.Add($copyStatusColumn) } } } Update-CopyButtonState -State $script:uiState # Initial check # General Browse Button Handlers (Keep existing logic) $script:uiState.Controls.btnBrowseFFUDevPath.Add_Click({ $selectedPath = Show-ModernFolderPicker -Title "Select FFU Development Path" if ($selectedPath) { $window.FindName('txtFFUDevPath').Text = $selectedPath } }) $script:uiState.Controls.btnBrowseFFUCaptureLocation.Add_Click({ $selectedPath = Show-ModernFolderPicker -Title "Select FFU Capture Location" if ($selectedPath) { $window.FindName('txtFFUCaptureLocation').Text = $selectedPath } }) $script:uiState.Controls.btnBrowseOfficePath.Add_Click({ $selectedPath = Show-ModernFolderPicker -Title "Select Office Path" if ($selectedPath) { $window.FindName('txtOfficePath').Text = $selectedPath } }) $script:uiState.Controls.btnBrowseDriversFolder.Add_Click({ $selectedPath = Show-ModernFolderPicker -Title "Select Drivers Folder" if ($selectedPath) { $window.FindName('txtDriversFolder').Text = $selectedPath } }) $script:uiState.Controls.btnBrowsePEDriversFolder.Add_Click({ $selectedPath = Show-ModernFolderPicker -Title "Select PE Drivers Folder" if ($selectedPath) { $window.FindName('txtPEDriversFolder').Text = $selectedPath } }) $script:uiState.Controls.btnBrowseDriversJsonPath.Add_Click({ $sfd = New-Object System.Windows.Forms.SaveFileDialog $sfd.Filter = "JSON files (*.json)|*.json|All files (*.*)|*.*" $sfd.Title = "Select or Create Drivers.json File" $sfd.FileName = "Drivers.json" $sfd.CheckFileExists = $false # Allow creating a new file or selecting existing $currentDriversJsonPath = $script:uiState.Controls.txtDriversJsonPath.Text $dialogInitialDirectory = $null # Initialize to null if (-not [string]::IsNullOrWhiteSpace($currentDriversJsonPath)) { WriteLog "Attempting to determine InitialDirectory for Drivers.json SaveFileDialog from txtDriversJsonPath: '$currentDriversJsonPath'" try { # Attempt to get the parent directory of the path in the textbox $parentDir = Split-Path -Path $currentDriversJsonPath -Parent -ErrorAction Stop # Check if the parent directory is not null/empty and actually exists as a directory if (-not ([string]::IsNullOrEmpty($parentDir)) -and (Test-Path -Path $parentDir -PathType Container)) { $dialogInitialDirectory = $parentDir WriteLog "Set InitialDirectory for SaveFileDialog to '$parentDir' based on parent of txtDriversJsonPath." } else { # Parent directory is invalid or doesn't exist WriteLog "Parent directory '$parentDir' from txtDriversJsonPath ('$currentDriversJsonPath') is not a valid existing directory. SaveFileDialog will use default InitialDirectory." # $dialogInitialDirectory remains $null, so dialog uses its default } } catch { # Error occurred trying to split the path (e.g., path is malformed) WriteLog "Error splitting path from txtDriversJsonPath ('$currentDriversJsonPath'): $($_.Exception.Message). SaveFileDialog will use default InitialDirectory." # $dialogInitialDirectory remains $null } } else { # TextBox is empty, dialog will use its default initial directory WriteLog "txtDriversJsonPath is empty. SaveFileDialog will use default InitialDirectory." # $dialogInitialDirectory remains $null } $sfd.InitialDirectory = $dialogInitialDirectory # Set to $null if no valid directory was found, dialog will use its default if ($sfd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $script:uiState.Controls.txtDriversJsonPath.Text = $sfd.FileName WriteLog "User selected or created Drivers.json at: $($sfd.FileName)" } else { WriteLog "User cancelled SaveFileDialog for Drivers.json." } }) # Driver Checkbox Conditional Logic $script:uiState.Controls.chkInstallDrivers.Add_Checked({ $script:uiState.Controls.chkCopyDrivers.IsEnabled = $false $script:uiState.Controls.chkCompressDriversToWIM.IsEnabled = $false }) $script:uiState.Controls.chkInstallDrivers.Add_Unchecked({ # Only re-enable if the other checkboxes are not checked if (-not $script:uiState.Controls.chkCopyDrivers.IsChecked) { $script:uiState.Controls.chkCopyDrivers.IsEnabled = $true } if (-not $script:uiState.Controls.chkCompressDriversToWIM.IsChecked) { $script:uiState.Controls.chkCompressDriversToWIM.IsEnabled = $true } }) $script:uiState.Controls.chkCopyDrivers.Add_Checked({ $script:uiState.Controls.chkInstallDrivers.IsEnabled = $false }) $script:uiState.Controls.chkCopyDrivers.Add_Unchecked({ # Only re-enable if InstallDrivers is not checked if (-not $script:uiState.Controls.chkInstallDrivers.IsChecked) { $script:uiState.Controls.chkInstallDrivers.IsEnabled = $true } }) $script:uiState.Controls.chkCompressDriversToWIM.Add_Checked({ $script:uiState.Controls.chkInstallDrivers.IsEnabled = $false }) $script:uiState.Controls.chkCompressDriversToWIM.Add_Unchecked({ # Only re-enable if InstallDrivers is not checked if (-not $script:uiState.Controls.chkInstallDrivers.IsChecked) { $script:uiState.Controls.chkInstallDrivers.IsEnabled = $true } }) # Set initial state based on defaults (assuming defaults are false) $script:uiState.Controls.chkInstallDrivers.IsEnabled = $true $script:uiState.Controls.chkCopyDrivers.IsEnabled = $true $script:uiState.Controls.chkCompressDriversToWIM.IsEnabled = $true # AppsScriptVariables Event Handlers $script:uiState.Controls.chkDefineAppsScriptVariables.Add_Checked({ $script:uiState.Controls.appsScriptVariablesPanel.Visibility = 'Visible' }) $script:uiState.Controls.chkDefineAppsScriptVariables.Add_Unchecked({ $script:uiState.Controls.appsScriptVariablesPanel.Visibility = 'Collapsed' }) $script:uiState.Controls.btnAddAppsScriptVariable.Add_Click({ $key = $script:uiState.Controls.txtAppsScriptKey.Text.Trim() $value = $script:uiState.Controls.txtAppsScriptValue.Text.Trim() if ([string]::IsNullOrWhiteSpace($key)) { [System.Windows.MessageBox]::Show("Apps Script Variable Key cannot be empty.", "Input Error", "OK", "Warning") return } # Check for duplicate keys $existingKey = $script:uiState.Controls.lstAppsScriptVariables.Items | Where-Object { $_.Key -eq $key } if ($existingKey) { [System.Windows.MessageBox]::Show("An Apps Script Variable with the key '$key' already exists.", "Duplicate Key", "OK", "Warning") return } $newItem = [PSCustomObject]@{ IsSelected = $false # Add IsSelected property Key = $key Value = $value } $script:uiState.Data.appsScriptVariablesDataList.Add($newItem) $script:uiState.Controls.lstAppsScriptVariables.ItemsSource = $script:uiState.Data.appsScriptVariablesDataList.ToArray() $script:uiState.Controls.txtAppsScriptKey.Clear() $script:uiState.Controls.txtAppsScriptValue.Clear() # Update the header checkbox state if ($null -ne $script:uiState.Controls.chkSelectAllAppsScriptVariables) { Update-SelectAllHeaderCheckBoxState -ListView $script:uiState.Controls.lstAppsScriptVariables -HeaderCheckBox $script:uiState.Controls.chkSelectAllAppsScriptVariables } }) $script:uiState.Controls.btnRemoveSelectedAppsScriptVariables.Add_Click({ $itemsToRemove = @($script:uiState.Data.appsScriptVariablesDataList | Where-Object { $_.IsSelected }) if ($itemsToRemove.Count -eq 0) { [System.Windows.MessageBox]::Show("Please select one or more Apps Script Variables to remove.", "Selection Error", "OK", "Warning") return } foreach ($itemToRemove in $itemsToRemove) { $script:uiState.Data.appsScriptVariablesDataList.Remove($itemToRemove) } $script:uiState.Controls.lstAppsScriptVariables.ItemsSource = $script:uiState.Data.appsScriptVariablesDataList.ToArray() # Update the header checkbox state if ($null -ne $script:uiState.Controls.chkSelectAllAppsScriptVariables) { # Check if variable exists Update-SelectAllHeaderCheckBoxState -ListView $script:uiState.Controls.lstAppsScriptVariables -HeaderCheckBox $script:uiState.Controls.chkSelectAllAppsScriptVariables } }) $script:uiState.Controls.btnClearAppsScriptVariables.Add_Click({ $script:uiState.Data.appsScriptVariablesDataList.Clear() $script:uiState.Controls.lstAppsScriptVariables.ItemsSource = $script:uiState.Data.appsScriptVariablesDataList.ToArray() # Update the header checkbox state if ($null -ne $script:uiState.Controls.chkSelectAllAppsScriptVariables) { Update-SelectAllHeaderCheckBoxState -ListView $script:uiState.Controls.lstAppsScriptVariables -HeaderCheckBox $script:uiState.Controls.chkSelectAllAppsScriptVariables } }) # Initial state for chkDefineAppsScriptVariables based on chkInstallApps if ($script:uiState.Controls.chkInstallApps.IsChecked) { $script:uiState.Controls.chkDefineAppsScriptVariables.Visibility = 'Visible' } else { $script:uiState.Controls.chkDefineAppsScriptVariables.Visibility = 'Collapsed' } # Initial state for appsScriptVariablesPanel based on chkDefineAppsScriptVariables if ($script:uiState.Controls.chkDefineAppsScriptVariables.IsChecked) { $script:uiState.Controls.appsScriptVariablesPanel.Visibility = 'Visible' } else { $script:uiState.Controls.appsScriptVariablesPanel.Visibility = 'Collapsed' } }) # Function to search for Winget apps function Search-WingetApps { try { $searchQuery = $script:uiState.Controls.txtWingetSearch.Text if ([string]::IsNullOrWhiteSpace($searchQuery)) { return } # Get current items from the ListView $currentItemsInListView = @() if ($null -ne $script:uiState.Controls.lstWingetResults.ItemsSource) { $currentItemsInListView = @($script:uiState.Controls.lstWingetResults.ItemsSource) } elseif ($script:uiState.Controls.lstWingetResults.HasItems) { $currentItemsInListView = @($script:uiState.Controls.lstWingetResults.Items) } # Store selected apps from the current view $selectedAppsFromView = @($currentItemsInListView | Where-Object { $_.IsSelected }) # Search for new apps $searchedAppResults = Search-WingetPackagesPublic -Query $searchQuery | ForEach-Object { [PSCustomObject]@{ IsSelected = $false # New items are not selected by default Name = $_.Name Id = $_.Id Version = $_.Version Source = $_.Source DownloadStatus = "" } } $finalAppList = [System.Collections.Generic.List[object]]::new() $addedAppIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) # Add previously selected apps first foreach ($app in $selectedAppsFromView) { $finalAppList.Add($app) $addedAppIds.Add($app.Id) | Out-Null } # Add new search results, avoiding duplicates of already added (selected) apps foreach ($result in $searchedAppResults) { if (-not $addedAppIds.Contains($result.Id)) { $finalAppList.Add($result) $addedAppIds.Add($result.Id) | Out-Null # Track added IDs to prevent duplicates from search results themselves } } # Update the ListView's ItemsSource $script:uiState.Controls.lstWingetResults.ItemsSource = $finalAppList.ToArray() } catch { [System.Windows.MessageBox]::Show("Error searching for apps: $_", "Error", "OK", "Error") } } # Function to save selected apps to JSON function Save-WingetList { try { $selectedApps = $script:uiState.Controls.lstWingetResults.Items | Where-Object { $_.IsSelected } if (-not $selectedApps) { [System.Windows.MessageBox]::Show("No apps selected to save.", "Warning", "OK", "Warning") return } $appList = @{ apps = @($selectedApps | ForEach-Object { [ordered]@{ name = $_.Name id = $_.Id source = $_.Source.ToLower() } }) } $sfd = New-Object System.Windows.Forms.SaveFileDialog $sfd.Filter = "JSON files (*.json)|*.json" $sfd.Title = "Save App List" $sfd.InitialDirectory = $AppsPath $sfd.FileName = "AppList.json" if ($sfd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $appList | ConvertTo-Json -Depth 10 | Set-Content $sfd.FileName -Encoding UTF8 [System.Windows.MessageBox]::Show("App list saved successfully.", "Success", "OK", "Information") } } catch { [System.Windows.MessageBox]::Show("Error saving app list: $_", "Error", "OK", "Error") } } # Function to import app list from JSON function Import-WingetList { try { $ofd = New-Object System.Windows.Forms.OpenFileDialog $ofd.Filter = "JSON files (*.json)|*.json" $ofd.Title = "Import App List" $ofd.InitialDirectory = $AppsPath if ($ofd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $importedAppsData = Get-Content $ofd.FileName -Raw | ConvertFrom-Json $newAppListForItemsSource = [System.Collections.Generic.List[object]]::new() if ($null -ne $importedAppsData.apps) { foreach ($appInfo in $importedAppsData.apps) { $newAppListForItemsSource.Add([PSCustomObject]@{ IsSelected = $true # Imported apps are marked as selected Name = $appInfo.name Id = $appInfo.id Version = "" # Will be populated when searching or if data exists Source = $appInfo.source DownloadStatus = "" }) } } $script:uiState.Controls.lstWingetResults.ItemsSource = $newAppListForItemsSource.ToArray() [System.Windows.MessageBox]::Show("App list imported successfully.", "Success", "OK", "Information") } } catch { [System.Windows.MessageBox]::Show("Error importing app list: $_", "Error", "OK", "Error") } } # Function to remove application and reorder priorities function Remove-Application { param( $priority, [psobject]$State ) $listView = $State.Controls.lstApplications # Remove the item with the specified priority $itemToRemove = $listView.Items | Where-Object { $_.Priority -eq $priority } | Select-Object -First 1 if ($itemToRemove) { $listView.Items.Remove($itemToRemove) # Reorder priorities for remaining items Update-ListViewPriorities -ListView $listView # Update the Copy Apps button state Update-CopyButtonState -State $State } } # Function to save BYO applications to JSON function Save-BYOApplicationList { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Path, [Parameter(Mandatory)] [psobject]$State ) $listView = $State.Controls.lstApplications if (-not $listView -or $listView.Items.Count -eq 0) { [System.Windows.MessageBox]::Show("No applications to save.", "Save Applications", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information) return } try { # Ensure items are sorted by current priority before saving # Exclude CopyStatus when saving $applications = $listView.Items | Sort-Object Priority | Select-Object Priority, Name, CommandLine, Arguments, Source $applications | ConvertTo-Json -Depth 5 | Set-Content -Path $Path -Force -Encoding UTF8 [System.Windows.MessageBox]::Show("Applications saved successfully to `"$Path`".", "Save Applications", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information) } catch { [System.Windows.MessageBox]::Show("Failed to save applications: $_", "Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error) } } # Function to load BYO applications from JSON function Import-BYOApplicationList { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Path ) if (-not (Test-Path $Path)) { [System.Windows.MessageBox]::Show("Application list file not found at `"$Path`".", "Import Applications", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning) return } try { $applications = Get-Content -Path $Path -Raw | ConvertFrom-Json $listView = $window.FindName('lstApplications') $listView.Items.Clear() # Add items and sort by priority from the file $sortedApps = $applications | Sort-Object Priority foreach ($app in $sortedApps) { # Ensure all properties exist, add CopyStatus $appObject = [PSCustomObject]@{ Priority = $app.Priority # Keep original priority for now Name = $app.Name CommandLine = $app.CommandLine Arguments = if ($app.PSObject.Properties['Arguments']) { $app.Arguments } else { "" } # Handle missing Arguments Source = $app.Source CopyStatus = "" # Initialize CopyStatus } $listView.Items.Add($appObject) } # Reorder priorities sequentially after loading Update-ListViewPriorities -ListView $listView # Update the Copy Apps button state Update-CopyButtonState [System.Windows.MessageBox]::Show("Applications imported successfully from `"$Path`".", "Import Applications", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information) } catch { [System.Windows.MessageBox]::Show("Failed to import applications: $_", "Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error) } } # Button: Build FFU $btnRun = $window.FindName('btnRun') $btnRun.Add_Click({ try { $progressBar = $window.FindName('progressBar') $txtStatus = $window.FindName('txtStatus') $progressBar.Visibility = 'Visible' $txtStatus.Text = "Starting FFU build..." $config = Get-UIConfig -State $script:uiState $configFilePath = Join-Path $config.FFUDevelopmentPath "FFUConfig.json" $config | ConvertTo-Json -Depth 10 | Set-Content $configFilePath -Encoding UTF8 $txtStatus.Text = "Executing BuildFFUVM script with config file..." & "$PSScriptRoot\BuildFFUVM.ps1" -ConfigFile $configFilePath -Verbose if ($config.InstallOffice -and $config.OfficeConfigXMLFile) { Copy-Item -Path $config.OfficeConfigXMLFile -Destination $config.OfficePath -Force $txtStatus.Text = "Office Configuration XML file copied successfully." } $txtStatus.Text = "FFU build completed successfully." } catch { [System.Windows.MessageBox]::Show("An error occurred: $_", "Error", "OK", "Error") $window.FindName('txtStatus').Text = "FFU build failed." } finally { $window.FindName('progressBar').Visibility = 'Collapsed' } }) # Button: Build Config $btnBuildConfig = $window.FindName('btnBuildConfig') $btnBuildConfig.Add_Click({ try { $config = Get-UIConfig -State $script:uiState $defaultConfigPath = Join-Path $config.FFUDevelopmentPath "config" if (-not (Test-Path $defaultConfigPath)) { New-Item -Path $defaultConfigPath -ItemType Directory -Force | Out-Null } $sfd = New-Object System.Windows.Forms.SaveFileDialog $sfd.Filter = "JSON files (*.json)|*.json|All files (*.*)|*.*" $sfd.Title = "Save Configuration File" $sfd.InitialDirectory = $defaultConfigPath $sfd.FileName = "FFUConfig.json" if ($sfd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $savePath = $sfd.FileName $config | ConvertTo-Json -Depth 10 | Set-Content $savePath -Encoding UTF8 [System.Windows.MessageBox]::Show("Configuration file saved to:`n$savePath", "Success", "OK", "Information") } } catch { [System.Windows.MessageBox]::Show("Error saving config file:`n$_", "Error", "OK", "Error") } }) # Button: Load Config File $btnLoadConfig = $window.FindName('btnLoadConfig') $btnLoadConfig.Add_Click({ try { $ofd = New-Object System.Windows.Forms.OpenFileDialog $ofd.Filter = "JSON files (*.json)|*.json|All files (*.*)|*.*" $ofd.Title = "Load Configuration File" if ($ofd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { WriteLog "Loading configuration from: $($ofd.FileName)" $configContent = Get-Content -Path $ofd.FileName -Raw | ConvertFrom-Json if ($null -eq $configContent) { WriteLog "LoadConfig Error: configContent is null after parsing $($ofd.FileName). File might be empty or malformed." [System.Windows.MessageBox]::Show("Failed to parse the configuration file. It might be empty or not valid JSON.", "Load Error", "OK", "Error") return } WriteLog "LoadConfig: Successfully parsed config file. Top-level keys: $($configContent.PSObject.Properties.Name -join ', ')" # Update Build tab values Set-UIValue -ControlName 'txtFFUDevPath' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'FFUDevelopmentPath' -State $script:uiState Set-UIValue -ControlName 'txtCustomFFUNameTemplate' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'CustomFFUNameTemplate' -State $script:uiState Set-UIValue -ControlName 'txtFFUCaptureLocation' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'FFUCaptureLocation' -State $script:uiState Set-UIValue -ControlName 'txtShareName' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'ShareName' -State $script:uiState Set-UIValue -ControlName 'txtUsername' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'Username' -State $script:uiState Set-UIValue -ControlName 'chkBuildUSBDriveEnable' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'BuildUSBDrive' -State $script:uiState Set-UIValue -ControlName 'chkCompactOS' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'CompactOS' -State $script:uiState Set-UIValue -ControlName 'chkUpdateADK' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'UpdateADK' -State $script:uiState Set-UIValue -ControlName 'chkOptimize' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'Optimize' -State $script:uiState Set-UIValue -ControlName 'chkAllowVHDXCaching' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'AllowVHDXCaching' -State $script:uiState Set-UIValue -ControlName 'chkAllowExternalHardDiskMedia' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'AllowExternalHardDiskMedia' -State $script:uiState Set-UIValue -ControlName 'chkPromptExternalHardDiskMedia' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'PromptExternalHardDiskMedia' -State $script:uiState Set-UIValue -ControlName 'chkCreateCaptureMedia' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'CreateCaptureMedia' -State $script:uiState Set-UIValue -ControlName 'chkCreateDeploymentMedia' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'CreateDeploymentMedia' -State $script:uiState # USB Drive Modification group (Build Tab) Set-UIValue -ControlName 'chkCopyAutopilot' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'CopyAutopilot' -State $script:uiState Set-UIValue -ControlName 'chkCopyUnattend' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'CopyUnattend' -State $script:uiState Set-UIValue -ControlName 'chkCopyPPKG' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'CopyPPKG' -State $script:uiState # Post Build Cleanup group (Build Tab) Set-UIValue -ControlName 'chkCleanupAppsISO' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'CleanupAppsISO' -State $script:uiState Set-UIValue -ControlName 'chkCleanupCaptureISO' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'CleanupCaptureISO' -State $script:uiState Set-UIValue -ControlName 'chkCleanupDeployISO' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'CleanupDeployISO' -State $script:uiState Set-UIValue -ControlName 'chkCleanupDrivers' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'CleanupDrivers' -State $script:uiState Set-UIValue -ControlName 'chkRemoveFFU' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'RemoveFFU' -State $script:uiState Set-UIValue -ControlName 'chkRemoveApps' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'RemoveApps' -State $script:uiState Set-UIValue -ControlName 'chkRemoveUpdates' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'RemoveUpdates' -State $script:uiState # Hyper-V Settings Set-UIValue -ControlName 'cmbVMSwitchName' -PropertyName 'SelectedItem' -ConfigObject $configContent -ConfigKey 'VMSwitchName' -State $script:uiState Set-UIValue -ControlName 'txtVMHostIPAddress' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'VMHostIPAddress' -State $script:uiState Set-UIValue -ControlName 'txtDiskSize' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'Disksize' -TransformValue { param($val) $val / 1GB } -State $script:uiState Set-UIValue -ControlName 'txtMemory' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'Memory' -TransformValue { param($val) $val / 1GB } -State $script:uiState Set-UIValue -ControlName 'txtProcessors' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'Processors' -State $script:uiState Set-UIValue -ControlName 'txtVMLocation' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'VMLocation' -State $script:uiState Set-UIValue -ControlName 'txtVMNamePrefix' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'FFUPrefix' -State $script:uiState Set-UIValue -ControlName 'cmbLogicalSectorSize' -PropertyName 'SelectedItem' -ConfigObject $configContent -ConfigKey 'LogicalSectorSizeBytes' -TransformValue { param($val) $val.ToString() } -State $script:uiState # Windows Settings Set-UIValue -ControlName 'txtISOPath' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'ISOPath' -State $script:uiState Set-UIValue -ControlName 'cmbWindowsRelease' -PropertyName 'SelectedItem' -ConfigObject $configContent -ConfigKey 'WindowsRelease' -State $script:uiState Set-UIValue -ControlName 'cmbWindowsVersion' -PropertyName 'SelectedItem' -ConfigObject $configContent -ConfigKey 'WindowsVersion' -State $script:uiState Set-UIValue -ControlName 'cmbWindowsArch' -PropertyName 'SelectedItem' -ConfigObject $configContent -ConfigKey 'WindowsArch' -State $script:uiState Set-UIValue -ControlName 'cmbWindowsLang' -PropertyName 'SelectedItem' -ConfigObject $configContent -ConfigKey 'WindowsLang' -State $script:uiState Set-UIValue -ControlName 'cmbWindowsSKU' -PropertyName 'SelectedItem' -ConfigObject $configContent -ConfigKey 'WindowsSKU' -State $script:uiState Set-UIValue -ControlName 'cmbMediaType' -PropertyName 'SelectedItem' -ConfigObject $configContent -ConfigKey 'MediaType' -State $script:uiState Set-UIValue -ControlName 'txtProductKey' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'ProductKey' -State $script:uiState Set-UIValue -ControlName 'txtOptionalFeatures' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'OptionalFeatures' -State $script:uiState # Update Optional Features checkboxes based on the loaded text $loadedFeaturesString = $script:uiState.Controls.txtOptionalFeatures.Text if (-not [string]::IsNullOrWhiteSpace($loadedFeaturesString)) { $loadedFeaturesArray = $loadedFeaturesString.Split(';') WriteLog "LoadConfig: Updating Optional Features checkboxes. Loaded features: $($loadedFeaturesArray -join ', ')" foreach ($featureEntry in $script:uiState.Controls.featureCheckBoxes.GetEnumerator()) { $featureName = $featureEntry.Key $featureCheckbox = $featureEntry.Value if ($loadedFeaturesArray -contains $featureName) { $featureCheckbox.IsChecked = $true WriteLog "LoadConfig: Checked checkbox for feature '$featureName'." } else { $featureCheckbox.IsChecked = $false } } } else { # If no optional features are loaded, uncheck all WriteLog "LoadConfig: No optional features string loaded. Unchecking all feature checkboxes." foreach ($featureEntry in $script:uiState.Controls.featureCheckBoxes.GetEnumerator()) { $featureEntry.Value.IsChecked = $false } } # M365 Apps/Office tab Set-UIValue -ControlName 'chkInstallOffice' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'InstallOffice' -State $script:uiState Set-UIValue -ControlName 'txtOfficePath' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'OfficePath' -State $script:uiState Set-UIValue -ControlName 'chkCopyOfficeConfigXML' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'CopyOfficeConfigXML' -State $script:uiState Set-UIValue -ControlName 'txtOfficeConfigXMLFilePath' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'OfficeConfigXMLFile' -State $script:uiState # Drivers tab Set-UIValue -ControlName 'chkInstallDrivers' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'InstallDrivers' -State $script:uiState Set-UIValue -ControlName 'chkDownloadDrivers' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'DownloadDrivers' -State $script:uiState Set-UIValue -ControlName 'chkCopyDrivers' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'CopyDrivers' -State $script:uiState Set-UIValue -ControlName 'cmbMake' -PropertyName 'SelectedItem' -ConfigObject $configContent -ConfigKey 'Make' -State $script:uiState Set-UIValue -ControlName 'txtDriversFolder' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'DriversFolder' -State $script:uiState Set-UIValue -ControlName 'txtPEDriversFolder' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'PEDriversFolder' -State $script:uiState Set-UIValue -ControlName 'txtDriversJsonPath' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'DriversJsonPath' -State $script:uiState Set-UIValue -ControlName 'chkCopyPEDrivers' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'CopyPEDrivers' -State $script:uiState Set-UIValue -ControlName 'chkCompressDriversToWIM' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'CompressDownloadedDriversToWim' -State $script:uiState # Updates tab Set-UIValue -ControlName 'chkUpdateLatestCU' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'UpdateLatestCU' -State $script:uiState Set-UIValue -ControlName 'chkUpdateLatestNet' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'UpdateLatestNet' -State $script:uiState Set-UIValue -ControlName 'chkUpdateLatestDefender' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'UpdateLatestDefender' -State $script:uiState Set-UIValue -ControlName 'chkUpdateEdge' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'UpdateEdge' -State $script:uiState Set-UIValue -ControlName 'chkUpdateOneDrive' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'UpdateOneDrive' -State $script:uiState Set-UIValue -ControlName 'chkUpdateLatestMSRT' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'UpdateLatestMSRT' -State $script:uiState Set-UIValue -ControlName 'chkUpdateLatestMicrocode' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'UpdateLatestMicrocode' -State $script:uiState Set-UIValue -ControlName 'chkUpdatePreviewCU' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'UpdatePreviewCU' -State $script:uiState # Applications tab Set-UIValue -ControlName 'chkInstallApps' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'InstallApps' -State $script:uiState Set-UIValue -ControlName 'chkInstallWingetApps' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'InstallWingetApps' -State $script:uiState Set-UIValue -ControlName 'chkBringYourOwnApps' -PropertyName 'IsChecked' -ConfigObject $configContent -ConfigKey 'BringYourOwnApps' -State $script:uiState Set-UIValue -ControlName 'txtApplicationPath' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'AppsPath' -State $script:uiState Set-UIValue -ControlName 'txtAppListJsonPath' -PropertyName 'Text' -ConfigObject $configContent -ConfigKey 'AppListPath' -State $script:uiState # Handle AppsScriptVariables $appsScriptVarsKeyExists = $false if ($configContent -is [System.Management.Automation.PSCustomObject] -and $null -ne $configContent.PSObject.Properties) { try { if (($configContent.PSObject.Properties.Match('AppsScriptVariables')).Count -gt 0) { $appsScriptVarsKeyExists = $true } } catch { WriteLog "ERROR: Exception while trying to Match key 'AppsScriptVariables'. Error: $($_.Exception.Message)" } } $lstAppsScriptVars = $script:uiState.Controls.lstAppsScriptVariables $chkDefineAppsScriptVars = $script:uiState.Controls.chkDefineAppsScriptVariables $appsScriptVarsPanel = $script:uiState.Controls.appsScriptVariablesPanel $script:uiState.Data.appsScriptVariablesDataList.Clear() # Clear the backing data list if ($appsScriptVarsKeyExists -and $null -ne $configContent.AppsScriptVariables -and $configContent.AppsScriptVariables -is [System.Management.Automation.PSCustomObject]) { WriteLog "LoadConfig: Processing AppsScriptVariables from config." $loadedVars = $configContent.AppsScriptVariables $hasVars = $false foreach ($prop in $loadedVars.PSObject.Properties) { $script:uiState.Data.appsScriptVariablesDataList.Add([PSCustomObject]@{ IsSelected = $false; Key = $prop.Name; Value = $prop.Value }) $hasVars = $true } if ($hasVars) { $chkDefineAppsScriptVars.IsChecked = $true $appsScriptVarsPanel.Visibility = 'Visible' WriteLog "LoadConfig: Loaded AppsScriptVariables and checked 'Define Apps Script Variables'." } else { $chkDefineAppsScriptVars.IsChecked = $false $appsScriptVarsPanel.Visibility = 'Collapsed' WriteLog "LoadConfig: AppsScriptVariables key was present but empty. Unchecked 'Define Apps Script Variables'." } } elseif ($appsScriptVarsKeyExists -and $null -ne $configContent.AppsScriptVariables -and $configContent.AppsScriptVariables -is [hashtable]) { # Handle if it's already a hashtable (e.g., from older config or direct creation) WriteLog "LoadConfig: Processing AppsScriptVariables (Hashtable) from config." $loadedVars = $configContent.AppsScriptVariables $hasVars = $false foreach ($keyName in $loadedVars.Keys) { $script:uiState.Data.appsScriptVariablesDataList.Add([PSCustomObject]@{ IsSelected = $false; Key = $keyName; Value = $loadedVars[$keyName] }) $hasVars = $true } if ($hasVars) { $chkDefineAppsScriptVars.IsChecked = $true $appsScriptVarsPanel.Visibility = 'Visible' WriteLog "LoadConfig: Loaded AppsScriptVariables (Hashtable) and checked 'Define Apps Script Variables'." } else { $chkDefineAppsScriptVars.IsChecked = $false $appsScriptVarsPanel.Visibility = 'Collapsed' WriteLog "LoadConfig: AppsScriptVariables (Hashtable) key was present but empty. Unchecked 'Define Apps Script Variables'." } } else { $chkDefineAppsScriptVars.IsChecked = $false $appsScriptVarsPanel.Visibility = 'Collapsed' WriteLog "LoadConfig Info: Key 'AppsScriptVariables' not found, is null, or not a PSCustomObject/Hashtable. Unchecked 'Define Apps Script Variables'." } # Update the ListView's ItemsSource after populating the data list $lstAppsScriptVars.ItemsSource = $script:uiState.Data.appsScriptVariablesDataList.ToArray() # Update the header checkbox state if ($null -ne (Get-Variable -Name 'chkSelectAllAppsScriptVariables' -Scope Script -ErrorAction SilentlyContinue)) { Update-SelectAllHeaderCheckBoxState -ListView $lstAppsScriptVars -HeaderCheckBox $script:chkSelectAllAppsScriptVariables } # Update USB Drive selection if present in config $usbDriveListKeyExists = $false if ($configContent -is [System.Management.Automation.PSCustomObject] -and $null -ne $configContent.PSObject.Properties) { try { if (($configContent.PSObject.Properties.Match('USBDriveList')).Count -gt 0) { $usbDriveListKeyExists = $true } } catch { WriteLog "ERROR: Exception while trying to Match key 'USBDriveList' on configContent.PSObject.Properties. Error: $($_.Exception.Message)" } } if ($usbDriveListKeyExists -and $null -ne $configContent.USBDriveList) { WriteLog "LoadConfig: Processing USBDriveList from config." # First click the Check USB Drives button to populate the list $script:uiState.Controls.btnCheckUSBDrives.RaiseEvent( [System.Windows.RoutedEventArgs]::new( [System.Windows.Controls.Button]::ClickEvent ) ) # Then select the drives that match the saved configuration foreach ($item in $script:uiState.Controls.lstUSBDrives.Items) { $propertyName = $item.Model $propertyExists = $false $propertyValue = $null # Ensure USBDriveList is a PSCustomObject before trying to access its properties dynamically if ($null -ne $configContent.USBDriveList -and $configContent.USBDriveList -is [System.Management.Automation.PSCustomObject]) { # Check if the property exists on the USBDriveList object if ($configContent.USBDriveList.PSObject.Properties.Match($propertyName).Count -gt 0) { $propertyExists = $true # Access the value dynamically $propertyValue = $configContent.USBDriveList.$($propertyName) } } if ($propertyExists -and ($propertyValue -eq $item.SerialNumber)) { WriteLog "LoadConfig: Selecting USB Drive Model '$($item.Model)' with Serial '$($item.SerialNumber)'." $item.IsSelected = $true } else { if (-not $propertyExists -and ($null -ne $configContent.USBDriveList)) { WriteLog "LoadConfig: Property '$($propertyName)' not found on USBDriveList for item Model '$($item.Model)'." } $item.IsSelected = $false # Ensure others are deselected if not in config or value mismatch } } $script:uiState.Controls.lstUSBDrives.Items.Refresh() # Update the Select All checkbox state $allSelected = $script:uiState.Controls.lstUSBDrives.Items.Count -gt 0 -and -not ($script:uiState.Controls.lstUSBDrives.Items | Where-Object { -not $_.IsSelected }) $script:uiState.Controls.chkSelectAllUSBDrives.IsChecked = $allSelected WriteLog "LoadConfig: USBDriveList processing complete." } else { WriteLog "LoadConfig Info: Key 'USBDriveList' not found or is null in configuration file. Skipping USB drive selection." } # If BuildUSBDrive is enabled and USBDriveList was present and not empty in the config, # ensure "Select Specific USB Drives" is checked to show the list. $shouldAutoCheckSpecificDrives = $false if ($window.FindName('chkBuildUSBDriveEnable').IsChecked -and $usbDriveListKeyExists -and ($null -ne $configContent.USBDriveList)) { if ($configContent.USBDriveList -is [System.Management.Automation.PSCustomObject]) { if ($configContent.USBDriveList.PSObject.Properties.Count -gt 0) { $shouldAutoCheckSpecificDrives = $true } } elseif ($configContent.USBDriveList -is [hashtable]) { # Fallback for older configs if ($configContent.USBDriveList.Keys.Count -gt 0) { $shouldAutoCheckSpecificDrives = $true } } } if ($shouldAutoCheckSpecificDrives) { WriteLog "LoadConfig: Auto-checking 'Select Specific USB Drives' due to pre-selected USB drives in config." $window.FindName('chkSelectSpecificUSBDrives').IsChecked = $true } else { WriteLog "LoadConfig: Condition to auto-check 'Select Specific USB Drives' was NOT met." } WriteLog "LoadConfig: Configuration loading process finished." } } catch { WriteLog "LoadConfig FATAL Error: $($_.Exception.ToString())" # Log full exception details [System.Windows.MessageBox]::Show("Error loading config file:`n$($_.Exception.Message)", "Error", "OK", "Error") } }) # Add handler for Remove button clicks $window.Add_SourceInitialized({ $listView = $window.FindName('lstApplications') $listView.AddHandler( [System.Windows.Controls.Button]::ClickEvent, [System.Windows.RoutedEventHandler] { param($buttonSender, $clickEventArgs) if ($clickEventArgs.OriginalSource -is [System.Windows.Controls.Button] -and $clickEventArgs.OriginalSource.Content -eq "Remove") { Remove-Application -priority $clickEventArgs.OriginalSource.Tag -State $script:uiState } } ) }) # Register cleanup to reclaim memory and revert LongPathsEnabled setting when the UI window closes $window.Add_Closed({ # Revert LongPathsEnabled registry setting if it was changed by this script if ($script:uiState.Flags.originalLongPathsValue -ne 1) { # Only revert if we changed it from something other than 1 try { $currentValue = Get-ItemPropertyValue -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -ErrorAction SilentlyContinue if ($currentValue -eq 1) { # Double-check it's still 1 before reverting $revertValue = if ($null -eq $script:uiState.Flags.originalLongPathsValue) { 0 } else { $script:uiState.Flags.originalLongPathsValue } # Revert to original or 0 if it didn't exist WriteLog "Reverting LongPathsEnabled registry key back to original value ($revertValue)." Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value $revertValue -Force WriteLog "LongPathsEnabled reverted." } } catch { WriteLog "Error reverting LongPathsEnabled registry key: $($_.Exception.Message)." } } # Garbage collection [System.GC]::Collect() [System.GC]::WaitForPendingFinalizers() }) [void]$window.ShowDialog()