From 802865c1a63aedc353861e00beedb44e8d3d0e7c Mon Sep 17 00:00:00 2001 From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com> Date: Tue, 18 Feb 2025 20:21:37 -0800 Subject: [PATCH] Add Clear List button to Save/Import functionality and fixed sorting issues with winget app list. --- FFUDevelopment/BuildFFUVM_UI.ps1 | 1497 +++++++++++++++-------------- FFUDevelopment/BuildFFUVM_UI.xaml | 7 +- 2 files changed, 756 insertions(+), 748 deletions(-) diff --git a/FFUDevelopment/BuildFFUVM_UI.ps1 b/FFUDevelopment/BuildFFUVM_UI.ps1 index 5b9d364..1ebae3e 100644 --- a/FFUDevelopment/BuildFFUVM_UI.ps1 +++ b/FFUDevelopment/BuildFFUVM_UI.ps1 @@ -6,22 +6,21 @@ param() # SECTION 1: Variables & Constants # -------------------------------------------------------------------------- $FFUDevelopmentPath = $PSScriptRoot -$AppsPath = Join-Path $FFUDevelopmentPath "Apps" +$AppsPath = Join-Path $FFUDevelopmentPath "Apps" # Add the new function for USB drive detection function Get-USBDrives { Get-WmiObject Win32_DiskDrive | Where-Object { - ($_.MediaType -eq 'Removable Media' -or $_.MediaType -eq 'External hard disk media') -and - $_.InterfaceType -eq 'USB' + ($_.MediaType -eq 'Removable Media' -or $_.MediaType -eq 'External hard disk media') } | ForEach-Object { $size = [math]::Round($_.Size / 1GB, 2) $serialNumber = if ($_.SerialNumber) { $_.SerialNumber.Trim() } else { "N/A" } @{ - IsSelected = $false - Model = $_.Model.Trim() + IsSelected = $false + Model = $_.Model.Trim() SerialNumber = $serialNumber - Size = $size - DriveIndex = $_.Index + Size = $size + DriveIndex = $_.Index } } } @@ -40,7 +39,7 @@ function Test-WingetCLI { if (-not $wingetCmd) { return @{ Version = "Not installed" - Status = "Not installed - Install from Microsoft Store" + Status = "Not installed - Install from Microsoft Store" } } @@ -51,18 +50,18 @@ function Test-WingetCLI { if ($version -lt $minVersion) { return @{ Version = $version.ToString() - Status = "Update required - Install from Microsoft Store" + Status = "Update required - Install from Microsoft Store" } } return @{ Version = $version.ToString() - Status = $version.ToString() + Status = $version.ToString() } } return @{ Version = "Unknown" - Status = "Version check failed" + Status = "Version check failed" } } @@ -76,12 +75,12 @@ function Update-WingetVersionFields { ) # Force UI update on the UI thread - $window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Normal, [Action]{ - $script:txtWingetVersion.Text = $wingetText - $script:txtWingetModuleVersion.Text = $moduleText - # Force immediate UI refresh - [System.Windows.Forms.Application]::DoEvents() - }) + $window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Normal, [Action] { + $script:txtWingetVersion.Text = $wingetText + $script:txtWingetModuleVersion.Text = $moduleText + # Force immediate UI refresh + [System.Windows.Forms.Application]::DoEvents() + }) } function Install-WingetComponents { @@ -132,8 +131,8 @@ function Confirm-WinGetInstallation { $minVersion = [version]"1.8.1911" $result = @{ - Success = $false - Message = "" + Success = $false + Message = "" RequiresRestart = $false } @@ -202,61 +201,61 @@ function Confirm-WinGetInstallation { } # Some default values -$defaultISOPath = "" -$defaultWindowsArch = "x64" -$defaultWindowsLang = "en-us" -$defaultWindowsSKU = "Pro" -$defaultMediaType = "Consumer" # updated value +$defaultISOPath = "" +$defaultWindowsArch = "x64" +$defaultWindowsLang = "en-us" +$defaultWindowsSKU = "Pro" +$defaultMediaType = "Consumer" # updated value $defaultOptionalFeatures = "" -$defaultProductKey = "" -$defaultFFUPrefix = "_FFU" # <-- new default for VM Name Prefix +$defaultProductKey = "" +$defaultFFUPrefix = "_FFU" # <-- new default for VM Name Prefix # Large list from the ValidateSet in BuildFFUVM.ps1 ($OptionalFeatures parameter) $allowedFeatures = @( - "AppServerClient","Client-DeviceLockdown","Client-EmbeddedBootExp","Client-EmbeddedLogon", - "Client-EmbeddedShellLauncher","Client-KeyboardFilter","Client-ProjFS","Client-UnifiedWriteFilter", - "Containers","Containers-DisposableClientVM","Containers-HNS","Containers-SDN","DataCenterBridging", - "DirectoryServices-ADAM-Client","DirectPlay","HostGuardian","HypervisorPlatform","IIS-ApplicationDevelopment", - "IIS-ApplicationInit","IIS-ASP","IIS-ASPNET45","IIS-BasicAuthentication","IIS-CertProvider", - "IIS-CGI","IIS-ClientCertificateMappingAuthentication","IIS-CommonHttpFeatures","IIS-CustomLogging", - "IIS-DefaultDocument","IIS-DirectoryBrowsing","IIS-DigestAuthentication","IIS-ESP","IIS-FTPServer", - "IIS-FTPExtensibility","IIS-FTPSvc","IIS-HealthAndDiagnostics","IIS-HostableWebCore","IIS-HttpCompressionDynamic", - "IIS-HttpCompressionStatic","IIS-HttpErrors","IIS-HttpLogging","IIS-HttpRedirect","IIS-HttpTracing", - "IIS-IPSecurity","IIS-IIS6ManagementCompatibility","IIS-IISCertificateMappingAuthentication", - "IIS-ISAPIExtensions","IIS-ISAPIFilter","IIS-LoggingLibraries","IIS-ManagementConsole","IIS-ManagementService", - "IIS-ManagementScriptingTools","IIS-Metabase","IIS-NetFxExtensibility","IIS-NetFxExtensibility45", - "IIS-ODBCLogging","IIS-Performance","IIS-RequestFiltering","IIS-RequestMonitor","IIS-Security","IIS-ServerSideIncludes", - "IIS-StaticContent","IIS-URLAuthorization","IIS-WebDAV","IIS-WebServer","IIS-WebServerManagementTools", - "IIS-WebServerRole","IIS-WebSockets","LegacyComponents","MediaPlayback","Microsoft-Hyper-V","Microsoft-Hyper-V-All", - "Microsoft-Hyper-V-Hypervisor","Microsoft-Hyper-V-Management-Clients","Microsoft-Hyper-V-Management-PowerShell", - "Microsoft-Hyper-V-Services","Microsoft-Windows-Subsystem-Linux","MSMQ-ADIntegration","MSMQ-Container","MSMQ-DCOMProxy", - "MSMQ-HTTP","MSMQ-Multicast","MSMQ-Server","MSMQ-Triggers","MultiPoint-Connector","MultiPoint-Connector-Services", - "MultiPoint-Tools","NetFx3","NetFx4-AdvSrvs","NetFx4Extended-ASPNET45","NFS-Administration","Printing-Foundation-Features", - "Printing-Foundation-InternetPrinting-Client","Printing-Foundation-LPDPrintService","Printing-Foundation-LPRPortMonitor", - "Printing-PrintToPDFServices-Features","Printing-XPSServices-Features","SearchEngine-Client-Package", - "ServicesForNFS-ClientOnly","SimpleTCP","SMB1Protocol","SMB1Protocol-Client","SMB1Protocol-Deprecation", - "SMB1Protocol-Server","SmbDirect","TFTP","TelnetClient","TIFFIFilter","VirtualMachinePlatform","WAS-ConfigurationAPI", - "WAS-NetFxEnvironment","WAS-ProcessModel","WAS-WindowsActivationService","WCF-HTTP-Activation","WCF-HTTP-Activation45", - "WCF-MSMQ-Activation45","WCF-MSMQ-Activation","WCF-NonHTTP-Activation","WCF-Pipe-Activation45","WCF-Services45", - "WCF-TCP-Activation45","WCF-TCP-PortSharing45","Windows-Defender-ApplicationGuard", - "Windows-Defender-Default-Definitions","Windows-Identity-Foundation","WindowsMediaPlayer","WorkFolders-Client" + "AppServerClient", "Client-DeviceLockdown", "Client-EmbeddedBootExp", "Client-EmbeddedLogon", + "Client-EmbeddedShellLauncher", "Client-KeyboardFilter", "Client-ProjFS", "Client-UnifiedWriteFilter", + "Containers", "Containers-DisposableClientVM", "Containers-HNS", "Containers-SDN", "DataCenterBridging", + "DirectoryServices-ADAM-Client", "DirectPlay", "HostGuardian", "HypervisorPlatform", "IIS-ApplicationDevelopment", + "IIS-ApplicationInit", "IIS-ASP", "IIS-ASPNET45", "IIS-BasicAuthentication", "IIS-CertProvider", + "IIS-CGI", "IIS-ClientCertificateMappingAuthentication", "IIS-CommonHttpFeatures", "IIS-CustomLogging", + "IIS-DefaultDocument", "IIS-DirectoryBrowsing", "IIS-DigestAuthentication", "IIS-ESP", "IIS-FTPServer", + "IIS-FTPExtensibility", "IIS-FTPSvc", "IIS-HealthAndDiagnostics", "IIS-HostableWebCore", "IIS-HttpCompressionDynamic", + "IIS-HttpCompressionStatic", "IIS-HttpErrors", "IIS-HttpLogging", "IIS-HttpRedirect", "IIS-HttpTracing", + "IIS-IPSecurity", "IIS-IIS6ManagementCompatibility", "IIS-IISCertificateMappingAuthentication", + "IIS-ISAPIExtensions", "IIS-ISAPIFilter", "IIS-LoggingLibraries", "IIS-ManagementConsole", "IIS-ManagementService", + "IIS-ManagementScriptingTools", "IIS-Metabase", "IIS-NetFxExtensibility", "IIS-NetFxExtensibility45", + "IIS-ODBCLogging", "IIS-Performance", "IIS-RequestFiltering", "IIS-RequestMonitor", "IIS-Security", "IIS-ServerSideIncludes", + "IIS-StaticContent", "IIS-URLAuthorization", "IIS-WebDAV", "IIS-WebServer", "IIS-WebServerManagementTools", + "IIS-WebServerRole", "IIS-WebSockets", "LegacyComponents", "MediaPlayback", "Microsoft-Hyper-V", "Microsoft-Hyper-V-All", + "Microsoft-Hyper-V-Hypervisor", "Microsoft-Hyper-V-Management-Clients", "Microsoft-Hyper-V-Management-PowerShell", + "Microsoft-Hyper-V-Services", "Microsoft-Windows-Subsystem-Linux", "MSMQ-ADIntegration", "MSMQ-Container", "MSMQ-DCOMProxy", + "MSMQ-HTTP", "MSMQ-Multicast", "MSMQ-Server", "MSMQ-Triggers", "MultiPoint-Connector", "MultiPoint-Connector-Services", + "MultiPoint-Tools", "NetFx3", "NetFx4-AdvSrvs", "NetFx4Extended-ASPNET45", "NFS-Administration", "Printing-Foundation-Features", + "Printing-Foundation-InternetPrinting-Client", "Printing-Foundation-LPDPrintService", "Printing-Foundation-LPRPortMonitor", + "Printing-PrintToPDFServices-Features", "Printing-XPSServices-Features", "SearchEngine-Client-Package", + "ServicesForNFS-ClientOnly", "SimpleTCP", "SMB1Protocol", "SMB1Protocol-Client", "SMB1Protocol-Deprecation", + "SMB1Protocol-Server", "SmbDirect", "TFTP", "TelnetClient", "TIFFIFilter", "VirtualMachinePlatform", "WAS-ConfigurationAPI", + "WAS-NetFxEnvironment", "WAS-ProcessModel", "WAS-WindowsActivationService", "WCF-HTTP-Activation", "WCF-HTTP-Activation45", + "WCF-MSMQ-Activation45", "WCF-MSMQ-Activation", "WCF-NonHTTP-Activation", "WCF-Pipe-Activation45", "WCF-Services45", + "WCF-TCP-Activation45", "WCF-TCP-PortSharing45", "Windows-Defender-ApplicationGuard", + "Windows-Defender-Default-Definitions", "Windows-Identity-Foundation", "WindowsMediaPlayer", "WorkFolders-Client" ) $skuList = @( - 'Home','Home N','Home Single Language','Education','Education N','Pro', - 'Pro N','Pro Education','Pro Education N','Pro for Workstations', - 'Pro N for Workstations','Enterprise','Enterprise N','Standard', - 'Standard (Desktop Experience)','Datacenter','Datacenter (Desktop Experience)' + 'Home', 'Home N', 'Home Single Language', 'Education', 'Education N', 'Pro', + 'Pro N', 'Pro Education', 'Pro Education N', 'Pro for Workstations', + 'Pro N for Workstations', 'Enterprise', 'Enterprise N', 'Standard', + 'Standard (Desktop Experience)', 'Datacenter', 'Datacenter (Desktop Experience)' ) # Full list of Windows releases (if ISO path != blank) $allWindowsReleases = @( - [PSCustomObject]@{ Display = "Windows 10"; Value = 10 }, - [PSCustomObject]@{ Display = "Windows 11"; Value = 11 }, - [PSCustomObject]@{ Display = "Windows Server 2016"; Value = 2016 }, - [PSCustomObject]@{ Display = "Windows Server 2019"; Value = 2019 }, - [PSCustomObject]@{ Display = "Windows Server 2022"; Value = 2022 }, - [PSCustomObject]@{ Display = "Windows Server 2025"; Value = 2025 } + [PSCustomObject]@{ Display = "Windows 10"; Value = 10 }, + [PSCustomObject]@{ Display = "Windows 11"; Value = 11 }, + [PSCustomObject]@{ Display = "Windows Server 2016"; Value = 2016 }, + [PSCustomObject]@{ Display = "Windows Server 2019"; Value = 2019 }, + [PSCustomObject]@{ Display = "Windows Server 2022"; Value = 2022 }, + [PSCustomObject]@{ Display = "Windows Server 2025"; Value = 2025 } ) # Subset for MCT (if ISO path is blank) @@ -267,12 +266,12 @@ $mctWindowsReleases = @( # Windows version sets $windowsVersionMap = @{ - 10 = @("22H2") - 11 = @("22H2","23H2","24H2") - 2016 = @("1607") - 2019 = @("1809") - 2022 = @("21H2") - 2025 = @("24H2") + 10 = @("22H2") + 11 = @("22H2", "23H2", "24H2") + 2016 = @("1607") + 2019 = @("1809") + 2022 = @("21H2") + 2025 = @("24H2") } # -------------------------------------------------------------------------- @@ -281,69 +280,70 @@ $windowsVersionMap = @{ function Get-UIConfig { # Create hash to store configuration $config = [ordered]@{ - AllowExternalHardDiskMedia = $window.FindName('chkAllowExternalHardDiskMedia').IsChecked - AllowVHDXCaching = $window.FindName('chkAllowVHDXCaching').IsChecked - BuildUSBDrive = $window.FindName('chkBuildUSBDriveEnable').IsChecked - CleanupAppsISO = $window.FindName('chkCleanupAppsISO').IsChecked - CleanupCaptureISO = $window.FindName('chkCleanupCaptureISO').IsChecked - CleanupDeployISO = $window.FindName('chkCleanupDeployISO').IsChecked - CleanupDrivers = $window.FindName('chkCleanupDrivers').IsChecked - CompactOS = $window.FindName('chkCompactOS').IsChecked - CopyAutopilot = $window.FindName('chkCopyAutopilot').IsChecked - CopyDrivers = $window.FindName('chkCopyDrivers').IsChecked - CopyOfficeConfigXML = $window.FindName('chkCopyOfficeConfigXML').IsChecked - CopyPEDrivers = $window.FindName('chkCopyPEDrivers').IsChecked - CopyPPKG = $window.FindName('chkCopyPPKG').IsChecked - CopyUnattend = $window.FindName('chkCopyUnattend').IsChecked - CreateCaptureMedia = $window.FindName('chkCreateCaptureMedia').IsChecked - CreateDeploymentMedia = $window.FindName('chkCreateDeploymentMedia').IsChecked - CustomFFUNameTemplate = $window.FindName('txtCustomFFUNameTemplate').Text - DiskSize = [int64]$window.FindName('txtDiskSize').Text * 1GB - DownloadDrivers = $window.FindName('chkDownloadDrivers').IsChecked - DriversFolder = $window.FindName('txtDriversFolder').Text - FFUCaptureLocation = $window.FindName('txtFFUCaptureLocation').Text - FFUDevelopmentPath = $window.FindName('txtFFUDevPath').Text - FFUPrefix = $window.FindName('txtVMNamePrefix').Text - InstallApps = $window.FindName('chkInstallApps').IsChecked - InstallDrivers = $window.FindName('chkInstallDrivers').IsChecked - InstallOffice = $window.FindName('chkInstallOffice').IsChecked - InstallWingetApps = $window.FindName('chkInstallWingetApps').IsChecked - ISOPath = $window.FindName('txtISOPath').Text - LogicalSectorSizeBytes = [int]$window.FindName('cmbLogicalSectorSize').SelectedItem.Content - Make = $window.FindName('cmbMake').SelectedItem - MediaType = $window.FindName('cmbMediaType').SelectedItem - Memory = [int64]$window.FindName('txtMemory').Text * 1GB - Model = $window.FindName('cmbModel').Text - OfficeConfigXMLFile = $window.FindName('txtOfficeConfigXMLFilePath').Text - OfficePath = $window.FindName('txtOfficePath').Text - Optimize = $window.FindName('chkOptimize').IsChecked - PEDriversFolder = $window.FindName('txtPEDriversFolder').Text - ProductKey = $window.FindName('txtProductKey').Text + AllowExternalHardDiskMedia = $window.FindName('chkAllowExternalHardDiskMedia').IsChecked + AllowVHDXCaching = $window.FindName('chkAllowVHDXCaching').IsChecked + BuildUSBDrive = $window.FindName('chkBuildUSBDriveEnable').IsChecked + CleanupAppsISO = $window.FindName('chkCleanupAppsISO').IsChecked + CleanupCaptureISO = $window.FindName('chkCleanupCaptureISO').IsChecked + CleanupDeployISO = $window.FindName('chkCleanupDeployISO').IsChecked + CleanupDrivers = $window.FindName('chkCleanupDrivers').IsChecked + CompactOS = $window.FindName('chkCompactOS').IsChecked + CopyAutopilot = $window.FindName('chkCopyAutopilot').IsChecked + CopyDrivers = $window.FindName('chkCopyDrivers').IsChecked + CopyOfficeConfigXML = $window.FindName('chkCopyOfficeConfigXML').IsChecked + CopyPEDrivers = $window.FindName('chkCopyPEDrivers').IsChecked + CopyPPKG = $window.FindName('chkCopyPPKG').IsChecked + CopyUnattend = $window.FindName('chkCopyUnattend').IsChecked + CreateCaptureMedia = $window.FindName('chkCreateCaptureMedia').IsChecked + CreateDeploymentMedia = $window.FindName('chkCreateDeploymentMedia').IsChecked + CustomFFUNameTemplate = $window.FindName('txtCustomFFUNameTemplate').Text + DiskSize = [int64]$window.FindName('txtDiskSize').Text * 1GB + DownloadDrivers = $window.FindName('chkDownloadDrivers').IsChecked + DriversFolder = $window.FindName('txtDriversFolder').Text + FFUCaptureLocation = $window.FindName('txtFFUCaptureLocation').Text + FFUDevelopmentPath = $window.FindName('txtFFUDevPath').Text + FFUPrefix = $window.FindName('txtVMNamePrefix').Text + InstallApps = $window.FindName('chkInstallApps').IsChecked + InstallDrivers = $window.FindName('chkInstallDrivers').IsChecked + InstallOffice = $window.FindName('chkInstallOffice').IsChecked + InstallWingetApps = $window.FindName('chkInstallWingetApps').IsChecked + ISOPath = $window.FindName('txtISOPath').Text + LogicalSectorSizeBytes = [int]$window.FindName('cmbLogicalSectorSize').SelectedItem.Content + Make = $window.FindName('cmbMake').SelectedItem + MediaType = $window.FindName('cmbMediaType').SelectedItem + Memory = [int64]$window.FindName('txtMemory').Text * 1GB + Model = $window.FindName('cmbModel').Text + OfficeConfigXMLFile = $window.FindName('txtOfficeConfigXMLFilePath').Text + OfficePath = $window.FindName('txtOfficePath').Text + Optimize = $window.FindName('chkOptimize').IsChecked + PEDriversFolder = $window.FindName('txtPEDriversFolder').Text + ProductKey = $window.FindName('txtProductKey').Text PromptExternalHardDiskMedia = $window.FindName('chkPromptExternalHardDiskMedia').IsChecked - Processors = [int]$window.FindName('txtProcessors').Text - RemoveFFU = $window.FindName('chkRemoveFFU').IsChecked - ShareName = $window.FindName('txtShareName').Text - UpdateEdge = $window.FindName('chkUpdateEdge').IsChecked - UpdateLatestCU = $window.FindName('chkUpdateLatestCU').IsChecked - UpdateLatestDefender = $window.FindName('chkUpdateLatestDefender').IsChecked - UpdateLatestMSRT = $window.FindName('chkUpdateLatestMSRT').IsChecked - UpdateLatestNet = $window.FindName('chkUpdateLatestNet').IsChecked - UpdateOneDrive = $window.FindName('chkUpdateOneDrive').IsChecked - UpdatePreviewCU = $window.FindName('chkUpdatePreviewCU').IsChecked - USBDriveList = @{} - Username = $window.FindName('txtUsername').Text - VMHostIPAddress = $window.FindName('txtVMHostIPAddress').Text - VMLocation = $window.FindName('txtVMLocation').Text - VMSwitchName = if ($window.FindName('cmbVMSwitchName').SelectedItem -eq 'Other') { + Processors = [int]$window.FindName('txtProcessors').Text + RemoveFFU = $window.FindName('chkRemoveFFU').IsChecked + ShareName = $window.FindName('txtShareName').Text + UpdateEdge = $window.FindName('chkUpdateEdge').IsChecked + UpdateLatestCU = $window.FindName('chkUpdateLatestCU').IsChecked + UpdateLatestDefender = $window.FindName('chkUpdateLatestDefender').IsChecked + UpdateLatestMSRT = $window.FindName('chkUpdateLatestMSRT').IsChecked + UpdateLatestNet = $window.FindName('chkUpdateLatestNet').IsChecked + UpdateOneDrive = $window.FindName('chkUpdateOneDrive').IsChecked + UpdatePreviewCU = $window.FindName('chkUpdatePreviewCU').IsChecked + USBDriveList = @{} + Username = $window.FindName('txtUsername').Text + VMHostIPAddress = $window.FindName('txtVMHostIPAddress').Text + VMLocation = $window.FindName('txtVMLocation').Text + VMSwitchName = if ($window.FindName('cmbVMSwitchName').SelectedItem -eq 'Other') { $window.FindName('txtCustomVMSwitchName').Text - } else { + } + else { $window.FindName('cmbVMSwitchName').SelectedItem } - WindowsArch = $window.FindName('cmbWindowsArch').SelectedItem - WindowsLang = $window.FindName('cmbWindowsLang').SelectedItem - WindowsRelease = [int]$window.FindName('cmbWindowsRelease').SelectedItem.Value - WindowsSKU = $window.FindName('cmbWindowsSKU').SelectedItem - WindowsVersion = $window.FindName('cmbWindowsVersion').SelectedItem + WindowsArch = $window.FindName('cmbWindowsArch').SelectedItem + WindowsLang = $window.FindName('cmbWindowsLang').SelectedItem + WindowsRelease = [int]$window.FindName('cmbWindowsRelease').SelectedItem.Value + WindowsSKU = $window.FindName('cmbWindowsSKU').SelectedItem + WindowsVersion = $window.FindName('cmbWindowsVersion').SelectedItem } # Add selected USB drives to the config @@ -390,8 +390,8 @@ function UpdateWindowsVersionCombo { $validVersions = $windowsVersionMap[$selectedRelease] if ([string]::IsNullOrEmpty($isoPath)) { switch ($selectedRelease) { - 10 { $default = "22H2" } - 11 { $default = "24H2" } + 10 { $default = "22H2" } + 11 { $default = "24H2" } 2016 { $default = "1607" } 2019 { $default = "1809" } 2022 { $default = "21H2" } @@ -420,7 +420,7 @@ $script:RefreshWindowsUI = { } Add-Type -AssemblyName WindowsBase -Add-Type -AssemblyName PresentationCore,PresentationFramework +Add-Type -AssemblyName PresentationCore, PresentationFramework Add-Type -AssemblyName System.Windows.Forms # Load XAML @@ -495,7 +495,8 @@ $script:UpdateInstallAppsBasedOnUpdates = { } $window.FindName('chkInstallApps').IsChecked = $true $window.FindName('chkInstallApps').IsEnabled = $false - } else { + } + else { if ($script:installAppsForcedByUpdates) { $window.FindName('chkInstallApps').IsChecked = $script:prevInstallAppsStateBeforeUpdates $script:installAppsForcedByUpdates = $false @@ -543,11 +544,11 @@ function Add-SortableColumn { $column.DisplayMemberBinding = New-Object System.Windows.Data.Binding($binding) } - # Create the header with sorting support - $columnHeader = New-Object System.Windows.Controls.GridViewColumnHeader - $columnHeader.Content = $header - $columnHeader.Tag = $binding - $column.Header = $columnHeader + # Create a header with the binding information stored in its Tag + $headerTemplate = New-Object System.Windows.Controls.GridViewColumnHeader + $headerTemplate.Content = $header + $headerTemplate.Tag = $binding + $column.Header = $headerTemplate if ($width -ne 'Auto') { $column.Width = $width @@ -570,12 +571,13 @@ function Invoke-ListViewSort { # Toggle sort direction if clicking the same column if ($script:lastSortProperty -eq $property) { $script:lastSortAscending = -not $script:lastSortAscending - } else { + } + else { $script:lastSortAscending = $true } $script:lastSortProperty = $property - # Convert ListView items to array and split into selected and unselected + # Store selected items $items = @($listView.Items) $selectedItems = @($items | Where-Object { $_.IsSelected }) $unselectedItems = @($items | Where-Object { -not $_.IsSelected }) @@ -583,495 +585,496 @@ function Invoke-ListViewSort { # Sort unselected items $sortedUnselected = if ($script:lastSortAscending) { @($unselectedItems | Sort-Object -Property $property) - } else { - @($unselectedUnselected | Sort-Object -Property $property -Descending) + } + else { + @($unselectedItems | Sort-Object -Property $property -Descending) } # Clear and repopulate ListView $listView.Items.Clear() - # Add items back in correct order (selected first, then sorted unselected) + # Add selected items first foreach ($item in $selectedItems) { - $listView.Items.Add($item) + [void]$listView.Items.Add($item) } + + # Add sorted unselected items foreach ($item in $sortedUnselected) { - $listView.Items.Add($item) + [void]$listView.Items.Add($item) } } # Fix event handler parameter names to avoid $eventArgs conflicts $window.Add_Loaded({ - $script:cmbWindowsRelease = $window.FindName('cmbWindowsRelease') - $script:cmbWindowsVersion = $window.FindName('cmbWindowsVersion') - $script:txtISOPath = $window.FindName('txtISOPath') - & $script:RefreshWindowsUI($defaultISOPath) - $script:txtISOPath.Add_TextChanged({ & $script:RefreshWindowsUI($script:txtISOPath.Text) }) - $script:cmbWindowsRelease.Add_SelectionChanged({ - $selItem = $script:cmbWindowsRelease.SelectedItem - if ($selItem -and $selItem.Value) { UpdateWindowsVersionCombo -selectedRelease $selItem.Value -isoPath $script:txtISOPath.Text } - }) - $script:btnBrowseISO = $window.FindName('btnBrowseISO') - $script: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:txtISOPath.Text = $ofd.FileName } - }) - # Home tab (renamed from Basic) displays static welcome text. - # Build tab defaults - $window.FindName('txtFFUDevPath').Text = $FFUDevelopmentPath - $window.FindName('txtCustomFFUNameTemplate').Text = "{WindowsRelease}_{WindowsVersion}_{SKU}_{yyyy}-{MM}-{dd}_{HH}{mm}" - $window.FindName('txtFFUCaptureLocation').Text = (Join-Path $FFUDevelopmentPath "FFU") - $window.FindName('txtShareName').Text = "FFUCaptureShare" - $window.FindName('txtUsername').Text = "ffu_user" - # Set VM Location default to $FFUDevelopmentPath\VM - $window.FindName('txtVMLocation').Text = (Join-Path $FFUDevelopmentPath "VM") - # <-- NEW: Set the default for the new VM Name Prefix textbox on the Hyper-V Settings tab - $window.FindName('txtVMNamePrefix').Text = $defaultFFUPrefix - # Hyper-V Settings: Populate defaults (Share Name and Username now on Build, so only populate remaining fields) - $script:vmSwitchMap = @{} - $script:allSwitches = Get-VMSwitch -ErrorAction SilentlyContinue - $script:cmbVMSwitchName = $window.FindName('cmbVMSwitchName') - foreach ($sw in $script:allSwitches) { - $script:cmbVMSwitchName.Items.Add($sw.Name) | Out-Null - $na = Get-NetAdapter -ErrorAction SilentlyContinue | Where-Object { $_.Name -like "*($($sw.Name))*" } - if ($na) { - $netIPs = Get-NetIPAddress -InterfaceIndex $na.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue - $validIPs = $netIPs | Where-Object { $_.IPAddress -notlike '169.254.*' -and $_.IPAddress } - if ($validIPs) { $script:vmSwitchMap[$sw.Name] = ($validIPs | Select-Object -First 1).IPAddress } + $script:cmbWindowsRelease = $window.FindName('cmbWindowsRelease') + $script:cmbWindowsVersion = $window.FindName('cmbWindowsVersion') + $script:txtISOPath = $window.FindName('txtISOPath') + & $script:RefreshWindowsUI($defaultISOPath) + $script:txtISOPath.Add_TextChanged({ & $script:RefreshWindowsUI($script:txtISOPath.Text) }) + $script:cmbWindowsRelease.Add_SelectionChanged({ + $selItem = $script:cmbWindowsRelease.SelectedItem + if ($selItem -and $selItem.Value) { UpdateWindowsVersionCombo -selectedRelease $selItem.Value -isoPath $script:txtISOPath.Text } + }) + $script:btnBrowseISO = $window.FindName('btnBrowseISO') + $script: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:txtISOPath.Text = $ofd.FileName } + }) + # Home tab (renamed from Basic) displays static welcome text. + # Build tab defaults + $window.FindName('txtFFUDevPath').Text = $FFUDevelopmentPath + $window.FindName('txtCustomFFUNameTemplate').Text = "{WindowsRelease}_{WindowsVersion}_{SKU}_{yyyy}-{MM}-{dd}_{HH}{mm}" + $window.FindName('txtFFUCaptureLocation').Text = (Join-Path $FFUDevelopmentPath "FFU") + $window.FindName('txtShareName').Text = "FFUCaptureShare" + $window.FindName('txtUsername').Text = "ffu_user" + # Set VM Location default to $FFUDevelopmentPath\VM + $window.FindName('txtVMLocation').Text = (Join-Path $FFUDevelopmentPath "VM") + # <-- NEW: Set the default for the new VM Name Prefix textbox on the Hyper-V Settings tab + $window.FindName('txtVMNamePrefix').Text = $defaultFFUPrefix + # Hyper-V Settings: Populate defaults (Share Name and Username now on Build, so only populate remaining fields) + $script:vmSwitchMap = @{} + $script:allSwitches = Get-VMSwitch -ErrorAction SilentlyContinue + $script:cmbVMSwitchName = $window.FindName('cmbVMSwitchName') + foreach ($sw in $script:allSwitches) { + $script:cmbVMSwitchName.Items.Add($sw.Name) | Out-Null + $na = Get-NetAdapter -ErrorAction SilentlyContinue | Where-Object { $_.Name -like "*($($sw.Name))*" } + if ($na) { + $netIPs = Get-NetIPAddress -InterfaceIndex $na.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue + $validIPs = $netIPs | Where-Object { $_.IPAddress -notlike '169.254.*' -and $_.IPAddress } + if ($validIPs) { $script:vmSwitchMap[$sw.Name] = ($validIPs | Select-Object -First 1).IPAddress } + } } - } - $script:cmbVMSwitchName.Items.Add('Other') | Out-Null - if ($script:cmbVMSwitchName.Items.Count -gt 0) { - if ($script:allSwitches.Count -gt 0) { - $script:cmbVMSwitchName.SelectedIndex = 0 - $first = $script:cmbVMSwitchName.SelectedItem - if ($script:vmSwitchMap.ContainsKey($first)) { $window.FindName('txtVMHostIPAddress').Text = $script:vmSwitchMap[$first] } - else { $window.FindName('txtVMHostIPAddress').Text = '' } + $script:cmbVMSwitchName.Items.Add('Other') | Out-Null + if ($script:cmbVMSwitchName.Items.Count -gt 0) { + if ($script:allSwitches.Count -gt 0) { + $script:cmbVMSwitchName.SelectedIndex = 0 + $first = $script:cmbVMSwitchName.SelectedItem + if ($script:vmSwitchMap.ContainsKey($first)) { $window.FindName('txtVMHostIPAddress').Text = $script:vmSwitchMap[$first] } + else { $window.FindName('txtVMHostIPAddress').Text = '' } + } + else { + $script:cmbVMSwitchName.SelectedItem = 'Other' + $window.FindName('txtCustomVMSwitchName').Visibility = 'Visible' + } + } + $script:cmbVMSwitchName.Add_SelectionChanged({ + if ($_.AddedItems -contains 'Other') { + $window.FindName('txtCustomVMSwitchName').Visibility = 'Visible' + $window.FindName('txtVMHostIPAddress').Text = '' + } + else { + $window.FindName('txtCustomVMSwitchName').Visibility = 'Collapsed' + $sel = $_.AddedItems[0] + if ($script:vmSwitchMap.ContainsKey($sel)) { $window.FindName('txtVMHostIPAddress').Text = $script:vmSwitchMap[$sel] } + else { $window.FindName('txtVMHostIPAddress').Text = '' } + } + }) + # Windows Arch, Lang, SKU, etc. + $script:cmbWindowsArch = $window.FindName('cmbWindowsArch') + foreach ($a in 'x86', 'x64', 'arm64') { [void]$script:cmbWindowsArch.Items.Add($a) } + $script:cmbWindowsArch.SelectedItem = $defaultWindowsArch + $script:cmbWindowsLang = $window.FindName('cmbWindowsLang') + $allowedLangs = @( + 'ar-sa', 'bg-bg', 'cs-cz', 'da-dk', 'de-de', 'el-gr', 'en-gb', 'en-us', 'es-es', 'es-mx', 'et-ee', + 'fi-fi', 'fr-ca', 'fr-fr', 'he-il', 'hr-hr', 'hu-hu', 'it-it', 'ja-jp', 'ko-kr', 'lt-lt', 'lv-lv', + 'nb-no', 'nl-nl', 'pl-pl', 'pt-br', 'pt-pt', 'ro-ro', 'ru-ru', 'sk-sk', 'sl-si', 'sr-latn-rs', + 'sv-se', 'th-th', 'tr-tr', 'uk-ua', 'zh-cn', 'zh-tw' + ) + foreach ($lang in $allowedLangs) { [void]$script:cmbWindowsLang.Items.Add($lang) } + $script:cmbWindowsLang.SelectedItem = $defaultWindowsLang + $script:cmbWindowsSKU = $window.FindName('cmbWindowsSKU') + $script:cmbWindowsSKU.Items.Clear() + foreach ($sku in $skuList) { [void]$script:cmbWindowsSKU.Items.Add($sku) } + $script:cmbWindowsSKU.SelectedItem = $defaultWindowsSKU + $script:cmbMediaType = $window.FindName('cmbMediaType') + foreach ($mt in "Consumer", "Business") { [void]$script:cmbMediaType.Items.Add($mt) } # updated options + $script:cmbMediaType.SelectedItem = $defaultMediaType + $script:txtOptionalFeatures = $window.FindName('txtOptionalFeatures') + $script:txtOptionalFeatures.Text = $defaultOptionalFeatures + $window.FindName('txtProductKey').Text = $defaultProductKey + # Drivers tab + $script:chkDownloadDrivers = $window.FindName('chkDownloadDrivers') + $script:cmbMake = $window.FindName('cmbMake') + $script:cmbModel = $window.FindName('cmbModel') + $script:spMakeModelSection = $window.FindName('spMakeModelSection') + $makeList = @('Microsoft', 'Dell', 'HP', 'Lenovo') + foreach ($m in $makeList) { [void]$script:cmbMake.Items.Add($m) } + if ($script:cmbMake.Items.Count -gt 0) { $script:cmbMake.SelectedIndex = 0 } + $script:chkDownloadDrivers.Add_Checked({ + $script:cmbMake.Visibility = 'Visible' + $script:cmbModel.Visibility = 'Visible' + $script:spMakeModelSection.Visibility = 'Visible' + }) + $script:chkDownloadDrivers.Add_Unchecked({ + $script:cmbMake.Visibility = 'Collapsed' + $script:cmbModel.Visibility = 'Collapsed' + $script:spMakeModelSection.Visibility = 'Collapsed' + }) + # Office interplay + $script:chkInstallOffice = $window.FindName('chkInstallOffice') + $script:chkInstallApps = $window.FindName('chkInstallApps') + $script:installAppsCheckedByOffice = $false + $script:OfficePathStackPanel = $window.FindName('OfficePathStackPanel') + $script:OfficePathGrid = $window.FindName('OfficePathGrid') + $script:CopyOfficeConfigXMLStackPanel = $window.FindName('CopyOfficeConfigXMLStackPanel') + $script:OfficeConfigurationXMLFileStackPanel = $window.FindName('OfficeConfigurationXMLFileStackPanel') + $script:OfficeConfigurationXMLFileGrid = $window.FindName('OfficeConfigurationXMLFileGrid') + $script:chkCopyOfficeConfigXML = $window.FindName('chkCopyOfficeConfigXML') + if ($script:chkInstallOffice.IsChecked) { + $script:OfficePathStackPanel.Visibility = 'Visible' + $script:OfficePathGrid.Visibility = 'Visible' + $script:CopyOfficeConfigXMLStackPanel.Visibility = 'Visible' } else { - $script:cmbVMSwitchName.SelectedItem = 'Other' - $window.FindName('txtCustomVMSwitchName').Visibility = 'Visible' + $script:OfficePathStackPanel.Visibility = 'Collapsed' + $script:OfficePathGrid.Visibility = 'Collapsed' + $script:CopyOfficeConfigXMLStackPanel.Visibility = 'Collapsed' + $script:OfficeConfigurationXMLFileStackPanel.Visibility = 'Collapsed' + $script:OfficeConfigurationXMLFileGrid.Visibility = 'Collapsed' } - } - $script:cmbVMSwitchName.Add_SelectionChanged({ - if ($_.AddedItems -contains 'Other') { - $window.FindName('txtCustomVMSwitchName').Visibility = 'Visible' - $window.FindName('txtVMHostIPAddress').Text = '' - } - else { - $window.FindName('txtCustomVMSwitchName').Visibility = 'Collapsed' - $sel = $_.AddedItems[0] - if ($script:vmSwitchMap.ContainsKey($sel)) { $window.FindName('txtVMHostIPAddress').Text = $script:vmSwitchMap[$sel] } - else { $window.FindName('txtVMHostIPAddress').Text = '' } - } - }) - # Windows Arch, Lang, SKU, etc. - $script:cmbWindowsArch = $window.FindName('cmbWindowsArch') - foreach ($a in 'x86','x64','arm64') { [void]$script:cmbWindowsArch.Items.Add($a) } - $script:cmbWindowsArch.SelectedItem = $defaultWindowsArch - $script:cmbWindowsLang = $window.FindName('cmbWindowsLang') - $allowedLangs = @( - 'ar-sa','bg-bg','cs-cz','da-dk','de-de','el-gr','en-gb','en-us','es-es','es-mx','et-ee', - 'fi-fi','fr-ca','fr-fr','he-il','hr-hr','hu-hu','it-it','ja-jp','ko-kr','lt-lt','lv-lv', - 'nb-no','nl-nl','pl-pl','pt-br','pt-pt','ro-ro','ru-ru','sk-sk','sl-si','sr-latn-rs', - 'sv-se','th-th','tr-tr','uk-ua','zh-cn','zh-tw' - ) - foreach ($lang in $allowedLangs) { [void]$script:cmbWindowsLang.Items.Add($lang) } - $script:cmbWindowsLang.SelectedItem = $defaultWindowsLang - $script:cmbWindowsSKU = $window.FindName('cmbWindowsSKU') - $script:cmbWindowsSKU.Items.Clear() - foreach ($sku in $skuList) { [void]$script:cmbWindowsSKU.Items.Add($sku) } - $script:cmbWindowsSKU.SelectedItem = $defaultWindowsSKU - $script:cmbMediaType = $window.FindName('cmbMediaType') - foreach ($mt in "Consumer","Business") { [void]$script:cmbMediaType.Items.Add($mt) } # updated options - $script:cmbMediaType.SelectedItem = $defaultMediaType - $script:txtOptionalFeatures = $window.FindName('txtOptionalFeatures') - $script:txtOptionalFeatures.Text = $defaultOptionalFeatures - $window.FindName('txtProductKey').Text = $defaultProductKey - # Drivers tab - $script:chkDownloadDrivers = $window.FindName('chkDownloadDrivers') - $script:cmbMake = $window.FindName('cmbMake') - $script:cmbModel = $window.FindName('cmbModel') - $script:spMakeModelSection = $window.FindName('spMakeModelSection') - $makeList = @('Microsoft','Dell','HP','Lenovo') - foreach ($m in $makeList) { [void]$script:cmbMake.Items.Add($m) } - if ($script:cmbMake.Items.Count -gt 0) { $script:cmbMake.SelectedIndex = 0 } - $script:chkDownloadDrivers.Add_Checked({ - $script:cmbMake.Visibility = 'Visible' - $script:cmbModel.Visibility = 'Visible' - $script:spMakeModelSection.Visibility = 'Visible' - }) - $script:chkDownloadDrivers.Add_Unchecked({ - $script:cmbMake.Visibility = 'Collapsed' - $script:cmbModel.Visibility = 'Collapsed' - $script:spMakeModelSection.Visibility = 'Collapsed' - }) - # Office interplay - $script:chkInstallOffice = $window.FindName('chkInstallOffice') - $script:chkInstallApps = $window.FindName('chkInstallApps') - $script:installAppsCheckedByOffice = $false - $script:OfficePathStackPanel = $window.FindName('OfficePathStackPanel') - $script:OfficePathGrid = $window.FindName('OfficePathGrid') - $script:CopyOfficeConfigXMLStackPanel = $window.FindName('CopyOfficeConfigXMLStackPanel') - $script:OfficeConfigurationXMLFileStackPanel = $window.FindName('OfficeConfigurationXMLFileStackPanel') - $script:OfficeConfigurationXMLFileGrid = $window.FindName('OfficeConfigurationXMLFileGrid') - $script:chkCopyOfficeConfigXML = $window.FindName('chkCopyOfficeConfigXML') - if ($script:chkInstallOffice.IsChecked) { - $script:OfficePathStackPanel.Visibility = 'Visible' - $script:OfficePathGrid.Visibility = 'Visible' - $script:CopyOfficeConfigXMLStackPanel.Visibility = 'Visible' - } - else { - $script:OfficePathStackPanel.Visibility = 'Collapsed' - $script:OfficePathGrid.Visibility = 'Collapsed' - $script:CopyOfficeConfigXMLStackPanel.Visibility = 'Collapsed' - $script:OfficeConfigurationXMLFileStackPanel.Visibility = 'Collapsed' - $script:OfficeConfigurationXMLFileGrid.Visibility = 'Collapsed' - } - $script:chkInstallOffice.Add_Checked({ - if (-not $script:chkInstallApps.IsChecked) { - $script:chkInstallApps.IsChecked = $true - $script:installAppsCheckedByOffice = $true - } - $script:chkInstallApps.IsEnabled = $false - $script:OfficePathStackPanel.Visibility = 'Visible' - $script:OfficePathGrid.Visibility = 'Visible' - $script:CopyOfficeConfigXMLStackPanel.Visibility = 'Visible' - }) - $script:chkInstallOffice.Add_Unchecked({ - if ($script:installAppsCheckedByOffice) { - $script:chkInstallApps.IsChecked = $false - $script:installAppsCheckedByOffice = $false - } - $script:chkInstallApps.IsEnabled = $true - $script:OfficePathStackPanel.Visibility = 'Collapsed' - $script:OfficePathGrid.Visibility = 'Collapsed' - $script:CopyOfficeConfigXMLStackPanel.Visibility = 'Collapsed' - $script:OfficeConfigurationXMLFileStackPanel.Visibility = 'Collapsed' - $script:OfficeConfigurationXMLFileGrid.Visibility = 'Collapsed' - }) - $script:chkCopyOfficeConfigXML.Add_Checked({ - $script:OfficeConfigurationXMLFileStackPanel.Visibility = 'Visible' - $script:OfficeConfigurationXMLFileGrid.Visibility = 'Visible' - }) - $script:chkCopyOfficeConfigXML.Add_Unchecked({ - $script:OfficeConfigurationXMLFileStackPanel.Visibility = 'Collapsed' - $script:OfficeConfigurationXMLFileGrid.Visibility = 'Collapsed' - }) - # Build dynamic multi-column checkboxes for optional features in Windows Settings tab - $script:featuresPanel = $window.FindName('stackFeaturesContainer') - if ($script:featuresPanel) { BuildFeaturesGrid -parent $script:featuresPanel } - # Variables for managing forced Install Apps state due to Updates - $script:installAppsForcedByUpdates = $false - $script:prevInstallAppsStateBeforeUpdates = $null - # Define the function in script scope to update Install Apps based on Updates tab - $script:UpdateInstallAppsBasedOnUpdates = { - $anyUpdateChecked = $window.FindName('chkUpdateLatestDefender').IsChecked -or $window.FindName('chkUpdateEdge').IsChecked -or $window.FindName('chkUpdateOneDrive').IsChecked -or $window.FindName('chkUpdateLatestMSRT').IsChecked - if ($anyUpdateChecked) { - if (-not $script:installAppsForcedByUpdates) { - $script:prevInstallAppsStateBeforeUpdates = $window.FindName('chkInstallApps').IsChecked - $script:installAppsForcedByUpdates = $true + $script:chkInstallOffice.Add_Checked({ + if (-not $script:chkInstallApps.IsChecked) { + $script:chkInstallApps.IsChecked = $true + $script:installAppsCheckedByOffice = $true + } + $script:chkInstallApps.IsEnabled = $false + $script:OfficePathStackPanel.Visibility = 'Visible' + $script:OfficePathGrid.Visibility = 'Visible' + $script:CopyOfficeConfigXMLStackPanel.Visibility = 'Visible' + }) + $script:chkInstallOffice.Add_Unchecked({ + if ($script:installAppsCheckedByOffice) { + $script:chkInstallApps.IsChecked = $false + $script:installAppsCheckedByOffice = $false + } + $script:chkInstallApps.IsEnabled = $true + $script:OfficePathStackPanel.Visibility = 'Collapsed' + $script:OfficePathGrid.Visibility = 'Collapsed' + $script:CopyOfficeConfigXMLStackPanel.Visibility = 'Collapsed' + $script:OfficeConfigurationXMLFileStackPanel.Visibility = 'Collapsed' + $script:OfficeConfigurationXMLFileGrid.Visibility = 'Collapsed' + }) + $script:chkCopyOfficeConfigXML.Add_Checked({ + $script:OfficeConfigurationXMLFileStackPanel.Visibility = 'Visible' + $script:OfficeConfigurationXMLFileGrid.Visibility = 'Visible' + }) + $script:chkCopyOfficeConfigXML.Add_Unchecked({ + $script:OfficeConfigurationXMLFileStackPanel.Visibility = 'Collapsed' + $script:OfficeConfigurationXMLFileGrid.Visibility = 'Collapsed' + }) + # Build dynamic multi-column checkboxes for optional features in Windows Settings tab + $script:featuresPanel = $window.FindName('stackFeaturesContainer') + if ($script:featuresPanel) { BuildFeaturesGrid -parent $script:featuresPanel } + # Variables for managing forced Install Apps state due to Updates + $script:installAppsForcedByUpdates = $false + $script:prevInstallAppsStateBeforeUpdates = $null + # Define the function in script scope to update Install Apps based on Updates tab + $script:UpdateInstallAppsBasedOnUpdates = { + $anyUpdateChecked = $window.FindName('chkUpdateLatestDefender').IsChecked -or $window.FindName('chkUpdateEdge').IsChecked -or $window.FindName('chkUpdateOneDrive').IsChecked -or $window.FindName('chkUpdateLatestMSRT').IsChecked + if ($anyUpdateChecked) { + if (-not $script:installAppsForcedByUpdates) { + $script:prevInstallAppsStateBeforeUpdates = $window.FindName('chkInstallApps').IsChecked + $script:installAppsForcedByUpdates = $true + } + $window.FindName('chkInstallApps').IsChecked = $true + $window.FindName('chkInstallApps').IsEnabled = $false } - $window.FindName('chkInstallApps').IsChecked = $true - $window.FindName('chkInstallApps').IsEnabled = $false - } else { - if ($script:installAppsForcedByUpdates) { - $window.FindName('chkInstallApps').IsChecked = $script:prevInstallAppsStateBeforeUpdates - $script:installAppsForcedByUpdates = $false - $script:prevInstallAppsStateBeforeUpdates = $null + else { + if ($script:installAppsForcedByUpdates) { + $window.FindName('chkInstallApps').IsChecked = $script:prevInstallAppsStateBeforeUpdates + $script:installAppsForcedByUpdates = $false + $script:prevInstallAppsStateBeforeUpdates = $null + } + $window.FindName('chkInstallApps').IsEnabled = $true } - $window.FindName('chkInstallApps').IsEnabled = $true } - } - # Add event handlers for Updates tab checkboxes to update Install Apps state - $window.FindName('chkUpdateLatestDefender').Add_Checked({ & $script:UpdateInstallAppsBasedOnUpdates }) - $window.FindName('chkUpdateLatestDefender').Add_Unchecked({ & $script:UpdateInstallAppsBasedOnUpdates }) - $window.FindName('chkUpdateEdge').Add_Checked({ & $script:UpdateInstallAppsBasedOnUpdates }) - $window.FindName('chkUpdateEdge').Add_Unchecked({ & $script:UpdateInstallAppsBasedOnUpdates }) - $window.FindName('chkUpdateOneDrive').Add_Checked({ & $script:UpdateInstallAppsBasedOnUpdates }) - $window.FindName('chkUpdateOneDrive').Add_Unchecked({ & $script:UpdateInstallAppsBasedOnUpdates }) - $window.FindName('chkUpdateLatestMSRT').Add_Checked({ & $script:UpdateInstallAppsBasedOnUpdates }) - $window.FindName('chkUpdateLatestMSRT').Add_Unchecked({ & $script:UpdateInstallAppsBasedOnUpdates }) - # Add interplay between Latest CU and Preview CU checkboxes - $script:chkLatestCU = $window.FindName('chkUpdateLatestCU') - $script:chkPreviewCU = $window.FindName('chkUpdatePreviewCU') + # Add event handlers for Updates tab checkboxes to update Install Apps state + $window.FindName('chkUpdateLatestDefender').Add_Checked({ & $script:UpdateInstallAppsBasedOnUpdates }) + $window.FindName('chkUpdateLatestDefender').Add_Unchecked({ & $script:UpdateInstallAppsBasedOnUpdates }) + $window.FindName('chkUpdateEdge').Add_Checked({ & $script:UpdateInstallAppsBasedOnUpdates }) + $window.FindName('chkUpdateEdge').Add_Unchecked({ & $script:UpdateInstallAppsBasedOnUpdates }) + $window.FindName('chkUpdateOneDrive').Add_Checked({ & $script:UpdateInstallAppsBasedOnUpdates }) + $window.FindName('chkUpdateOneDrive').Add_Unchecked({ & $script:UpdateInstallAppsBasedOnUpdates }) + $window.FindName('chkUpdateLatestMSRT').Add_Checked({ & $script:UpdateInstallAppsBasedOnUpdates }) + $window.FindName('chkUpdateLatestMSRT').Add_Unchecked({ & $script:UpdateInstallAppsBasedOnUpdates }) + # Add interplay between Latest CU and Preview CU checkboxes + $script:chkLatestCU = $window.FindName('chkUpdateLatestCU') + $script:chkPreviewCU = $window.FindName('chkUpdatePreviewCU') - $script:chkLatestCU.Add_Checked({ - $script:chkPreviewCU.IsEnabled = $false - }) - $script:chkLatestCU.Add_Unchecked({ - $script:chkPreviewCU.IsEnabled = $true - }) + $script:chkLatestCU.Add_Checked({ + $script:chkPreviewCU.IsEnabled = $false + }) + $script:chkLatestCU.Add_Unchecked({ + $script:chkPreviewCU.IsEnabled = $true + }) - $script:chkPreviewCU.Add_Checked({ - $script:chkLatestCU.IsEnabled = $false - }) - $script:chkPreviewCU.Add_Unchecked({ - $script:chkLatestCU.IsEnabled = $true - }) + $script:chkPreviewCU.Add_Checked({ + $script:chkLatestCU.IsEnabled = $false + }) + $script:chkPreviewCU.Add_Unchecked({ + $script:chkLatestCU.IsEnabled = $true + }) - # Add USB Drive Detection handler - $script:btnCheckUSBDrives = $window.FindName('btnCheckUSBDrives') - $script:lstUSBDrives = $window.FindName('lstUSBDrives') - $script:chkSelectAllUSBDrives = $window.FindName('chkSelectAllUSBDrives') + # Add USB Drive Detection handler + $script:btnCheckUSBDrives = $window.FindName('btnCheckUSBDrives') + $script:lstUSBDrives = $window.FindName('lstUSBDrives') + $script:chkSelectAllUSBDrives = $window.FindName('chkSelectAllUSBDrives') - $script:btnCheckUSBDrives.Add_Click({ - $script:lstUSBDrives.Items.Clear() - $usbDrives = Get-USBDrives - foreach ($drive in $usbDrives) { - $script:lstUSBDrives.Items.Add([PSCustomObject]$drive) - } - if ($script:lstUSBDrives.Items.Count -gt 0) { - $script:lstUSBDrives.SelectedIndex = 0 - } - }) + $script:btnCheckUSBDrives.Add_Click({ + $script:lstUSBDrives.Items.Clear() + $usbDrives = Get-USBDrives + foreach ($drive in $usbDrives) { + $script:lstUSBDrives.Items.Add([PSCustomObject]$drive) + } + if ($script:lstUSBDrives.Items.Count -gt 0) { + $script:lstUSBDrives.SelectedIndex = 0 + } + }) - # Handle Select All checkbox - $script:chkSelectAllUSBDrives.Add_Checked({ - foreach ($item in $script:lstUSBDrives.Items) { - $item.IsSelected = $true - } - $script:lstUSBDrives.Items.Refresh() - }) - - $script:chkSelectAllUSBDrives.Add_Unchecked({ - foreach ($item in $script:lstUSBDrives.Items) { - $item.IsSelected = $false - } - $script:lstUSBDrives.Items.Refresh() - }) - - # Add keyboard handler - $script:lstUSBDrives.Add_KeyDown({ - param($eventSrc, $keyEvent) - if ($keyEvent.Key -eq 'Space') { - $selectedItem = $script:lstUSBDrives.SelectedItem - if ($selectedItem) { - $selectedItem.IsSelected = !$selectedItem.IsSelected + # Handle Select All checkbox + $script:chkSelectAllUSBDrives.Add_Checked({ + foreach ($item in $script:lstUSBDrives.Items) { + $item.IsSelected = $true + } $script:lstUSBDrives.Items.Refresh() + }) + + $script:chkSelectAllUSBDrives.Add_Unchecked({ + foreach ($item in $script:lstUSBDrives.Items) { + $item.IsSelected = $false + } + $script:lstUSBDrives.Items.Refresh() + }) + + # Add keyboard handler + $script:lstUSBDrives.Add_KeyDown({ + param($eventSrc, $keyEvent) + if ($keyEvent.Key -eq 'Space') { + $selectedItem = $script:lstUSBDrives.SelectedItem + if ($selectedItem) { + $selectedItem.IsSelected = !$selectedItem.IsSelected + $script:lstUSBDrives.Items.Refresh() + # Update Select All checkbox state + $allSelected = -not ($script:lstUSBDrives.Items | Where-Object { -not $_.IsSelected }) + $script:chkSelectAllUSBDrives.IsChecked = $allSelected + } + } + }) + + # Add selection change handler + $script:lstUSBDrives.Add_SelectionChanged({ + param($eventSrc, $selChangeEvent) # Update Select All checkbox state $allSelected = -not ($script:lstUSBDrives.Items | Where-Object { -not $_.IsSelected }) $script:chkSelectAllUSBDrives.IsChecked = $allSelected + }) + + # Add handler to show/hide USB drive section based on Build USB Drive checkbox + $script:chkBuildUSBDriveEnable = $window.FindName('chkBuildUSBDriveEnable') + $script:usbSection = $window.FindName('usbDriveSection') + $script:chkSelectSpecificUSBDrives = $window.FindName('chkSelectSpecificUSBDrives') + $script:usbSelectionPanel = $window.FindName('usbDriveSelectionPanel') + + # Set initial visibility states + $script:usbSection.Visibility = if ($script:chkBuildUSBDriveEnable.IsChecked) { 'Visible' } else { 'Collapsed' } + $script:usbSelectionPanel.Visibility = if ($script:chkSelectSpecificUSBDrives.IsChecked) { 'Visible' } else { 'Collapsed' } + + $script:chkBuildUSBDriveEnable.Add_Checked({ + $script:usbSection.Visibility = 'Visible' + $script:chkSelectSpecificUSBDrives.IsEnabled = $true + }) + + $script:chkBuildUSBDriveEnable.Add_Unchecked({ + $script:usbSection.Visibility = 'Collapsed' + $script:chkSelectSpecificUSBDrives.IsEnabled = $false + $script:chkSelectSpecificUSBDrives.IsChecked = $false + $script:lstUSBDrives.Items.Clear() + $script:chkSelectAllUSBDrives.IsChecked = $false + }) + + # Add handler to show/hide USB drive selection panel based on Select Specific USB Drives checkbox + $script:chkSelectSpecificUSBDrives.Add_Checked({ + $script:usbSelectionPanel.Visibility = 'Visible' + }) + + $script:chkSelectSpecificUSBDrives.Add_Unchecked({ + $script:usbSelectionPanel.Visibility = 'Collapsed' + $script:lstUSBDrives.Items.Clear() + $script:chkSelectAllUSBDrives.IsChecked = $false + }) + + # Set initial state of Select Specific USB Drives checkbox + $script:chkSelectSpecificUSBDrives.IsEnabled = $script:chkBuildUSBDriveEnable.IsChecked + + # Add handler for Allow External Hard Disk Media checkbox + $script:chkAllowExternalHardDiskMedia = $window.FindName('chkAllowExternalHardDiskMedia') + $script:chkPromptExternalHardDiskMedia = $window.FindName('chkPromptExternalHardDiskMedia') + + $script:chkAllowExternalHardDiskMedia.Add_Checked({ + $script:chkPromptExternalHardDiskMedia.IsEnabled = $true + }) + + $script:chkAllowExternalHardDiskMedia.Add_Unchecked({ + $script:chkPromptExternalHardDiskMedia.IsEnabled = $false + $script:chkPromptExternalHardDiskMedia.IsChecked = $false + }) + + # Add Winget panel visibility handler + $script:chkInstallApps = $window.FindName('chkInstallApps') + $script:chkInstallWingetApps = $window.FindName('chkInstallWingetApps') + $script:wingetPanel = $window.FindName('wingetPanel') + $script:btnCheckWingetModule = $window.FindName('btnCheckWingetModule') + $script:txtWingetVersion = $window.FindName('txtWingetVersion') + $script:txtWingetModuleVersion = $window.FindName('txtWingetModuleVersion') + + # Hide Winget Apps checkbox initially if Install Apps is unchecked + $script:chkInstallWingetApps.Visibility = if ($script:chkInstallApps.IsChecked) { 'Visible' } else { 'Collapsed' } + + # Show/Hide Winget Apps checkbox based on Install Apps state + $script:chkInstallApps.Add_Checked({ + $script:chkInstallWingetApps.Visibility = 'Visible' + }) + $script:chkInstallApps.Add_Unchecked({ + $script:chkInstallWingetApps.IsChecked = $false + $script:chkInstallWingetApps.Visibility = 'Collapsed' + $script:wingetPanel.Visibility = 'Collapsed' + }) + + # Show/Hide Winget panel based on checkbox state + $script:chkInstallWingetApps.Add_Checked({ + $script:wingetPanel.Visibility = 'Visible' + # Don't show search panel here - it should only show after validation + }) + $script:chkInstallWingetApps.Add_Unchecked({ + $script:wingetPanel.Visibility = 'Collapsed' + $script:wingetSearchPanel.Visibility = 'Collapsed' + }) + + # Handle Winget component check/installation + $script:btnCheckWingetModule.Add_Click({ + $this.IsEnabled = $false + $window.Cursor = [System.Windows.Input.Cursors]::Wait + + # Show initial checking status + Update-WingetVersionFields -wingetText "Checking..." -moduleText "Checking..." + + # Run checks in background to prevent UI freezing + $window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [Action] { + try { + # Check Winget CLI first + $cliStatus = Test-WingetCLI + + # Install/Update PowerShell module if needed + $module = Install-WingetComponents -currentWingetVersion $cliStatus.Status + + # Update UI with final status + Update-WingetVersionFields -wingetText $cliStatus.Status -moduleText $module.Version + + # Show search panel only if versions are valid and checkbox is still checked + if ($cliStatus.Status -match '^\d+\.\d+\.\d+$' -and + $module.Version -match '^\d+\.\d+\.\d+$' -and + $script:chkInstallWingetApps.IsChecked) { + $script:wingetSearchPanel.Visibility = 'Visible' + } + } + catch { + Update-WingetVersionFields -wingetText "Error" -moduleText "Error" + [System.Windows.MessageBox]::Show( + "Error checking winget components: $_", + "Error", + "OK", + "Error" + ) + } + finally { + $this.IsEnabled = $true + $window.Cursor = $null + } + }) + }) + + # Create Winget Search section (initially hidden) + $script:wingetSearchPanel = $window.FindName('wingetSearchPanel') + $script:txtWingetSearch = $window.FindName('txtWingetSearch') + $script:btnWingetSearch = $window.FindName('btnWingetSearch') + $script:lstWingetResults = $window.FindName('lstWingetResults') + $script:btnSaveWingetList = $window.FindName('btnSaveWingetList') + $script:btnImportWingetList = $window.FindName('btnImportWingetList') + $script:btnClearWingetList = $window.FindName('btnClearWingetList') + + # Initialize ListView with GridView columns + $gridView = New-Object System.Windows.Controls.GridView + Add-SortableColumn -gridView $gridView -header "Selected" -binding "IsSelected" -width 60 -isCheckbox $true + Add-SortableColumn -gridView $gridView -header "Name" -binding "Name" -width 200 + Add-SortableColumn -gridView $gridView -header "Id" -binding "Id" -width 200 + Add-SortableColumn -gridView $gridView -header "Version" -binding "Version" -width 100 + Add-SortableColumn -gridView $gridView -header "Source" -binding "Source" -width 100 + $script:lstWingetResults.View = $gridView + + # Add column header click handler for sorting + $script:lstWingetResults.AddHandler( + [System.Windows.Controls.GridViewColumnHeader]::ClickEvent, + [System.Windows.RoutedEventHandler] { + param($sender, $e) + $header = $e.OriginalSource + if ($header -is [System.Windows.Controls.GridViewColumnHeader] -and $header.Tag) { + Invoke-ListViewSort -listView $script:lstWingetResults -property $header.Tag + } } - } - }) + ) - # Add selection change handler - $script:lstUSBDrives.Add_SelectionChanged({ - param($eventSrc, $selChangeEvent) - # Update Select All checkbox state - $allSelected = -not ($script:lstUSBDrives.Items | Where-Object { -not $_.IsSelected }) - $script:chkSelectAllUSBDrives.IsChecked = $allSelected - }) - - # Add handler to show/hide USB drive section based on Build USB Drive checkbox - $script:chkBuildUSBDriveEnable = $window.FindName('chkBuildUSBDriveEnable') - $script:usbSection = $window.FindName('usbDriveSection') - $script:chkSelectSpecificUSBDrives = $window.FindName('chkSelectSpecificUSBDrives') - $script:usbSelectionPanel = $window.FindName('usbDriveSelectionPanel') - - # Set initial visibility states - $script:usbSection.Visibility = if ($script:chkBuildUSBDriveEnable.IsChecked) { 'Visible' } else { 'Collapsed' } - $script:usbSelectionPanel.Visibility = if ($script:chkSelectSpecificUSBDrives.IsChecked) { 'Visible' } else { 'Collapsed' } - - $script:chkBuildUSBDriveEnable.Add_Checked({ - $script:usbSection.Visibility = 'Visible' - $script:chkSelectSpecificUSBDrives.IsEnabled = $true - }) - - $script:chkBuildUSBDriveEnable.Add_Unchecked({ - $script:usbSection.Visibility = 'Collapsed' - $script:chkSelectSpecificUSBDrives.IsEnabled = $false - $script:chkSelectSpecificUSBDrives.IsChecked = $false - $script:lstUSBDrives.Items.Clear() - $script:chkSelectAllUSBDrives.IsChecked = $false - }) - - # Add handler to show/hide USB drive selection panel based on Select Specific USB Drives checkbox - $script:chkSelectSpecificUSBDrives.Add_Checked({ - $script:usbSelectionPanel.Visibility = 'Visible' - }) - - $script:chkSelectSpecificUSBDrives.Add_Unchecked({ - $script:usbSelectionPanel.Visibility = 'Collapsed' - $script:lstUSBDrives.Items.Clear() - $script:chkSelectAllUSBDrives.IsChecked = $false - }) - - # Set initial state of Select Specific USB Drives checkbox - $script:chkSelectSpecificUSBDrives.IsEnabled = $script:chkBuildUSBDriveEnable.IsChecked - - # Add handler for Allow External Hard Disk Media checkbox - $script:chkAllowExternalHardDiskMedia = $window.FindName('chkAllowExternalHardDiskMedia') - $script:chkPromptExternalHardDiskMedia = $window.FindName('chkPromptExternalHardDiskMedia') - - $script:chkAllowExternalHardDiskMedia.Add_Checked({ - $script:chkPromptExternalHardDiskMedia.IsEnabled = $true - }) - - $script:chkAllowExternalHardDiskMedia.Add_Unchecked({ - $script:chkPromptExternalHardDiskMedia.IsEnabled = $false - $script:chkPromptExternalHardDiskMedia.IsChecked = $false - }) - - # Add Winget panel visibility handler - $script:chkInstallApps = $window.FindName('chkInstallApps') - $script:chkInstallWingetApps = $window.FindName('chkInstallWingetApps') - $script:wingetPanel = $window.FindName('wingetPanel') - $script:btnCheckWingetModule = $window.FindName('btnCheckWingetModule') - $script:txtWingetVersion = $window.FindName('txtWingetVersion') - $script:txtWingetModuleVersion = $window.FindName('txtWingetModuleVersion') - - # Hide Winget Apps checkbox initially if Install Apps is unchecked - $script:chkInstallWingetApps.Visibility = if ($script:chkInstallApps.IsChecked) { 'Visible' } else { 'Collapsed' } - - # Show/Hide Winget Apps checkbox based on Install Apps state - $script:chkInstallApps.Add_Checked({ - $script:chkInstallWingetApps.Visibility = 'Visible' - }) - $script:chkInstallApps.Add_Unchecked({ - $script:chkInstallWingetApps.IsChecked = $false - $script:chkInstallWingetApps.Visibility = 'Collapsed' - $script:wingetPanel.Visibility = 'Collapsed' - }) - - # Show/Hide Winget panel based on checkbox state - $script:chkInstallWingetApps.Add_Checked({ - $script:wingetPanel.Visibility = 'Visible' - # Don't show search panel here - it should only show after validation - }) - $script:chkInstallWingetApps.Add_Unchecked({ - $script:wingetPanel.Visibility = 'Collapsed' + # Hide search panel initially $script:wingetSearchPanel.Visibility = 'Collapsed' - }) - - # Handle Winget component check/installation - $script:btnCheckWingetModule.Add_Click({ - $this.IsEnabled = $false - $window.Cursor = [System.Windows.Input.Cursors]::Wait - - # Show initial checking status - Update-WingetVersionFields -wingetText "Checking..." -moduleText "Checking..." - - # Run checks in background to prevent UI freezing - $window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [Action]{ - try { - # Check Winget CLI first - $cliStatus = Test-WingetCLI - - # Install/Update PowerShell module if needed - $module = Install-WingetComponents -currentWingetVersion $cliStatus.Status - - # Update UI with final status - Update-WingetVersionFields -wingetText $cliStatus.Status -moduleText $module.Version - - # Show search panel only if versions are valid and checkbox is still checked - if ($cliStatus.Status -match '^\d+\.\d+\.\d+$' -and - $module.Version -match '^\d+\.\d+\.\d+$' -and - $script:chkInstallWingetApps.IsChecked) { - $script:wingetSearchPanel.Visibility = 'Visible' + + # Add search functionality + $script:btnWingetSearch.Add_Click({ + Search-WingetApps + }) + + $script:txtWingetSearch.Add_KeyDown({ + param($eventSrc, $keyEvent) + if ($keyEvent.Key -eq 'Return') { + Search-WingetApps + $keyEvent.Handled = $true } - } - catch { - Update-WingetVersionFields -wingetText "Error" -moduleText "Error" - [System.Windows.MessageBox]::Show( - "Error checking winget components: $_", - "Error", - "OK", - "Error" - ) - } - finally { - $this.IsEnabled = $true - $window.Cursor = $null - } - }) - }) - - # Create Winget Search section (initially hidden) - $script:wingetSearchPanel = $window.FindName('wingetSearchPanel') - $script:txtWingetSearch = $window.FindName('txtWingetSearch') - $script:btnWingetSearch = $window.FindName('btnWingetSearch') - $script:lstWingetResults = $window.FindName('lstWingetResults') - $script:btnSaveWingetList = $window.FindName('btnSaveWingetList') - $script:btnImportWingetList = $window.FindName('btnImportWingetList') + }) - # Initialize ListView with GridView columns - $gridView = New-Object System.Windows.Controls.GridView - Add-SortableColumn -gridView $gridView -header "Selected" -binding "IsSelected" -width 60 -isCheckbox $true - Add-SortableColumn -gridView $gridView -header "Name" -binding "Name" -width 200 - Add-SortableColumn -gridView $gridView -header "Id" -binding "Id" -width 200 - Add-SortableColumn -gridView $gridView -header "Version" -binding "Version" -width 100 - Add-SortableColumn -gridView $gridView -header "Source" -binding "Source" -width 100 - $script:lstWingetResults.View = $gridView - - # Hide search panel initially - $script:wingetSearchPanel.Visibility = 'Collapsed' - - # Add search functionality - $script:btnWingetSearch.Add_Click({ - Search-WingetApps - }) - - $script:txtWingetSearch.Add_KeyDown({ - param($eventSrc, $keyEvent) - if ($keyEvent.Key -eq 'Return') { - Search-WingetApps - $keyEvent.Handled = $true - } - }) - - # Save and Import buttons - $script:btnSaveWingetList.Add_Click({ - Save-WingetList - }) - - $script:btnImportWingetList.Add_Click({ - Import-WingetList - }) - - # Show search panel after successful Winget validation - $script:btnCheckWingetModule.Add_Click({ - # ...existing code... - try { - # Check Winget CLI first - $cliStatus = Test-WingetCLI + # Show search panel after successful Winget validation + $script:btnCheckWingetModule.Add_Click({ + # ...existing code... + try { + # Check Winget CLI first + $cliStatus = Test-WingetCLI - # Install/Update PowerShell module if needed - $module = Install-WingetComponents -currentWingetVersion $cliStatus.Status + # Install/Update PowerShell module if needed + $module = Install-WingetComponents -currentWingetVersion $cliStatus.Status - # Update UI with final status - Update-WingetVersionFields -wingetText $cliStatus.Status -moduleText $module.Version + # Update UI with final status + Update-WingetVersionFields -wingetText $cliStatus.Status -moduleText $module.Version - # Show search panel if versions are valid - if ($cliStatus.Status -match '^\d+\.\d+\.\d+$' -and $module.Version -match '^\d+\.\d+\.\d+$') { - $script:wingetSearchPanel.Visibility = 'Visible' - } - } - catch { - # ...existing error handling code... - } - }) - - $script:lstWingetResults.Add_PreviewMouseLeftButtonUp({ - param($eventSrc, $mouseEventArgs) - $originalSource = $mouseEventArgs.OriginalSource - $header = [System.Windows.Media.VisualTreeHelper]::GetParent($originalSource) - while ($header -ne $null -and $header -isnot [System.Windows.Controls.GridViewColumnHeader]) { - $header = [System.Windows.Media.VisualTreeHelper]::GetParent($header) - } - - if ($header -is [System.Windows.Controls.GridViewColumnHeader]) { - $headerContent = $header.Column.Header - if ($headerContent -is [System.Windows.Controls.GridViewColumnHeader]) { - if ($headerContent.Tag) { - Invoke-ListViewSort -listView $script:lstWingetResults -property $headerContent.Tag + # Show search panel if versions are valid + if ($cliStatus.Status -match '^\d+\.\d+\.\d+$' -and $module.Version -match '^\d+\.\d+\.\d+$') { + $script:wingetSearchPanel.Visibility = 'Visible' + } } - } - } + catch { + # ...existing error handling code... + } + }) + + # Add handlers for Winget app list buttons + $script:btnSaveWingetList.Add_Click({ Save-WingetList }) + $script:btnImportWingetList.Add_Click({ Import-WingetList }) + $script:btnClearWingetList.Add_Click({ + $script:lstWingetResults.Items.Clear() + $script:txtWingetSearch.Text = "" # Clear the Winget search textbox + if ($script:txtStatus) { + $script:txtStatus.Text = "Cleared all applications from the list" + } + }) }) -}) # Function to search for Winget apps function Search-WingetApps { @@ -1086,10 +1089,10 @@ function Search-WingetApps { $results = Find-WingetPackage -Query $searchQuery | ForEach-Object { [PSCustomObject]@{ IsSelected = $false - Name = $_.Name - Id = $_.Id - Version = $_.Version - Source = $_.Source + Name = $_.Name + Id = $_.Id + Version = $_.Version + Source = $_.Source } } @@ -1124,12 +1127,12 @@ function Save-WingetList { $appList = @{ apps = @($selectedApps | ForEach-Object { - [ordered]@{ - name = $_.Name - id = $_.Id - source = $_.Source.ToLower() - } - }) + [ordered]@{ + name = $_.Name + id = $_.Id + source = $_.Source.ToLower() + } + }) } $sfd = New-Object System.Windows.Forms.SaveFileDialog @@ -1165,12 +1168,12 @@ function Import-WingetList { # Add imported apps foreach ($app in $importedApps.apps) { $script:lstWingetResults.Items.Add([PSCustomObject]@{ - IsSelected = $true - Name = $app.name - Id = $app.id - Version = "" # Will be populated when searching - Source = $app.source - }) + IsSelected = $true + Name = $app.name + Id = $app.id + Version = "" # Will be populated when searching + Source = $app.source + }) } [System.Windows.MessageBox]::Show("App list imported successfully.", "Success", "OK", "Information") @@ -1184,167 +1187,167 @@ function Import-WingetList { # 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 - $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." + try { + $progressBar = $window.FindName('progressBar') + $txtStatus = $window.FindName('txtStatus') + $progressBar.Visibility = 'Visible' + $txtStatus.Text = "Starting FFU build..." + $config = Get-UIConfig + $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." } - $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' - } -}) + 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 - $defaultConfigPath = Join-Path $config.FFUDevelopmentPath "config" - if (-not (Test-Path $defaultConfigPath)) { - New-Item -Path $defaultConfigPath -ItemType Directory -Force | Out-Null + try { + $config = Get-UIConfig + $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") + } } - $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") } - } - 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) { - $configContent = Get-Content -Path $ofd.FileName -Raw | ConvertFrom-Json - # Update Build tab values - $window.FindName('txtFFUDevPath').Text = $configContent.FFUDevelopmentPath - $window.FindName('txtCustomFFUNameTemplate').Text = $configContent.CustomFFUNameTemplate - $window.FindName('txtFFUCaptureLocation').Text = $configContent.FFUCaptureLocation - $window.FindName('txtShareName').Text = $configContent.ShareName - $window.FindName('txtUsername').Text = $configContent.Username - $window.FindName('chkBuildUSBDriveEnable').IsChecked = $configContent.BuildUSBDrive - $window.FindName('chkCompactOS').IsChecked = $configContent.CompactOS - $window.FindName('chkOptimize').IsChecked = $configContent.Optimize - $window.FindName('chkAllowVHDXCaching').IsChecked = $configContent.AllowVHDXCaching - $window.FindName('chkAllowExternalHardDiskMedia').IsChecked = $configContent.AllowExternalHardDiskMedia - $window.FindName('chkPromptExternalHardDiskMedia').IsChecked = $configContent.PromptExternalHardDiskMedia - $window.FindName('chkCreateCaptureMedia').IsChecked = $configContent.CreateCaptureMedia - $window.FindName('chkCreateDeploymentMedia').IsChecked = $configContent.CreateDeploymentMedia + 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) { + $configContent = Get-Content -Path $ofd.FileName -Raw | ConvertFrom-Json + # Update Build tab values + $window.FindName('txtFFUDevPath').Text = $configContent.FFUDevelopmentPath + $window.FindName('txtCustomFFUNameTemplate').Text = $configContent.CustomFFUNameTemplate + $window.FindName('txtFFUCaptureLocation').Text = $configContent.FFUCaptureLocation + $window.FindName('txtShareName').Text = $configContent.ShareName + $window.FindName('txtUsername').Text = $configContent.Username + $window.FindName('chkBuildUSBDriveEnable').IsChecked = $configContent.BuildUSBDrive + $window.FindName('chkCompactOS').IsChecked = $configContent.CompactOS + $window.FindName('chkOptimize').IsChecked = $configContent.Optimize + $window.FindName('chkAllowVHDXCaching').IsChecked = $configContent.AllowVHDXCaching + $window.FindName('chkAllowExternalHardDiskMedia').IsChecked = $configContent.AllowExternalHardDiskMedia + $window.FindName('chkPromptExternalHardDiskMedia').IsChecked = $configContent.PromptExternalHardDiskMedia + $window.FindName('chkCreateCaptureMedia').IsChecked = $configContent.CreateCaptureMedia + $window.FindName('chkCreateDeploymentMedia').IsChecked = $configContent.CreateDeploymentMedia - # USB Drive Modification group - $window.FindName('chkCopyAutopilot').IsChecked = $configContent.CopyAutopilot - $window.FindName('chkCopyUnattend').IsChecked = $configContent.CopyUnattend - $window.FindName('chkCopyPPKG').IsChecked = $configContent.CopyPPKG + # USB Drive Modification group + $window.FindName('chkCopyAutopilot').IsChecked = $configContent.CopyAutopilot + $window.FindName('chkCopyUnattend').IsChecked = $configContent.CopyUnattend + $window.FindName('chkCopyPPKG').IsChecked = $configContent.CopyPPKG - # Post Build Cleanup group - $window.FindName('chkCleanupAppsISO').IsChecked = $configContent.CleanupAppsISO - $window.FindName('chkCleanupCaptureISO').IsChecked = $configContent.CleanupCaptureISO - $window.FindName('chkCleanupDeployISO').IsChecked = $configContent.CleanupDeployISO - $window.FindName('chkCleanupDrivers').IsChecked = $configContent.CleanupDrivers - $window.FindName('chkRemoveFFU').IsChecked = $configContent.RemoveFFU - # USB Drive Modification group (now in Build USB Drive section) - $window.FindName('chkCopyAutopilot').IsChecked = $configContent.CopyAutopilot - $window.FindName('chkCopyUnattend').IsChecked = $configContent.CopyUnattend - $window.FindName('chkCopyPPKG').IsChecked = $configContent.CopyPPKG - # Hyper-V Settings - $window.FindName('cmbVMSwitchName').SelectedItem = $configContent.VMSwitchName - $window.FindName('txtVMHostIPAddress').Text = $configContent.VMHostIPAddress - $window.FindName('txtDiskSize').Text = $configContent.Disksize / 1GB - $window.FindName('txtMemory').Text = $configContent.Memory / 1GB - $window.FindName('txtProcessors').Text = $configContent.Processors - $window.FindName('txtVMLocation').Text = $configContent.VMLocation - # <-- NEW: Update the VM Name Prefix textbox value during config load - $window.FindName('txtVMNamePrefix').Text = $configContent.FFUPrefix - $window.FindName('cmbLogicalSectorSize').SelectedItem = $configContent.LogicalSectorSizeBytes - # Windows Settings - $window.FindName('txtISOPath').Text = $configContent.ISOPath - $window.FindName('cmbWindowsRelease').SelectedItem = ($script:allWindowsReleases | Where-Object { $_.Value -eq $configContent.WindowsRelease }) - $window.FindName('cmbWindowsVersion').SelectedItem = $configContent.WindowsVersion - $window.FindName('cmbWindowsArch').SelectedItem = $configContent.WindowsArch - $window.FindName('cmbWindowsLang').SelectedItem = $configContent.WindowsLang - $window.FindName('cmbWindowsSKU').SelectedItem = $configContent.WindowsSKU - $window.FindName('cmbMediaType').SelectedItem = $configContent.MediaType - $window.FindName('txtProductKey').Text = $configContent.ProductKey - # M365 Apps/Office tab - $window.FindName('chkInstallOffice').IsChecked = $configContent.InstallOffice - $window.FindName('txtOfficePath').Text = $configContent.OfficePath - $window.FindName('chkCopyOfficeConfigXML').IsChecked = $configContent.CopyOfficeConfigXML - $window.FindName('txtOfficeConfigXMLFilePath').Text = $configContent.OfficeConfigXMLFile - # Drivers tab - $window.FindName('chkInstallDrivers').IsChecked = $configContent.InstallDrivers - $window.FindName('chkDownloadDrivers').IsChecked = $configContent.DownloadDrivers - $window.FindName('chkCopyDrivers').IsChecked = $configContent.CopyDrivers - $window.FindName('cmbMake').SelectedItem = $configContent.Make - $window.FindName('cmbModel').Text = $configContent.Model - $window.FindName('txtDriversFolder').Text = $configContent.DriversFolder - $window.FindName('txtPEDriversFolder').Text = $configContent.PEDriversFolder - $window.FindName('chkCopyPEDrivers').IsChecked = $configContent.CopyPEDrivers - # Updates tab - $window.FindName('chkUpdateLatestCU').IsChecked = $configContent.UpdateLatestCU - $window.FindName('chkUpdateLatestNet').IsChecked = $configContent.UpdateLatestNet - $window.FindName('chkUpdateLatestDefender').IsChecked = $configContent.UpdateLatestDefender - $window.FindName('chkUpdateEdge').IsChecked = $configContent.UpdateEdge - $window.FindName('chkUpdateOneDrive').IsChecked = $configContent.UpdateOneDrive - $window.FindName('chkUpdateLatestMSRT').IsChecked = $configContent.UpdateLatestMSRT - $window.FindName('chkUpdatePreviewCU').IsChecked = $configContent.UpdatePreviewCU - # Applications tab - $window.FindName('chkInstallApps').IsChecked = $configContent.InstallApps - $window.FindName('chkInstallWingetApps').IsChecked = $configContent.InstallWingetApps + # Post Build Cleanup group + $window.FindName('chkCleanupAppsISO').IsChecked = $configContent.CleanupAppsISO + $window.FindName('chkCleanupCaptureISO').IsChecked = $configContent.CleanupCaptureISO + $window.FindName('chkCleanupDeployISO').IsChecked = $configContent.CleanupDeployISO + $window.FindName('chkCleanupDrivers').IsChecked = $configContent.CleanupDrivers + $window.FindName('chkRemoveFFU').IsChecked = $configContent.RemoveFFU + # USB Drive Modification group (now in Build USB Drive section) + $window.FindName('chkCopyAutopilot').IsChecked = $configContent.CopyAutopilot + $window.FindName('chkCopyUnattend').IsChecked = $configContent.CopyUnattend + $window.FindName('chkCopyPPKG').IsChecked = $configContent.CopyPPKG + # Hyper-V Settings + $window.FindName('cmbVMSwitchName').SelectedItem = $configContent.VMSwitchName + $window.FindName('txtVMHostIPAddress').Text = $configContent.VMHostIPAddress + $window.FindName('txtDiskSize').Text = $configContent.Disksize / 1GB + $window.FindName('txtMemory').Text = $configContent.Memory / 1GB + $window.FindName('txtProcessors').Text = $configContent.Processors + $window.FindName('txtVMLocation').Text = $configContent.VMLocation + # <-- NEW: Update the VM Name Prefix textbox value during config load + $window.FindName('txtVMNamePrefix').Text = $configContent.FFUPrefix + $window.FindName('cmbLogicalSectorSize').SelectedItem = $configContent.LogicalSectorSizeBytes + # Windows Settings + $window.FindName('txtISOPath').Text = $configContent.ISOPath + $window.FindName('cmbWindowsRelease').SelectedItem = ($script:allWindowsReleases | Where-Object { $_.Value -eq $configContent.WindowsRelease }) + $window.FindName('cmbWindowsVersion').SelectedItem = $configContent.WindowsVersion + $window.FindName('cmbWindowsArch').SelectedItem = $configContent.WindowsArch + $window.FindName('cmbWindowsLang').SelectedItem = $configContent.WindowsLang + $window.FindName('cmbWindowsSKU').SelectedItem = $configContent.WindowsSKU + $window.FindName('cmbMediaType').SelectedItem = $configContent.MediaType + $window.FindName('txtProductKey').Text = $configContent.ProductKey + # M365 Apps/Office tab + $window.FindName('chkInstallOffice').IsChecked = $configContent.InstallOffice + $window.FindName('txtOfficePath').Text = $configContent.OfficePath + $window.FindName('chkCopyOfficeConfigXML').IsChecked = $configContent.CopyOfficeConfigXML + $window.FindName('txtOfficeConfigXMLFilePath').Text = $configContent.OfficeConfigXMLFile + # Drivers tab + $window.FindName('chkInstallDrivers').IsChecked = $configContent.InstallDrivers + $window.FindName('chkDownloadDrivers').IsChecked = $configContent.DownloadDrivers + $window.FindName('chkCopyDrivers').IsChecked = $configContent.CopyDrivers + $window.FindName('cmbMake').SelectedItem = $configContent.Make + $window.FindName('cmbModel').Text = $configContent.Model + $window.FindName('txtDriversFolder').Text = $configContent.DriversFolder + $window.FindName('txtPEDriversFolder').Text = $configContent.PEDriversFolder + $window.FindName('chkCopyPEDrivers').IsChecked = $configContent.CopyPEDrivers + # Updates tab + $window.FindName('chkUpdateLatestCU').IsChecked = $configContent.UpdateLatestCU + $window.FindName('chkUpdateLatestNet').IsChecked = $configContent.UpdateLatestNet + $window.FindName('chkUpdateLatestDefender').IsChecked = $configContent.UpdateLatestDefender + $window.FindName('chkUpdateEdge').IsChecked = $configContent.UpdateEdge + $window.FindName('chkUpdateOneDrive').IsChecked = $configContent.UpdateOneDrive + $window.FindName('chkUpdateLatestMSRT').IsChecked = $configContent.UpdateLatestMSRT + $window.FindName('chkUpdatePreviewCU').IsChecked = $configContent.UpdatePreviewCU + # Applications tab + $window.FindName('chkInstallApps').IsChecked = $configContent.InstallApps + $window.FindName('chkInstallWingetApps').IsChecked = $configContent.InstallWingetApps - # Update USB Drive selection if present in config - if ($configContent.USBDriveList) { - # First click the Check USB Drives button to populate the list - $script:btnCheckUSBDrives.RaiseEvent( - [System.Windows.RoutedEventArgs]::new( - [System.Windows.Controls.Button]::ClickEvent + # Update USB Drive selection if present in config + if ($configContent.USBDriveList) { + # First click the Check USB Drives button to populate the list + $script:btnCheckUSBDrives.RaiseEvent( + [System.Windows.RoutedEventArgs]::new( + [System.Windows.Controls.Button]::ClickEvent + ) ) - ) - # Then select the drives that match the saved configuration - foreach ($item in $script:lstUSBDrives.Items) { - if ($configContent.USBDriveList.ContainsKey($item.Model) -and - $configContent.USBDriveList[$item.Model] -eq $item.SerialNumber) { - $item.IsSelected = $true + # Then select the drives that match the saved configuration + foreach ($item in $script:lstUSBDrives.Items) { + if ($configContent.USBDriveList.ContainsKey($item.Model) -and + $configContent.USBDriveList[$item.Model] -eq $item.SerialNumber) { + $item.IsSelected = $true + } } - } - $script:lstUSBDrives.Items.Refresh() + $script:lstUSBDrives.Items.Refresh() - # Update the Select All checkbox state - $allSelected = -not ($script:lstUSBDrives.Items | Where-Object { -not $_.IsSelected }) - $script:chkSelectAllUSBDrives.IsChecked = $allSelected + # Update the Select All checkbox state + $allSelected = -not ($script:lstUSBDrives.Items | Where-Object { -not $_.IsSelected }) + $script:chkSelectAllUSBDrives.IsChecked = $allSelected + } } } - } - catch { - [System.Windows.MessageBox]::Show("Error loading config file:`n$_","Error","OK","Error") - } -}) + catch { + [System.Windows.MessageBox]::Show("Error loading config file:`n$_", "Error", "OK", "Error") + } + }) [void]$window.ShowDialog() diff --git a/FFUDevelopment/BuildFFUVM_UI.xaml b/FFUDevelopment/BuildFFUVM_UI.xaml index 34e9774..833bba3 100644 --- a/FFUDevelopment/BuildFFUVM_UI.xaml +++ b/FFUDevelopment/BuildFFUVM_UI.xaml @@ -637,7 +637,7 @@ ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.HorizontalScrollBarVisibility="Auto"/> - +