Files
FFU/FFUDevelopment/BuildFFUVM_UI.ps1
T
2025-05-26 11:32:35 -07:00

604 lines
23 KiB
PowerShell

[CmdletBinding()]
[System.STAThread()]
param()
# --------------------------------------------------------------------------
# SECTION 1: Variables & Constants
# --------------------------------------------------------------------------
$FFUDevelopmentPath = $PSScriptRoot
$infoImagePath = Join-Path $PSScriptRoot "info.png"
$AppsPath = Join-Path $FFUDevelopmentPath "Apps"
$OfficePath = Join-Path $AppsPath "Office"
# Some default values
$defaultISOPath = ""
$defaultWindowsRelease = 11 # numeric
$defaultWindowsArch = "x64"
$defaultWindowsLang = "en-us"
$defaultWindowsSKU = "Pro"
$defaultMediaType = "consumer"
$defaultOptionalFeatures = ""
$defaultProductKey = ""
$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)'
)
# Info icons
$imageNames = @(
"imgFFUNameInfo",
"imgISOPathInfo",
"imgWindowsSKUInfo",
"imgVMSwitchNameInfo",
"imgVMHostIPAddressInfo",
"imgInstallOfficeInfo",
"imgInstallAppsInfo",
"imgInstallDriversInfo",
"imgCopyDriversInfo",
"imgFFUDevPathInfo",
"imgOfficePathInfo",
"imgCopyOfficeConfigXMLInfo",
"imgOfficeConfigXMLFileInfo",
"imgMakeInfo",
"imgModelInfo",
"imgDownloadDriversInfo",
"imgDriversFolderInfo",
"imgPEDriversFolderInfo",
"imgCopyPEDriversInfo"
)
# Full list of Windows releases (for when ISO path is non-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 (Media Creation Tool) only
$mctWindowsReleases = @(
[PSCustomObject]@{ Display = "Windows 10"; Value = 10 },
[PSCustomObject]@{ Display = "Windows 11"; Value = 11 }
)
# Windows version sets for each release (for "Version" combobox)
$windowsVersionMap = @{
10 = @("22H2")
11 = @("22H2","23H2","24H2")
2016 = @("1607")
2019 = @("1809")
2022 = @("21H2")
2025 = @("24H2")
}
# --------------------------------------------------------------------------
function Set-ImageSource {
param(
[System.Windows.Window]$window,
[string]$imageName,
[string]$sourcePath
)
$imgControl = $window.FindName($imageName)
if ($imgControl) {
$uri = New-Object System.Uri($sourcePath, [System.UriKind]::Absolute)
$bitmap = New-Object System.Windows.Media.Imaging.BitmapImage($uri)
$imgControl.Source = $bitmap
}
}
function Get-UIConfig {
# Basic tab
$ffuDevPath = $window.FindName('txtFFUDevPath').Text
$ffuName = $window.FindName('txtFFUName').Text
$vmSwitchSelected = $window.FindName('cmbVMSwitchName').SelectedItem
$customVMSwitch = $window.FindName('txtCustomVMSwitchName').Text
$vmHostIPAddress = $window.FindName('txtVMHostIPAddress').Text
# Windows tab
$wrItem = $window.FindName('cmbWindowsRelease').SelectedItem
$windowsRelease = if ($wrItem -and $wrItem.Value) {
[int]$wrItem.Value
} else {
10
}
$windowsVersion = $window.FindName('cmbWindowsVersion').SelectedItem
$windowsArch = $window.FindName('cmbWindowsArch').SelectedItem
$windowsLang = $window.FindName('cmbWindowsLang').SelectedItem
$windowsSKU = $window.FindName('cmbWindowsSKU').SelectedItem
$mediaType = $window.FindName('cmbMediaType').SelectedItem
$optionalFeatures= $window.FindName('txtOptionalFeatures').Text
$productKey = $window.FindName('txtProductKey').Text
$isoPath = $window.FindName('txtISOPath').Text
# Apps tab
$installApps = $window.FindName('chkInstallApps').IsChecked
# Office tab
$installOffice = $window.FindName('chkInstallOffice').IsChecked
$officePath = $window.FindName('txtOfficePath').Text
$copyOfficeConfig = $window.FindName('chkCopyOfficeConfigXML').IsChecked
$officeConfigXMLFile = $window.FindName('txtOfficeConfigXMLFilePath').Text
# Drivers tab
$installDrivers = $window.FindName('chkInstallDrivers').IsChecked
$copyDrivers = $window.FindName('chkCopyDrivers').IsChecked
$downloadDrivers = $window.FindName('chkDownloadDrivers').IsChecked
$make = $window.FindName('cmbMake').SelectedItem
$model = $window.FindName('cmbModel').Text
$driversFolder = $window.FindName('txtDriversFolder').Text
$peDriversFolder = $window.FindName('txtPEDriversFolder').Text
$copyPEDrivers = $window.FindName('chkCopyPEDrivers').IsChecked
# Basic validations
if (-not $ffuDevPath) {
throw "FFU Development Path is required."
}
if ($installDrivers -and (-not $driversFolder)) {
throw "Drivers Folder is required when Install Drivers is checked."
}
if ($copyDrivers -and (-not $driversFolder)) {
throw "Drivers Folder is required when Copy Drivers is checked."
}
if ($copyPEDrivers -and (-not $peDriversFolder)) {
throw "PE Drivers Folder is required when Copy PE Drivers is checked."
}
if ($downloadDrivers -and (-not $make)) {
throw "Make is required when Download Drivers is checked."
}
if ($downloadDrivers -and (-not $model)) {
throw "Model is required when Download Drivers is checked."
}
# If user picks 'Other' for VM Switch, use custom
$vmSwitchName = if ($vmSwitchSelected -eq 'Other') { $customVMSwitch } else { $vmSwitchSelected }
# Build config
$config = [ordered]@{
AppsPath = $AppsPath
CopyDrivers = $copyDrivers
CopyPEDrivers = $copyPEDrivers
CopyOfficeConfigXML = $copyOfficeConfig
DriversFolder = $driversFolder
DownloadDrivers = $downloadDrivers
FFUDevelopmentPath = $ffuDevPath
FFUName = $ffuName
InstallApps = $installApps
InstallDrivers = $installDrivers
InstallOffice = $installOffice
ISOPath = $isoPath
Make = if ($downloadDrivers) { $make } else { $null }
MediaType = $mediaType
Model = if ($downloadDrivers) { $model } else { $null }
OfficeConfigXMLFile = if ($installOffice -and $copyOfficeConfig) { $officeConfigXMLFile } else { $null }
OfficePath = $officePath
OptionalFeatures = $optionalFeatures
PEDriversFolder = $peDriversFolder
ProductKey = $productKey
VMHostIPAddress = $vmHostIPAddress
VMSwitchName = $vmSwitchName
WindowsArch = $windowsArch
WindowsLang = $windowsLang
WindowsRelease = $windowsRelease
WindowsSKU = $windowsSKU
WindowsVersion = $windowsVersion
}
return $config
}
# This function updates the Windows Release list:
# - If ISO path is blank => only show Windows 10, Windows 11
# - If ISO path is not blank => show all releases
function UpdateWindowsReleaseList {
param([string]$isoPath)
if (-not $script:cmbWindowsRelease) { return }
# Remember old item
$oldItem = $script:cmbWindowsRelease.SelectedItem
# Clear
$script:cmbWindowsRelease.Items.Clear()
# IMPORTANT: Set these paths so the ComboBox shows 'Display' text
$script:cmbWindowsRelease.DisplayMemberPath = 'Display'
$script:cmbWindowsRelease.SelectedValuePath = 'Value'
# If blank => only MCT
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
}
}
# Attempt to re-select old item (based on Value)
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)
}
$combo.SelectedIndex = 0
$combo.IsEnabled = $true
}
}
$script:RefreshWindowsUI = {
param([string]$isoPath)
# Refresh releases
UpdateWindowsReleaseList -isoPath $isoPath
# Then refresh version
$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)
# Assign images
foreach ($imgName in $imageNames) {
Set-ImageSource -window $window -imageName $imgName -sourcePath $infoImagePath
}
function UpdateOptionalFeaturesString {
if ($script:chkTelnetClient -and $script:chkNetFx3 -and $script:chkDirectPlay -and $script:chkSMB1Protocol -and $script:txtOptionalFeatures) {
$f = @()
if ($script:chkTelnetClient.IsChecked) { $f += "TelnetClient" }
if ($script:chkNetFx3.IsChecked) { $f += "NetFx3" }
if ($script:chkDirectPlay.IsChecked) { $f += "DirectPlay" }
if ($script:chkSMB1Protocol.IsChecked) { $f += "SMB1Protocol" }
$script:txtOptionalFeatures.Text = $f -join ";"
}
}
# Window Loaded
$window.Add_Loaded({
# Windows Release
$script:cmbWindowsRelease = $window.FindName('cmbWindowsRelease')
$script:cmbWindowsVersion = $window.FindName('cmbWindowsVersion')
$script:txtISOPath = $window.FindName('txtISOPath')
& $script:RefreshWindowsUI($defaultISOPath)
# Event: ISO path changed
$script:txtISOPath.Add_TextChanged({
& $script:RefreshWindowsUI($script:txtISOPath.Text)
})
# Event: Windows release changed
$script:cmbWindowsRelease.Add_SelectionChanged({
$selItem = $script:cmbWindowsRelease.SelectedItem
if ($selItem -and $selItem.Value) {
UpdateWindowsVersionCombo -selectedRelease $selItem.Value -isoPath $script:txtISOPath.Text
}
})
# Browse ISO
$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
}
})
# Basic defaults
$window.FindName('txtFFUDevPath').Text = $FFUDevelopmentPath
$window.FindName('txtFFUName').Text = "{WindowsRelease}_{WindowsVersion}_{SKU}_{yyyy}-{MM}-{dd}_{HH}{mm}"
# VM Switch detection
$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)
}
$script:cmbMediaType.SelectedItem = $defaultMediaType
$window.FindName('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'
})
# Optional Features
$script:chkTelnetClient = $window.FindName('chkTelnetClient')
$script:chkNetFx3 = $window.FindName('chkNetFx3')
$script:chkDirectPlay = $window.FindName('chkDirectPlay')
$script:chkSMB1Protocol = $window.FindName('chkSMB1Protocol')
$script:txtOptionalFeatures = $window.FindName('txtOptionalFeatures')
$script:chkTelnetClient.Add_Checked({ UpdateOptionalFeaturesString })
$script:chkTelnetClient.Add_Unchecked({ UpdateOptionalFeaturesString })
$script:chkNetFx3.Add_Checked({ UpdateOptionalFeaturesString })
$script:chkNetFx3.Add_Unchecked({ UpdateOptionalFeaturesString })
$script:chkDirectPlay.Add_Checked({ UpdateOptionalFeaturesString })
$script:chkDirectPlay.Add_Unchecked({ UpdateOptionalFeaturesString })
$script:chkSMB1Protocol.Add_Checked({ UpdateOptionalFeaturesString })
$script:chkSMB1Protocol.Add_Unchecked({ UpdateOptionalFeaturesString })
})
# 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 Office config XML needed
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")
}
})
[void]$window.ShowDialog()