[CmdletBinding()] [System.STAThread()] param() # Dot-source the common functions shared between the BuildFFUVM and BuildFFUVM_UI scripts . "$PSScriptRoot\Common\WingetFunctions.ps1" # -------------------------------------------------------------------------- # SECTION: Variables & Constants # -------------------------------------------------------------------------- $FFUDevelopmentPath = $PSScriptRoot $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') } | ForEach-Object { $size = [math]::Round($_.Size / 1GB, 2) $serialNumber = if ($_.SerialNumber) { $_.SerialNumber.Trim() } else { "N/A" } @{ IsSelected = $false Model = $_.Model.Trim() SerialNumber = $serialNumber Size = $size DriveIndex = $_.Index } } } # -------------------------------------------------------------------------- # SECTION: Modern folder dialog # -------------------------------------------------------------------------- # 1) Define a C# class that uses the correct GUIDs for IFileDialog, IFileOpenDialog, and FileOpenDialog, # while omitting conflicting "GetResults/GetSelectedItems" from IFileDialog. Add-Type -TypeDefinition @" using System; using System.Runtime.InteropServices; public static class ModernFolderBrowser { // Flags for IFileDialog [Flags] private enum FileDialogOptions : uint { OverwritePrompt = 0x00000002, StrictFileTypes = 0x00000004, NoChangeDir = 0x00000008, PickFolders = 0x00000020, ForceFileSystem = 0x00000040, AllNonStorageItems = 0x00000080, NoValidate = 0x00000100, AllowMultiSelect = 0x00000200, PathMustExist = 0x00000800, FileMustExist = 0x00001000, CreatePrompt = 0x00002000, ShareAware = 0x00004000, NoReadOnlyReturn = 0x00008000, NoTestFileCreate = 0x00010000, DontAddToRecent = 0x02000000, ForceShowHidden = 0x10000000 } // IFileDialog (GUID from Windows SDK) // - Omitting GetResults / GetSelectedItems to avoid overshadow. [ComImport] [Guid("42F85136-DB7E-439C-85F1-E4075D135FC8")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IFileDialog { [PreserveSig] int Show(IntPtr parent); void SetFileTypes(uint cFileTypes, IntPtr rgFilterSpec); void SetFileTypeIndex(uint iFileType); void GetFileTypeIndex(out uint piFileType); void Advise(IntPtr pfde, out uint pdwCookie); void Unadvise(uint dwCookie); void SetOptions(FileDialogOptions fos); void GetOptions(out FileDialogOptions pfos); void SetDefaultFolder(IShellItem psi); void SetFolder(IShellItem psi); void GetFolder(out IShellItem ppsi); void GetCurrentSelection(out IShellItem ppsi); void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName); void GetFileName(out IntPtr pszName); void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle); void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText); void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel); void GetResult(out IShellItem ppsi); void AddPlace(IShellItem psi, int fdap); void SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); void Close(int hr); void SetClientGuid(ref Guid guid); void ClearClientData(); void SetFilter(IntPtr pFilter); // NOTE: We intentionally do NOT define GetResults and GetSelectedItems here, // because they cause overshadow warnings in IFileOpenDialog. } // IFileOpenDialog extends IFileDialog by adding 2 new methods with the same name, // which otherwise cause overshadow warnings. We'll define them only here. [ComImport] [Guid("D57C7288-D4AD-4768-BE02-9D969532D960")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IFileOpenDialog : IFileDialog { // These two come after the parent's vtable: void GetResults(out IntPtr ppenum); void GetSelectedItems(out IntPtr ppsai); } // The coclass for creating an IFileOpenDialog [ComImport] [Guid("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7")] private class FileOpenDialog { } // IShellItem [ComImport] [Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IShellItem { void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv); void GetParent(out IShellItem ppsi); void GetDisplayName(uint sigdnName, out IntPtr ppszName); void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs); void Compare(IShellItem psi, uint hint, out int piOrder); } private const uint SIGDN_FILESYSPATH = 0x80058000; public static string ShowDialog(string title, IntPtr parentHandle) { // Create COM dialog instance IFileOpenDialog dialog = (IFileOpenDialog)(new FileOpenDialog()); // Get current options FileDialogOptions opts; dialog.GetOptions(out opts); // Add flags for picking folders opts |= FileDialogOptions.PickFolders | FileDialogOptions.PathMustExist | FileDialogOptions.ForceFileSystem; dialog.SetOptions(opts); // Set title if (!string.IsNullOrEmpty(title)) { dialog.SetTitle(title); } // Show the dialog int hr = dialog.Show(parentHandle); // 0 = S_OK. 1 or 0x800704C7 often means user canceled. Return null if so. if (hr != 0) { if ((uint)hr == 0x800704C7 || hr == 1) { return null; // Canceled } else { Marshal.ThrowExceptionForHR(hr); } } // Retrieve the selection (IShellItem) IShellItem shellItem; dialog.GetResult(out shellItem); if (shellItem == null) return null; // Convert to file system path IntPtr pszPath = IntPtr.Zero; shellItem.GetDisplayName(SIGDN_FILESYSPATH, out pszPath); if (pszPath == IntPtr.Zero) return null; string folderPath = Marshal.PtrToStringAuto(pszPath); Marshal.FreeCoTaskMem(pszPath); return folderPath; } } "@ -Language CSharp # 2) Define a PowerShell function that invokes our C# wrapper function Show-ModernFolderPicker { param( [string]$Title = "Select a folder" ) # For a simple test, pass IntPtr.Zero as the parent window handle return [ModernFolderBrowser]::ShowDialog($Title, [IntPtr]::Zero) } # -------------------------------------------------------------------------- # SECTION: Winget Management Functions # -------------------------------------------------------------------------- function Test-WingetCLI { [CmdletBinding()] param() $minVersion = [version]"1.8.1911" # Check Winget CLI $wingetCmd = Get-Command -Name winget -ErrorAction SilentlyContinue if (-not $wingetCmd) { return @{ Version = "Not installed" Status = "Not installed - Install from Microsoft Store" } } # Get and check version $wingetVersion = & winget.exe --version if ($wingetVersion -match 'v?(\d+\.\d+.\d+)') { $version = [version]$matches[1] if ($version -lt $minVersion) { return @{ Version = $version.ToString() Status = "Update required - Install from Microsoft Store" } } return @{ Version = $version.ToString() Status = $version.ToString() } } return @{ Version = "Unknown" Status = "Version check failed" } } 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:txtWingetVersion.Text = $wingetText $script:txtWingetModuleVersion.Text = $moduleText # Force immediate UI refresh [System.Windows.Forms.Application]::DoEvents() }) } function Install-WingetComponents { [CmdletBinding()] param( [string]$currentWingetVersion = "Checking..." ) $minVersion = [version]"1.8.1911" try { # Check and update PowerShell Module $module = Get-InstalledModule -Name Microsoft.WinGet.Client -ErrorAction SilentlyContinue if (-not $module -or $module.Version -lt $minVersion) { Update-WingetVersionFields -wingetText $currentWingetVersion -moduleText "Installing..." # Store and modify PSGallery trust setting temporarily if needed $PSGalleryTrust = (Get-PSRepository -Name 'PSGallery').InstallationPolicy if ($PSGalleryTrust -eq 'Untrusted') { Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted } # Install/Update the module Install-Module -Name Microsoft.WinGet.Client -Force -Repository 'PSGallery' # Restore original PSGallery trust setting if ($PSGalleryTrust -eq 'Untrusted') { Set-PSRepository -Name 'PSGallery' -InstallationPolicy Untrusted } $module = Get-InstalledModule -Name Microsoft.WinGet.Client -ErrorAction Stop } return $module } catch { Write-Error "Failed to install/update Winget PowerShell module: $_" throw } } # Winget Module Check Function function Confirm-WinGetInstallation { param( [System.Windows.Controls.TextBlock]$txtWingetVersion, [System.Windows.Controls.TextBlock]$txtWingetModuleVersion ) $minVersion = [version]"1.8.1911" $result = @{ Success = $false Message = "" RequiresRestart = $false } # Check if winget executable exists and is accessible if (-not (Get-Command -Name winget -ErrorAction SilentlyContinue)) { Update-VersionTextFields -wingetText "Not installed" -moduleText "Not installed" $result.Message = "WinGet not found. Installing..." $result.RequiresRestart = $true return $result } # Get winget version $wingetVersion = & winget.exe --version if ($wingetVersion -match 'v?(\d+\.\d+.\d+)') { $currentVersion = [version]$matches[1] Update-VersionTextFields -wingetText $matches[1] -moduleText $txtWingetModuleVersion.Text if ($currentVersion -lt $minVersion) { Update-VersionTextFields -wingetText "Updating..." -moduleText $txtWingetModuleVersion.Text $result.Message = "WinGet version $currentVersion is outdated. Minimum required version is $minVersion" $result.RequiresRestart = $true return $result } } # Check if Winget PowerShell module is installed and up to date $wingetModule = Get-InstalledModule -Name Microsoft.WinGet.Client -ErrorAction SilentlyContinue if ($null -eq $wingetModule) { Update-VersionTextFields -wingetText $txtWingetVersion.Text -moduleText "Installing..." $result.Message = "Microsoft.WinGet.Client module needs to be installed..." } elseif ($wingetModule.Version -lt $minVersion) { Update-VersionTextFields -wingetText $txtWingetVersion.Text -moduleText "Updating..." $result.Message = "Microsoft.WinGet.Client module needs to be updated..." } else { Update-VersionTextFields -wingetText $txtWingetVersion.Text -moduleText $wingetModule.Version.ToString() $result.Success = $true $result.Message = "Winget and its PowerShell module are installed and up to date." return $result } # Install/Update module if needed try { # Check if PSGallery is trusted $PSGalleryTrust = (Get-PSRepository -Name 'PSGallery').InstallationPolicy if ($PSGalleryTrust -eq 'Untrusted') { Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted } # Install/Update the module Install-Module -Name Microsoft.WinGet.Client -Force -Repository 'PSGallery' # Restore PSGallery trust setting if it was untrusted if ($PSGalleryTrust -eq 'Untrusted') { Set-PSRepository -Name 'PSGallery' -InstallationPolicy Untrusted } } catch { Update-VersionTextFields -wingetText $txtWingetVersion.Text -moduleText "Error" throw } $result.RequiresRestart = $true return $result } # Some default values $defaultISOPath = "" $defaultWindowsArch = "x64" $defaultWindowsLang = "en-us" $defaultWindowsSKU = "Pro" $defaultMediaType = "Consumer" # updated value $defaultOptionalFeatures = "" $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" ) $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)' ) # 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 } ) # Subset for MCT (if ISO path is blank) $mctWindowsReleases = @( [PSCustomObject]@{ Display = "Windows 10"; Value = 10 }, [PSCustomObject]@{ Display = "Windows 11"; Value = 11 } ) # Windows version sets $windowsVersionMap = @{ 10 = @("22H2") 11 = @("22H2", "23H2", "24H2") 2016 = @("1607") 2019 = @("1809") 2022 = @("21H2") 2025 = @("24H2") } # -------------------------------------------------------------------------- # Clean up the Get-UIConfig function to remove duplicates and fix USBDriveList 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 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') { $window.FindName('txtCustomVMSwitchName').Text } 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 } # Add selected USB drives to the config $window.FindName('lstUSBDrives').Items | Where-Object { $_.IsSelected } | ForEach-Object { $config.USBDriveList[$_.Model] = $_.SerialNumber } return $config } function UpdateWindowsReleaseList { param([string]$isoPath) if (-not $script:cmbWindowsRelease) { return } $oldItem = $script:cmbWindowsRelease.SelectedItem $script:cmbWindowsRelease.Items.Clear() $script:cmbWindowsRelease.DisplayMemberPath = 'Display' $script:cmbWindowsRelease.SelectedValuePath = 'Value' if ([string]::IsNullOrEmpty($isoPath)) { foreach ($rel in $mctWindowsReleases) { $script:cmbWindowsRelease.Items.Add($rel) | Out-Null } } else { foreach ($rel in $allWindowsReleases) { $script:cmbWindowsRelease.Items.Add($rel) | Out-Null } } if ($oldItem) { $reSelect = $script:cmbWindowsRelease.Items | Where-Object { $_.Value -eq $oldItem.Value } if ($reSelect) { $script:cmbWindowsRelease.SelectedItem = $reSelect } else { $script:cmbWindowsRelease.SelectedIndex = 0 } } else { $script:cmbWindowsRelease.SelectedIndex = 0 } } function UpdateWindowsVersionCombo { param( [int]$selectedRelease, [string]$isoPath ) $combo = $window.FindName('cmbWindowsVersion') if (-not $combo) { return } $combo.Items.Clear() if (-not $windowsVersionMap.ContainsKey($selectedRelease)) { $combo.IsEnabled = $false return } $validVersions = $windowsVersionMap[$selectedRelease] if ([string]::IsNullOrEmpty($isoPath)) { switch ($selectedRelease) { 10 { $default = "22H2" } 11 { $default = "24H2" } 2016 { $default = "1607" } 2019 { $default = "1809" } 2022 { $default = "21H2" } 2025 { $default = "24H2" } default { $default = $validVersions[0] } } $combo.Items.Add($default) | Out-Null $combo.SelectedIndex = 0 $combo.IsEnabled = $false } else { foreach ($v in $validVersions) { [void]$combo.Items.Add($v) } if ($selectedRelease -eq 11 -and $validVersions -contains "24H2") { $combo.SelectedItem = "24H2" } else { $combo.SelectedIndex = 0 } $combo.IsEnabled = $true } } $script:RefreshWindowsUI = { param([string]$isoPath) UpdateWindowsReleaseList -isoPath $isoPath $selItem = $script:cmbWindowsRelease.SelectedItem if ($selItem -and $selItem.Value -is [int]) { $selectedRelease = [int]$selItem.Value } else { $selectedRelease = 10 } UpdateWindowsVersionCombo -selectedRelease $selectedRelease -isoPath $isoPath } 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 $script:featureCheckBoxes = @{} function UpdateOptionalFeaturesString { $checkedFeatures = @() foreach ($entry in $script:featureCheckBoxes.GetEnumerator()) { if ($entry.Value.IsChecked) { $checkedFeatures += $entry.Key } } $window.FindName('txtOptionalFeatures').Text = $checkedFeatures -join ";" } function BuildFeaturesGrid { param ( [System.Windows.FrameworkElement]$parent ) $parent.Children.Clear() $sortedFeatures = $allowedFeatures | Sort-Object $rows = 10 $columns = [math]::Ceiling($sortedFeatures.Count / $rows) $featuresGrid = New-Object System.Windows.Controls.Grid $featuresGrid.Margin = "0,5,0,5" $featuresGrid.ShowGridLines = $false for ($r = 0; $r -lt $rows; $r++) { $rowDef = New-Object System.Windows.Controls.RowDefinition $rowDef.Height = 'Auto' $featuresGrid.RowDefinitions.Add($rowDef) | Out-Null } for ($c = 0; $c -lt $columns; $c++) { $colDef = New-Object System.Windows.Controls.ColumnDefinition $colDef.Width = 'Auto' $featuresGrid.ColumnDefinitions.Add($colDef) | Out-Null } 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 }) $chk.Add_Unchecked({ UpdateOptionalFeaturesString }) $script:featureCheckBoxes[$featureName] = $chk [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 } # 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 } else { if ($script:installAppsForcedByUpdates) { $window.FindName('chkInstallApps').IsChecked = $script:prevInstallAppsStateBeforeUpdates $script:installAppsForcedByUpdates = $false $script:prevInstallAppsStateBeforeUpdates = $null } $window.FindName('chkInstallApps').IsEnabled = $true } } # Create data context class for version binding $script:versionData = [PSCustomObject]@{ WingetVersion = "Not checked" ModuleVersion = "Not checked" } # Add observable property support $script:versionData | Add-Member -MemberType ScriptMethod -Name NotifyPropertyChanged -Value { param($PropertyName) if ($this.PropertyChanged) { $this.PropertyChanged.Invoke($this, [System.ComponentModel.PropertyChangedEventArgs]::new($PropertyName)) } } $script:versionData | Add-Member -MemberType NoteProperty -Name PropertyChanged -Value $null $script:versionData | Add-Member -TypeName "System.ComponentModel.INotifyPropertyChanged" # 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 ) $column = New-Object System.Windows.Controls.GridViewColumn if ($isCheckbox) { $template = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.CheckBox]) $template.SetBinding([System.Windows.Controls.CheckBox]::IsCheckedProperty, (New-Object System.Windows.Data.Binding("IsSelected"))) $column.CellTemplate = New-Object System.Windows.DataTemplate $column.CellTemplate.VisualTree = $template } else { $column.DisplayMemberBinding = New-Object System.Windows.Data.Binding($binding) } # 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 } $gridView.Columns.Add($column) } # Initialize tracking variables for sorting $script:lastSortProperty = $null $script:lastSortAscending = $true # 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:lastSortProperty -eq $property) { $script:lastSortAscending = -not $script:lastSortAscending } else { $script:lastSortAscending = $true } $script:lastSortProperty = $property # Store selected items $items = @($listView.Items) $selectedItems = @($items | Where-Object { $_.IsSelected }) $unselectedItems = @($items | Where-Object { -not $_.IsSelected }) # Sort unselected items $sortedUnselected = if ($script:lastSortAscending) { @($unselectedItems | Sort-Object -Property $property) } else { #DO NOT CHANGE THIS LINE @($unselectedItems | Sort-Object -Property $property -Descending) } # Clear and repopulate ListView $listView.Items.Clear() # Add selected items first foreach ($item in $selectedItems) { [void]$listView.Items.Add($item) } # Add sorted unselected items foreach ($item in $sortedUnselected) { [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: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: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 } $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 } $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') $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 }) # 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 } }) # 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($eventSource, $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($eventSource, $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' }) # Bring Your Own Applications checkbox should only show if Install Applications is checked $script:chkBringYourOwnApps = $window.FindName('chkBringYourOwnApps') $script:chkBringYourOwnApps.Visibility = if ($script:chkInstallApps.IsChecked) { 'Visible' } else { 'Collapsed' } $script:chkInstallApps.Add_Checked({ $script:chkBringYourOwnApps.Visibility = 'Visible' }) $script:chkInstallApps.Add_Unchecked({ $script:chkBringYourOwnApps.IsChecked = $false $script:chkBringYourOwnApps.Visibility = 'Collapsed' }) # Bring Your Own Applications checkbox should only show if Install Applications is checked $script:chkBringYourOwnApps = $window.FindName('chkBringYourOwnApps') $script:byoApplicationPanel = $window.FindName('byoApplicationPanel') $script:chkBringYourOwnApps.Visibility = if ($script:chkInstallApps.IsChecked) { 'Visible' } else { 'Collapsed' } # Show/Hide Bring Your Own Applications based on Install Apps state $script:chkInstallApps.Add_Checked({ $script:chkBringYourOwnApps.Visibility = 'Visible' }) $script:chkInstallApps.Add_Unchecked({ $script:chkBringYourOwnApps.IsChecked = $false $script:chkBringYourOwnApps.Visibility = 'Collapsed' $script:byoApplicationPanel.Visibility = 'Collapsed' }) # Show/Hide Application Information section based on Bring Your Own Applications state $script:chkBringYourOwnApps.Add_Checked({ $script:byoApplicationPanel.Visibility = 'Visible' }) $script:chkBringYourOwnApps.Add_Unchecked({ $script:byoApplicationPanel.Visibility = 'Collapsed' $window.FindName('txtAppName').Text = '' $window.FindName('txtAppCommandLine').Text = '' $window.FindName('txtAppSource').Text = '' }) # 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 }) # 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($eventSource, $e) $header = $e.OriginalSource if ($header -is [System.Windows.Controls.GridViewColumnHeader] -and $header.Tag) { Invoke-ListViewSort -listView $script:lstWingetResults -property $header.Tag } } ) # 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 } }) # Show search panel after successful Winget validation $script:btnCheckWingetModule.Add_Click({ 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 if versions are valid if ($cliStatus.Status -match '^\d+\.\d+\.\d+$' -and $module.Version -match '^\d+\.\d+\.\d+$') { $script:wingetSearchPanel.Visibility = 'Visible' } } catch { Update-WingetVersionFields -wingetText "Error" -moduleText "Error" [System.Windows.MessageBox]::Show( "Error checking winget components: $_", "Error", "OK", "Error" ) } }) # 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" } }) # Add Browse button handler for App Source $script:btnBrowseAppSource = $window.FindName('btnBrowseAppSource') $script:btnBrowseAppSource.Add_Click({ $selectedPath = Show-ModernFolderPicker -Title "Select Application Source Folder" if ($selectedPath) { $window.FindName('txtAppSource').Text = $selectedPath } }) # Add Browse button handler for FFU Development Path $script:btnBrowseFFUDevPath = $window.FindName('btnBrowseFFUDevPath') $script:btnBrowseFFUDevPath.Add_Click({ $selectedPath = Show-ModernFolderPicker -Title "Select FFU Development Path" if ($selectedPath) { $window.FindName('txtFFUDevPath').Text = $selectedPath } }) # Add Browse button handler for FFU Capture Location $script:btnBrowseFFUCaptureLocation = $window.FindName('btnBrowseFFUCaptureLocation') $script:btnBrowseFFUCaptureLocation.Add_Click({ $selectedPath = Show-ModernFolderPicker -Title "Select FFU Capture Location" if ($selectedPath) { $window.FindName('txtFFUCaptureLocation').Text = $selectedPath } }) # Add Browse button handler for Office Path $script:btnBrowseOfficePath = $window.FindName('btnBrowseOfficePath') $script:btnBrowseOfficePath.Add_Click({ $selectedPath = Show-ModernFolderPicker -Title "Select Office Path" if ($selectedPath) { $window.FindName('txtOfficePath').Text = $selectedPath } }) # Add Browse button handler for Drivers Folder $script:btnBrowseDriversFolder = $window.FindName('btnBrowseDriversFolder') $script:btnBrowseDriversFolder.Add_Click({ $selectedPath = Show-ModernFolderPicker -Title "Select Drivers Folder" if ($selectedPath) { $window.FindName('txtDriversFolder').Text = $selectedPath } }) # Add Browse button handler for PE Drivers Folder $script:btnBrowsePEDriversFolder = $window.FindName('btnBrowsePEDriversFolder') $script:btnBrowsePEDriversFolder.Add_Click({ $selectedPath = Show-ModernFolderPicker -Title "Select PE Drivers Folder" if ($selectedPath) { $window.FindName('txtPEDriversFolder').Text = $selectedPath } }) # Add button handler for Add Application $script:btnAddApplication = $window.FindName('btnAddApplication') $script:btnAddApplication.Add_Click({ $name = $window.FindName('txtAppName').Text $commandLine = $window.FindName('txtAppCommandLine').Text $source = $window.FindName('txtAppSource').Text if ([string]::IsNullOrWhiteSpace($name) -or [string]::IsNullOrWhiteSpace($commandLine) -or [string]::IsNullOrWhiteSpace($source)) { [System.Windows.MessageBox]::Show("Please fill in all fields (Name, Command Line, and Source)", "Missing Information", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning) return } $listView = $window.FindName('lstApplications') # Calculate the next priority number $priority = 1 if ($listView.Items.Count -gt 0) { $priority = ($listView.Items | Measure-Object -Property Priority -Maximum).Maximum + 1 } # Create new application object $application = [PSCustomObject]@{ Priority = $priority Name = $name CommandLine = $commandLine Source = $source } # Add to ListView $listView.Items.Add($application) # Clear the input fields $window.FindName('txtAppName').Text = "" $window.FindName('txtAppCommandLine').Text = "" $window.FindName('txtAppSource').Text = "" }) # Add visibility handling for BYO Applications panel $script:chkBringYourOwnApps = $window.FindName('chkBringYourOwnApps') $script:byoApplicationPanel = $window.FindName('byoApplicationPanel') $script:chkBringYourOwnApps.Add_Checked({ $script:byoApplicationPanel.Visibility = 'Visible' }) $script:chkBringYourOwnApps.Add_Unchecked({ $script:byoApplicationPanel.Visibility = 'Collapsed' }) # Add event handlers for Save/Load/Clear buttons $script:btnSaveBYOApplications = $window.FindName('btnSaveBYOApplications') $script:btnLoadBYOApplications = $window.FindName('btnLoadBYOApplications') $script:btnClearBYOApplications = $window.FindName('btnClearBYOApplications') $script: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" if ($saveDialog.ShowDialog()) { Save-BYOApplicationList -Path $saveDialog.FileName } }) $script:btnLoadBYOApplications.Add_Click({ $openDialog = New-Object Microsoft.Win32.OpenFileDialog $openDialog.Filter = "JSON files (*.json)|*.json|All files (*.*)|*.*" $openDialog.Title = "Import Application List" if ($openDialog.ShowDialog()) { Import-BYOApplicationList -Path $openDialog.FileName } }) $script: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() } }) }) # Function to search for Winget apps function Search-WingetApps { try { $searchQuery = $script:txtWingetSearch.Text if ([string]::IsNullOrWhiteSpace($searchQuery)) { return } # Store selected apps $selectedApps = $script:lstWingetResults.Items | Where-Object { $_.IsSelected } # Search for new apps $results = Find-WingetPackage -Query $searchQuery | ForEach-Object { [PSCustomObject]@{ IsSelected = $false Name = $_.Name Id = $_.Id Version = $_.Version Source = $_.Source } } # Clear and repopulate list view $script:lstWingetResults.Items.Clear() # Add back selected apps first foreach ($app in $selectedApps) { $script:lstWingetResults.Items.Add($app) # Remove from new results if already selected $results = $results | Where-Object { $_.Id -ne $app.Id } } # Add new search results foreach ($result in $results) { $script:lstWingetResults.Items.Add($result) } } 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: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) { $importedApps = Get-Content $ofd.FileName -Raw | ConvertFrom-Json # Clear existing items $script:lstWingetResults.Items.Clear() # 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 }) } [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) $listView = $window.FindName('lstApplications') # Remove the item with the specified priority $itemToRemove = $listView.Items | Where-Object { $_.Priority -eq $priority } | Select-Object -First 1 $listView.Items.Remove($itemToRemove) # Reorder priorities for remaining items $currentPriority = 1 foreach ($item in $listView.Items) { $item.Priority = $currentPriority $currentPriority++ } # Refresh the ListView $listView.Items.Refresh() } # Function to save BYO applications to JSON function Save-BYOApplicationList { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Path ) $listView = $window.FindName('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 { $applications = $listView.Items | Select-Object Priority, Name, CommandLine, Source $applications | ConvertTo-Json | Set-Content -Path $Path -Force [System.Windows.MessageBox]::Show("Applications saved successfully.", "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.", "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() foreach ($app in $applications) { $listView.Items.Add([PSCustomObject]@{ Priority = $app.Priority Name = $app.Name CommandLine = $app.CommandLine Source = $app.Source }) } # Reorder priorities to ensure they are sequential $currentPriority = 1 foreach ($item in $listView.Items) { $item.Priority = $currentPriority $currentPriority++ } [System.Windows.MessageBox]::Show("Applications imported successfully.", "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 $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 $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) { $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 # 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 $window.FindName('chkBringYourOwnApps').IsChecked = $configContent.BringYourOwnApps # 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 } } $script:lstUSBDrives.Items.Refresh() # 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") } }) # 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, $eventArgs) if ($eventArgs.OriginalSource -is [System.Windows.Controls.Button] -and $eventArgs.OriginalSource.Content -eq "Remove") { Remove-Application -priority $eventArgs.OriginalSource.Tag } } ) }) [void]$window.ShowDialog()