@@ -1,5 +1,63 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
# 2604.1
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
|
||||||
|
### Fluent style
|
||||||
|
|
||||||
|
With the release of PowerShell 7.6 finally going to GA, I was able to release the Fluent UI styling refresh. This will bring significant improvement to the look and feel of FFU Builder. Note that you will want to make sure you're running **PowerShell 7.6**, otherwise the listviews for Drivers and Applications will be missing the column headers.
|
||||||
|
|
||||||
|
### Build tab reorganization
|
||||||
|
|
||||||
|
The build tab sections now have expanders for the settings within. This should help with organization of each setting.
|
||||||
|
|
||||||
|
### Home page build and release status
|
||||||
|
|
||||||
|
The Home page of FFU Builder will now tell you what build you're on and if there's a new build along with the release notes for the new build. You can also see disk space and hyper-v status, as well as the latest Github repo discussions and a list of resources.
|
||||||
|
|
||||||
|
### Fixed an issue with Surface and Lenovo driver downloads
|
||||||
|
|
||||||
|
Microsoft changed the Surface driver download support page. FFU Builder now uses the [Microsoft Learn page for Surface driver downloads ](https://learn.microsoft.com/en-us/surface/manage-surface-driver-and-firmware-updates) that's designed for IT Admins. It's an easier table to parse rather than trying to parse the updated support page that FFU Builder used to use.
|
||||||
|
|
||||||
|
Fixed an issue where retrieving Lenovo models was failing.
|
||||||
|
|
||||||
|
### Removed Capture ISO
|
||||||
|
|
||||||
|
FFU Builder no longer relies on booting to WinPE to capture builds done via the VM. Instead, FFU Builder will now just capture the VHDX directly. This improves FFU build times tremendously and reduces the need for the VM Switch. The switch is still necessary for those that want to add internet connectivity during the FFU build process.
|
||||||
|
|
||||||
|
### Refresh Windows SKU after fallback SKU selection
|
||||||
|
|
||||||
|
Fixed an issue grabbing the correct Windows SKU when the user incorrectly chose a SKU that wasn't in the media and had to later be prompted for an available SKU. In this situation the SKU that was provided earlier was chosen, which caused a variety of issues.
|
||||||
|
|
||||||
|
### Removed registry-based FFU file naming
|
||||||
|
|
||||||
|
Removed registry-based FFU file naming and now rely on parameters provided at build time and the custom FFU naming template. This will remove the hard coded wait times that had to do with loading/unloading of the registry.
|
||||||
|
|
||||||
|
### Added a checkbox to enable network connectivity during VM build
|
||||||
|
|
||||||
|
Add a checkbox to enable network connectivity during the VM build. I'm fairly confident that the build process should be able to withstand any sysprep-related issues being connected to the internet. The checkbox is flagged as experimental. Give it a try and let me know if you notice any issues.
|
||||||
|
|
||||||
|
### Added UI controls for Device Naming
|
||||||
|
|
||||||
|
Device naming now has an expander in the Build tab that will expose a number of new options available. Rather than writing up a whole thing here in the release notes, the UI should be intuitive enough to explain how it works. The docs have also been updated. I spent a lot of time testing the changes with both legacy naming scenarios and if you make changes in the UI. If you see something that doesn't work, open a discussion or issue.
|
||||||
|
|
||||||
|
### Fixed Office installation issues on ARM64 VMs
|
||||||
|
|
||||||
|
I actually didn't fix anything, but rather removed a restriction that was put in place due to Office requiring internet access to install on ARM64. It seems the PG has fixed the issue requiring internet access and office will now install. However there's a caveat that it will prompt with a compatibility assistant popup. I think we can disable the compatibility assistant service to prevent the pop up from happening in the orchestrator. Will look into this in a future release.
|
||||||
|
|
||||||
|
### Auto-generate ComputerName in Unattend.xml
|
||||||
|
|
||||||
|
Now you can provide your own Unattend.xml without a ComputerName element and FFU Builder will add it if you've chosen to include a computer name. If there's a ComputerName element already in the file, ApplyFFU.ps1 will find it and modify it as per your naming choices.
|
||||||
|
|
||||||
|
### Add custom unattend.xml paths
|
||||||
|
|
||||||
|
There's a new expander for Unattend.xml options in the Build tab which includes paths for the x64 and arm64 unattend.xml files. This means that you can have your unattend files in any location instead of in the FFUDevelopment\unattend folder. This should make upgrades easier for those that have custom unattend.xml files and copying new releases would overwrite your customized unattend files.
|
||||||
|
|
||||||
|
### Fixed an issue where CUs wouldn't service after the March 31, 2026 OOB update (KB5086672) was installed on your host machine
|
||||||
|
|
||||||
|
The KB5086672 CU which is rolled into the April 14, 2026 update (KB5083769) caused an issue with Add-WindowsPackage. Add-WindowsPackage uses the DISM API to service a Windows image. The native dism.exe doesn't have this issue. To keep things consistent, FFU Builder will now use the dism.exe from the installed Windows ADK. While this version of dism might be older than what's on your machine, it should be consistent and not be impacted by future CUs.
|
||||||
|
|
||||||
# 2603.2
|
# 2603.2
|
||||||
|
|
||||||
## What's Changed
|
## What's Changed
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
# Allow Orchestrator.ps1 to override the app list file paths while preserving legacy defaults.
|
||||||
|
param(
|
||||||
|
[Parameter()]
|
||||||
|
[string]$wingetAppsJsonFile = (Join-Path -Path $PSScriptRoot -ChildPath "WinGetWin32Apps.json"),
|
||||||
|
[Parameter()]
|
||||||
|
[string]$userAppsJsonFile = (Join-Path -Path (Split-Path -Parent $PSScriptRoot) -ChildPath "UserAppList.json")
|
||||||
|
)
|
||||||
|
|
||||||
function Invoke-Process {
|
function Invoke-Process {
|
||||||
[CmdletBinding(SupportsShouldProcess)]
|
[CmdletBinding(SupportsShouldProcess)]
|
||||||
param
|
param
|
||||||
@@ -247,11 +255,6 @@ function Install-Applications {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Define paths for the JSON files
|
|
||||||
$wingetAppsJsonFile = "$PSScriptRoot\WinGetWin32Apps.json"
|
|
||||||
# Look for UserAppList.json one directory level up from the script's location. This keeps the user specific json files (AppList.json and UserAppList.json in the Apps dir)
|
|
||||||
$userAppsJsonFile = Join-Path -Path (Split-Path -Parent $PSScriptRoot) -ChildPath "UserAppList.json"
|
|
||||||
|
|
||||||
# Initialize empty arrays for apps from each source
|
# Initialize empty arrays for apps from each source
|
||||||
$wingetApps = @()
|
$wingetApps = @()
|
||||||
$userApps = @()
|
$userApps = @()
|
||||||
@@ -286,9 +289,9 @@ if ($wingetApps.Count -gt 0) {
|
|||||||
Install-Applications -apps $wingetApps
|
Install-Applications -apps $wingetApps
|
||||||
}
|
}
|
||||||
|
|
||||||
# Read the UserAppList.json file if it exists
|
# Read the configured BYO app list file if it exists
|
||||||
if (Test-Path -Path $userAppsJsonFile) {
|
if (Test-Path -Path $userAppsJsonFile) {
|
||||||
Write-Host "Processing UserAppList.json..."
|
Write-Host "Processing $(Split-Path -Path $userAppsJsonFile -Leaf)..."
|
||||||
try {
|
try {
|
||||||
$userContent = Get-Content -Path $userAppsJsonFile -Raw -ErrorAction Stop | ConvertFrom-Json
|
$userContent = Get-Content -Path $userAppsJsonFile -Raw -ErrorAction Stop | ConvertFrom-Json
|
||||||
if ($userContent -is [array]) {
|
if ($userContent -is [array]) {
|
||||||
@@ -296,19 +299,19 @@ if (Test-Path -Path $userAppsJsonFile) {
|
|||||||
Write-Host "Found $(($userApps | Measure-Object).Count) user-defined apps."
|
Write-Host "Found $(($userApps | Measure-Object).Count) user-defined apps."
|
||||||
}
|
}
|
||||||
elseif ($userContent) {
|
elseif ($userContent) {
|
||||||
$userApps = @($userContent) # Ensure it's an array
|
$userApps = @($userContent)
|
||||||
Write-Host "Found 1 user-defined app."
|
Write-Host "Found 1 user-defined app."
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Write-Host "UserAppList.json is empty or invalid."
|
Write-Host "$(Split-Path -Path $userAppsJsonFile -Leaf) is empty or invalid."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
Write-Error "Failed to read or parse UserAppList.json file: $_"
|
Write-Error "Failed to read or parse BYO app list file '$userAppsJsonFile': $_"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Write-Host "UserAppList.json file not found. Skipping."
|
Write-Host "BYO app list file not found at $userAppsJsonFile. Skipping."
|
||||||
}
|
}
|
||||||
|
|
||||||
# Install User apps if any were found
|
# Install User apps if any were found
|
||||||
|
|||||||
@@ -28,6 +28,23 @@ Write-Host "---------------------------------------------------" -ForegroundColo
|
|||||||
# Define the path to the scripts
|
# Define the path to the scripts
|
||||||
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
|
||||||
|
# Resolve the configured BYO app list path for runtime orchestration.
|
||||||
|
$appInstallConfigPath = Join-Path -Path $scriptPath -ChildPath "AppInstallConfig.json"
|
||||||
|
$userAppsJsonFile = Join-Path -Path (Split-Path -Parent $scriptPath) -ChildPath "UserAppList.json"
|
||||||
|
|
||||||
|
if (Test-Path -Path $appInstallConfigPath) {
|
||||||
|
try {
|
||||||
|
$appInstallConfig = Get-Content -Path $appInstallConfigPath -Raw | ConvertFrom-Json
|
||||||
|
if ($null -ne $appInstallConfig -and $appInstallConfig.PSObject.Properties.Match('UserAppListPath').Count -gt 0 -and -not [string]::IsNullOrWhiteSpace($appInstallConfig.UserAppListPath)) {
|
||||||
|
$userAppsJsonFile = $appInstallConfig.UserAppListPath
|
||||||
|
Write-Host "Using BYO app list path from AppInstallConfig.json: $userAppsJsonFile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Host "Failed to parse AppInstallConfig.json. Falling back to default BYO app list path."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Define the list of scripts to run
|
# Define the list of scripts to run
|
||||||
$scriptList = @(
|
$scriptList = @(
|
||||||
"Install-LTSCUpdate.ps1",
|
"Install-LTSCUpdate.ps1",
|
||||||
@@ -51,7 +68,6 @@ foreach ($script in $scriptList) {
|
|||||||
switch ($script) {
|
switch ($script) {
|
||||||
"Install-Win32Apps.ps1" {
|
"Install-Win32Apps.ps1" {
|
||||||
$wingetAppsJsonFile = Join-Path -Path $scriptPath -ChildPath "WinGetWin32Apps.json"
|
$wingetAppsJsonFile = Join-Path -Path $scriptPath -ChildPath "WinGetWin32Apps.json"
|
||||||
$userAppsJsonFile = Join-Path -Path (Split-Path -Parent $scriptPath) -ChildPath "UserAppList.json"
|
|
||||||
if (-not (Test-Path -Path $wingetAppsJsonFile) -and -not (Test-Path -Path $userAppsJsonFile)) {
|
if (-not (Test-Path -Path $wingetAppsJsonFile) -and -not (Test-Path -Path $userAppsJsonFile)) {
|
||||||
$shouldRun = $false
|
$shouldRun = $false
|
||||||
}
|
}
|
||||||
@@ -69,8 +85,13 @@ foreach ($script in $scriptList) {
|
|||||||
Write-Host "---------------------------------------------------" -ForegroundColor Yellow
|
Write-Host "---------------------------------------------------" -ForegroundColor Yellow
|
||||||
Write-Host " Running script: $script " -ForegroundColor Yellow
|
Write-Host " Running script: $script " -ForegroundColor Yellow
|
||||||
Write-Host "---------------------------------------------------" -ForegroundColor Yellow
|
Write-Host "---------------------------------------------------" -ForegroundColor Yellow
|
||||||
# Run script and wait for it to finish
|
# Run script and wait for it to finish.
|
||||||
& $scriptFile
|
if ($script -eq "Install-Win32Apps.ps1") {
|
||||||
|
& $scriptFile -UserAppsJsonFile $userAppsJsonFile
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
& $scriptFile
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ $script:uiState = [PSCustomObject]@{
|
|||||||
logStreamReader = $null;
|
logStreamReader = $null;
|
||||||
pollTimer = $null;
|
pollTimer = $null;
|
||||||
currentBuildProcess = $null;
|
currentBuildProcess = $null;
|
||||||
lastConfigFilePath = $null
|
lastConfigFilePath = $null;
|
||||||
|
loadedDeviceNamingMode = $null
|
||||||
};
|
};
|
||||||
Flags = @{
|
Flags = @{
|
||||||
installAppsForcedByUpdates = $false;
|
installAppsForcedByUpdates = $false;
|
||||||
@@ -55,7 +56,10 @@ $script:uiState = [PSCustomObject]@{
|
|||||||
lastSortProperty = $null;
|
lastSortProperty = $null;
|
||||||
lastSortAscending = $true;
|
lastSortAscending = $true;
|
||||||
isBuilding = $false;
|
isBuilding = $false;
|
||||||
isCleanupRunning = $false
|
isCleanupRunning = $false;
|
||||||
|
isFluentSupported = $false;
|
||||||
|
deviceNamingModeWasExplicitlyChanged = $false;
|
||||||
|
suppressDeviceNamingChangeTracking = $false
|
||||||
};
|
};
|
||||||
Defaults = @{};
|
Defaults = @{};
|
||||||
LogFilePath = "$FFUDevelopmentPath\FFUDevelopment_UI.log"
|
LogFilePath = "$FFUDevelopmentPath\FFUDevelopment_UI.log"
|
||||||
@@ -120,6 +124,9 @@ $reader = New-Object System.IO.StringReader($xamlString)
|
|||||||
$xmlReader = [System.Xml.XmlReader]::Create($reader)
|
$xmlReader = [System.Xml.XmlReader]::Create($reader)
|
||||||
$window = [Windows.Markup.XamlReader]::Load($xmlReader)
|
$window = [Windows.Markup.XamlReader]::Load($xmlReader)
|
||||||
|
|
||||||
|
# Apply Fluent theme before the window renders (requires PowerShell 7.5+ / .NET 9+)
|
||||||
|
Initialize-FluentTheme -Window $window -ThemeMode "System" -State $script:uiState
|
||||||
|
|
||||||
$window.Add_Loaded({
|
$window.Add_Loaded({
|
||||||
# Pass the state object to all initialization functions
|
# Pass the state object to all initialization functions
|
||||||
$script:uiState.Window = $window
|
$script:uiState.Window = $window
|
||||||
@@ -129,6 +136,9 @@ $window.Add_Loaded({
|
|||||||
Initialize-DynamicUIElements -State $script:uiState
|
Initialize-DynamicUIElements -State $script:uiState
|
||||||
Register-EventHandlers -State $script:uiState
|
Register-EventHandlers -State $script:uiState
|
||||||
|
|
||||||
|
# Populate the Home page build and release status after the window initializes
|
||||||
|
Start-HomeStatusRefresh -State $script:uiState
|
||||||
|
|
||||||
# Attempt automatic load of previous environment (silent)
|
# Attempt automatic load of previous environment (silent)
|
||||||
try {
|
try {
|
||||||
Invoke-AutoLoadPreviousEnvironment -State $script:uiState
|
Invoke-AutoLoadPreviousEnvironment -State $script:uiState
|
||||||
@@ -390,8 +400,11 @@ $script:uiState.Controls.btnRun.Add_Click({
|
|||||||
# Not currently building: start a new build
|
# Not currently building: start a new build
|
||||||
$btnRun.IsEnabled = $false
|
$btnRun.IsEnabled = $false
|
||||||
|
|
||||||
# Switch to Monitor Tab
|
# Switch to Monitor page via navigation
|
||||||
$script:uiState.Controls.MainTabControl.SelectedItem = $script:uiState.Controls.MonitorTab
|
$monitorIndex = 8 # Monitor is the 9th item (index 8) in the navigation list
|
||||||
|
if ($null -ne $script:uiState.Controls.lstNavigation) {
|
||||||
|
$script:uiState.Controls.lstNavigation.SelectedIndex = $monitorIndex
|
||||||
|
}
|
||||||
|
|
||||||
# Clear previous log data and reset autoscroll
|
# Clear previous log data and reset autoscroll
|
||||||
if ($null -ne $script:uiState.Data.logData) {
|
if ($null -ne $script:uiState.Data.logData) {
|
||||||
@@ -415,6 +428,123 @@ $script:uiState.Controls.btnRun.Add_Click({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($config.EnableVMNetworking -and $config.InstallApps -and [string]::IsNullOrWhiteSpace([string]$config.VMSwitchName)) {
|
||||||
|
[System.Windows.MessageBox]::Show("Select or enter a VM Switch Name before enabling VM networking.", "VM Switch Required", "OK", "Warning") | Out-Null
|
||||||
|
$btnRun.IsEnabled = $true
|
||||||
|
$script:uiState.Controls.txtStatus.Text = "Build canceled: VM switch required for experimental networking."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($config.CopyUnattend -and $config.InjectUnattend) {
|
||||||
|
[System.Windows.MessageBox]::Show("Copy Unattend.xml and Inject Unattend.xml cannot both be selected. Choose only one unattend delivery method.", "Unattend Selection Required", "OK", "Warning") | Out-Null
|
||||||
|
$btnRun.IsEnabled = $true
|
||||||
|
$script:uiState.Controls.txtStatus.Text = "Build canceled: choose only one unattend delivery method."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($config.CopyUnattend -or $config.InjectUnattend) {
|
||||||
|
$selectedUnattendArch = if ($config.WindowsArch -ieq 'arm64') { 'arm64' } else { 'x64' }
|
||||||
|
$selectedUnattendSourcePath = if ($selectedUnattendArch -eq 'arm64') {
|
||||||
|
[string]$config.UnattendArm64FilePath
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[string]$config.UnattendX64FilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($selectedUnattendSourcePath)) {
|
||||||
|
[System.Windows.MessageBox]::Show("Select a valid $selectedUnattendArch unattend XML file before using Copy Unattend.xml or Inject Unattend.xml.", "Unattend File Required", "OK", "Warning") | Out-Null
|
||||||
|
$btnRun.IsEnabled = $true
|
||||||
|
$script:uiState.Controls.txtStatus.Text = "Build canceled: unattend file path required."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -Path $selectedUnattendSourcePath -PathType Leaf)) {
|
||||||
|
[System.Windows.MessageBox]::Show("The selected $selectedUnattendArch unattend XML file was not found:`n$selectedUnattendSourcePath", "Unattend File Missing", "OK", "Warning") | Out-Null
|
||||||
|
$btnRun.IsEnabled = $true
|
||||||
|
$script:uiState.Controls.txtStatus.Text = "Build canceled: unattend file missing."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedUnattendFileInfo = Get-Item -Path $selectedUnattendSourcePath -ErrorAction SilentlyContinue
|
||||||
|
if (($null -eq $selectedUnattendFileInfo) -or ($selectedUnattendFileInfo.Length -le 0)) {
|
||||||
|
[System.Windows.MessageBox]::Show("The selected $selectedUnattendArch unattend XML file is empty:`n$selectedUnattendSourcePath", "Unattend File Empty", "OK", "Warning") | Out-Null
|
||||||
|
$btnRun.IsEnabled = $true
|
||||||
|
$script:uiState.Controls.txtStatus.Text = "Build canceled: unattend file empty."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($config.DeviceNamingMode -eq 'Prompt') {
|
||||||
|
if (-not $config.CopyUnattend) {
|
||||||
|
[System.Windows.MessageBox]::Show("Select Copy Unattend.xml before using 'Prompt for Device Name'.", "Copy Unattend Required", "OK", "Warning") | Out-Null
|
||||||
|
$btnRun.IsEnabled = $true
|
||||||
|
$script:uiState.Controls.txtStatus.Text = "Build canceled: prompt naming requires Copy Unattend.xml."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($config.DeviceNamingMode -eq 'Template') {
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$config.DeviceNameTemplate)) {
|
||||||
|
[System.Windows.MessageBox]::Show("Specify a device name before using 'Specify Device Name'.", "Device Name Required", "OK", "Warning") | Out-Null
|
||||||
|
$btnRun.IsEnabled = $true
|
||||||
|
$script:uiState.Controls.txtStatus.Text = "Build canceled: device name required."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not ($config.CopyUnattend -or $config.InjectUnattend)) {
|
||||||
|
[System.Windows.MessageBox]::Show("Select Copy Unattend.xml or Inject Unattend.xml before using 'Specify Device Name'.", "Unattend Selection Required", "OK", "Warning") | Out-Null
|
||||||
|
$btnRun.IsEnabled = $true
|
||||||
|
$script:uiState.Controls.txtStatus.Text = "Build canceled: unattend delivery method required for device naming."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$templateWithoutSupportedVariables = ([string]$config.DeviceNameTemplate) -replace '(?i)%serial%', ''
|
||||||
|
if ($templateWithoutSupportedVariables -match '%') {
|
||||||
|
[System.Windows.MessageBox]::Show("Only the %serial% device name variable is supported.", "Unsupported Device Name Variable", "OK", "Warning") | Out-Null
|
||||||
|
$btnRun.IsEnabled = $true
|
||||||
|
$script:uiState.Controls.txtStatus.Text = "Build canceled: unsupported device name variable."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($config.InjectUnattend -and (-not $config.CopyUnattend) -and ([string]$config.DeviceNameTemplate -match '(?i)%serial%')) {
|
||||||
|
[System.Windows.MessageBox]::Show("The %serial% device name variable is only supported when Copy Unattend.xml is selected.", "Unsupported Inject Unattend Setting", "OK", "Warning") | Out-Null
|
||||||
|
$btnRun.IsEnabled = $true
|
||||||
|
$script:uiState.Controls.txtStatus.Text = "Build canceled: %serial% requires Copy Unattend.xml."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($config.DeviceNamingMode -eq 'Prefixes') {
|
||||||
|
if (-not $config.CopyUnattend) {
|
||||||
|
[System.Windows.MessageBox]::Show("Select Copy Unattend.xml before using 'Specify a list of Prefixes'.", "Copy Unattend Required", "OK", "Warning") | Out-Null
|
||||||
|
$btnRun.IsEnabled = $true
|
||||||
|
$script:uiState.Controls.txtStatus.Text = "Build canceled: prefixes require Copy Unattend.xml."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasSavedPrefixesPath = -not [string]::IsNullOrWhiteSpace([string]$config.DeviceNamePrefixesPath) -and (Test-Path -Path $config.DeviceNamePrefixesPath -PathType Leaf)
|
||||||
|
if ((($null -eq $config.DeviceNamePrefixes) -or ($config.DeviceNamePrefixes.Count -eq 0)) -and -not $hasSavedPrefixesPath) {
|
||||||
|
[System.Windows.MessageBox]::Show("Enter at least one prefix or choose a valid prefixes file before using 'Specify a list of Prefixes'.", "Prefixes Required", "OK", "Warning") | Out-Null
|
||||||
|
$btnRun.IsEnabled = $true
|
||||||
|
$script:uiState.Controls.txtStatus.Text = "Build canceled: prefixes required."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($config.DeviceNamingMode -eq 'SerialComputerNames') {
|
||||||
|
if (-not $config.CopyUnattend) {
|
||||||
|
[System.Windows.MessageBox]::Show("Select Copy Unattend.xml before using 'Specify Serial to Device Name Mapping'.", "Copy Unattend Required", "OK", "Warning") | Out-Null
|
||||||
|
$btnRun.IsEnabled = $true
|
||||||
|
$script:uiState.Controls.txtStatus.Text = "Build canceled: serial computer-name mapping requires Copy Unattend.xml."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasSavedSerialComputerNamesPath = -not [string]::IsNullOrWhiteSpace([string]$config.DeviceNameSerialComputerNamesPath) -and (Test-Path -Path $config.DeviceNameSerialComputerNamesPath -PathType Leaf)
|
||||||
|
if ((($null -eq $config.DeviceNameSerialComputerNames) -or ($config.DeviceNameSerialComputerNames.Count -eq 0)) -and -not $hasSavedSerialComputerNamesPath) {
|
||||||
|
[System.Windows.MessageBox]::Show("Enter CSV content or choose a valid Serial Computer Names CSV Mapping File Path before using 'Specify Serial to Device Name Mapping'.", "Serial Mapping Required", "OK", "Warning") | Out-Null
|
||||||
|
$btnRun.IsEnabled = $true
|
||||||
|
$script:uiState.Controls.txtStatus.Text = "Build canceled: serial computer-name mapping required."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$configFilePath = Join-Path $config.FFUDevelopmentPath "\config\FFUConfig.json"
|
$configFilePath = Join-Path $config.FFUDevelopmentPath "\config\FFUConfig.json"
|
||||||
# Sort top-level keys alphabetically for consistent output
|
# Sort top-level keys alphabetically for consistent output
|
||||||
$sortedConfig = [ordered]@{}
|
$sortedConfig = [ordered]@{}
|
||||||
|
|||||||
@@ -3,11 +3,8 @@ param (
|
|||||||
[string]$adkPath = 'C:\Program Files (x86)\Windows Kits\10\',
|
[string]$adkPath = 'C:\Program Files (x86)\Windows Kits\10\',
|
||||||
[string]$WindowsArch = 'x64',
|
[string]$WindowsArch = 'x64',
|
||||||
[bool]$CopyPEDrivers = $false,
|
[bool]$CopyPEDrivers = $false,
|
||||||
[string]$CaptureISO = "$PSScriptRoot\WinPE_FFU_Capture_x64.iso",
|
|
||||||
[string]$DeployISO = "$PSScriptRoot\WinPE_FFU_Deploy_x64.iso",
|
[string]$DeployISO = "$PSScriptRoot\WinPE_FFU_Deploy_x64.iso",
|
||||||
[string]$LogFile = "$PSScriptRoot\Create-PEMedia.log",
|
[string]$LogFile = "$PSScriptRoot\Create-PEMedia.log"
|
||||||
[bool]$Capture,
|
|
||||||
[bool]$Deploy = $true
|
|
||||||
)
|
)
|
||||||
|
|
||||||
function WriteLog($LogText) {
|
function WriteLog($LogText) {
|
||||||
@@ -77,12 +74,7 @@ function Invoke-Process {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function New-PEMedia {
|
function New-PEMedia {
|
||||||
param (
|
param ()
|
||||||
[Parameter()]
|
|
||||||
[bool]$Capture,
|
|
||||||
[Parameter()]
|
|
||||||
[bool]$Deploy
|
|
||||||
)
|
|
||||||
#Need to use the Demployment and Imaging tools environment to create winPE media
|
#Need to use the Demployment and Imaging tools environment to create winPE media
|
||||||
$DandIEnv = "$adkPath`Assessment and Deployment Kit\Deployment Tools\DandISetEnv.bat"
|
$DandIEnv = "$adkPath`Assessment and Deployment Kit\Deployment Tools\DandISetEnv.bat"
|
||||||
$WinPEFFUPath = "$FFUDevelopmentPath\WinPE"
|
$WinPEFFUPath = "$FFUDevelopmentPath\WinPE"
|
||||||
@@ -135,36 +127,21 @@ function New-PEMedia {
|
|||||||
Add-WindowsPackage -Path "$WinPEFFUPath\mount" -PackagePath $PackagePath | Out-Null
|
Add-WindowsPackage -Path "$WinPEFFUPath\mount" -PackagePath $PackagePath | Out-Null
|
||||||
WriteLog "Adding package complete"
|
WriteLog "Adding package complete"
|
||||||
}
|
}
|
||||||
If ($Capture) {
|
WriteLog "Copying $FFUDevelopmentPath\WinPEDeployFFUFiles\* to WinPE deploy media"
|
||||||
WriteLog "Copying $FFUDevelopmentPath\WinPECaptureFFUFiles\* to WinPE capture media"
|
Copy-Item -Path "$FFUDevelopmentPath\WinPEDeployFFUFiles\*" -Destination "$WinPEFFUPath\mount" -Recurse -Force | Out-Null
|
||||||
Copy-Item -Path "$FFUDevelopmentPath\WinPECaptureFFUFiles\*" -Destination "$WinPEFFUPath\mount" -Recurse -Force | out-null
|
WriteLog 'Copy complete'
|
||||||
WriteLog "Copy complete"
|
#If $CopyPEDrivers = $true, add drivers to WinPE media using dism
|
||||||
#Remove Bootfix.bin - for BIOS systems, shouldn't be needed, but doesn't hurt to remove for our purposes
|
if ($CopyPEDrivers) {
|
||||||
#Remove-Item -Path "$WinPEFFUPath\media\boot\bootfix.bin" -Force | Out-null
|
WriteLog "Adding drivers to WinPE media"
|
||||||
# $WinPEISOName = 'WinPE_FFU_Capture.iso'
|
try {
|
||||||
$WinPEISOFile = $CaptureISO
|
Add-WindowsDriver -Path "$WinPEFFUPath\Mount" -Driver "$FFUDevelopmentPath\PEDrivers" -Recurse -ErrorAction SilentlyContinue | Out-null
|
||||||
# $Capture = $false
|
|
||||||
}
|
|
||||||
If ($Deploy) {
|
|
||||||
WriteLog "Copying $FFUDevelopmentPath\WinPEDeployFFUFiles\* to WinPE deploy media"
|
|
||||||
Copy-Item -Path "$FFUDevelopmentPath\WinPEDeployFFUFiles\*" -Destination "$WinPEFFUPath\mount" -Recurse -Force | Out-Null
|
|
||||||
WriteLog 'Copy complete'
|
|
||||||
#If $CopyPEDrivers = $true, add drivers to WinPE media using dism
|
|
||||||
if ($CopyPEDrivers) {
|
|
||||||
WriteLog "Adding drivers to WinPE media"
|
|
||||||
try {
|
|
||||||
Add-WindowsDriver -Path "$WinPEFFUPath\Mount" -Driver "$FFUDevelopmentPath\PEDrivers" -Recurse -ErrorAction SilentlyContinue | Out-null
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
WriteLog 'Some drivers failed to be added to the FFU. This can be expected. Continuing.'
|
|
||||||
}
|
|
||||||
WriteLog "Adding drivers complete"
|
|
||||||
}
|
}
|
||||||
# $WinPEISOName = 'WinPE_FFU_Deploy.iso'
|
catch {
|
||||||
$WinPEISOFile = $DeployISO
|
WriteLog 'Some drivers failed to be added to the FFU. This can be expected. Continuing.'
|
||||||
|
}
|
||||||
# $Deploy = $false
|
WriteLog "Adding drivers complete"
|
||||||
}
|
}
|
||||||
|
$WinPEISOFile = $DeployISO
|
||||||
WriteLog 'Dismounting WinPE media'
|
WriteLog 'Dismounting WinPE media'
|
||||||
Dismount-WindowsImage -Path "$WinPEFFUPath\mount" -Save | Out-Null
|
Dismount-WindowsImage -Path "$WinPEFFUPath\mount" -Save | Out-Null
|
||||||
WriteLog 'Dismount complete'
|
WriteLog 'Dismount complete'
|
||||||
@@ -179,21 +156,10 @@ function New-PEMedia {
|
|||||||
WriteLog "Creating WinPE ISO at $WinPEISOFile"
|
WriteLog "Creating WinPE ISO at $WinPEISOFile"
|
||||||
# & "$OSCDIMG" -m -o -u2 -udfver102 -bootdata:2`#p0,e,b$OSCDIMGPath\etfsboot.com`#pEF,e,b$OSCDIMGPath\Efisys_noprompt.bin $WinPEFFUPath\media $FFUDevelopmentPath\$WinPEISOName | Out-null
|
# & "$OSCDIMG" -m -o -u2 -udfver102 -bootdata:2`#p0,e,b$OSCDIMGPath\etfsboot.com`#pEF,e,b$OSCDIMGPath\Efisys_noprompt.bin $WinPEFFUPath\media $FFUDevelopmentPath\$WinPEISOName | Out-null
|
||||||
if($WindowsArch -eq 'x64'){
|
if($WindowsArch -eq 'x64'){
|
||||||
if($Capture){
|
$OSCDIMGArgs = "-m -o -u2 -udfver102 -bootdata:2`#p0,e,b`"$OSCDIMGPath\etfsboot.com`"`#pEF,e,b`"$OSCDIMGPath\Efisys.bin`" `"$WinPEFFUPath\media`" `"$WinPEISOFile`""
|
||||||
$OSCDIMGArgs = "-m -o -u2 -udfver102 -bootdata:2`#p0,e,b`"$OSCDIMGPath\etfsboot.com`"`#pEF,e,b`"$OSCDIMGPath\Efisys_noprompt.bin`" `"$WinPEFFUPath\media`" `"$WinPEISOFile`""
|
|
||||||
}
|
|
||||||
if($Deploy){
|
|
||||||
$OSCDIMGArgs = "-m -o -u2 -udfver102 -bootdata:2`#p0,e,b`"$OSCDIMGPath\etfsboot.com`"`#pEF,e,b`"$OSCDIMGPath\Efisys.bin`" `"$WinPEFFUPath\media`" `"$WinPEISOFile`""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
elseif($WindowsArch -eq 'arm64'){
|
elseif($WindowsArch -eq 'arm64'){
|
||||||
if($Capture){
|
$OSCDIMGArgs = "-m -o -u2 -udfver102 -bootdata:1`#pEF,e,b`"$OSCDIMGPath\Efisys.bin`" `"$WinPEFFUPath\media`" `"$WinPEISOFile`""
|
||||||
$OSCDIMGArgs = "-m -o -u2 -udfver102 -bootdata:1`#pEF,e,b`"$OSCDIMGPath\Efisys_noprompt.bin`" `"$WinPEFFUPath\media`" `"$WinPEISOFile`""
|
|
||||||
}
|
|
||||||
if($Deploy){
|
|
||||||
$OSCDIMGArgs = "-m -o -u2 -udfver102 -bootdata:1`#pEF,e,b`"$OSCDIMGPath\Efisys.bin`" `"$WinPEFFUPath\media`" `"$WinPEISOFile`""
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
Invoke-Process $OSCDIMG $OSCDIMGArgs
|
Invoke-Process $OSCDIMG $OSCDIMGArgs
|
||||||
WriteLog "ISO created successfully"
|
WriteLog "ISO created successfully"
|
||||||
@@ -201,9 +167,4 @@ function New-PEMedia {
|
|||||||
Remove-Item -Path "$WinPEFFUPath" -Recurse -Force
|
Remove-Item -Path "$WinPEFFUPath" -Recurse -Force
|
||||||
WriteLog 'Cleanup complete'
|
WriteLog 'Cleanup complete'
|
||||||
}
|
}
|
||||||
if($Capture){
|
New-PEMedia
|
||||||
New-PEMedia -Capture $Capture
|
|
||||||
}
|
|
||||||
if($Deploy){
|
|
||||||
New-PEMedia -Deploy $Deploy
|
|
||||||
}
|
|
||||||
@@ -85,12 +85,10 @@ graph TD
|
|||||||
subgraph "VM-Based Capture (-InstallApps)"
|
subgraph "VM-Based Capture (-InstallApps)"
|
||||||
direction LR
|
direction LR
|
||||||
BB[Create Hyper-V VM from VHDX];
|
BB[Create Hyper-V VM from VHDX];
|
||||||
BB --> BC["Create WinPE Capture Media iso"];
|
BB --> BE["Start VM: Boots to Audit Mode"];
|
||||||
BC --> BD[Configure network share for capture];
|
|
||||||
BD --> BE["Start VM: Boots to Audit Mode"];
|
|
||||||
BE --> BF[Orchestrator runs: Installs apps, syspreps, shuts down];
|
BE --> BF[Orchestrator runs: Installs apps, syspreps, shuts down];
|
||||||
BF --> BG[VM reboots from Capture Media];
|
BF --> BG[Host optimizes and remounts VHDX];
|
||||||
BG --> BH["CaptureFFU.ps1 runs, saves FFU to share, shuts down"];
|
BG --> BH["DISM captures FFU directly from host-mounted VHDX"];
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph "Direct VHDX Capture"
|
subgraph "Direct VHDX Capture"
|
||||||
|
|||||||
@@ -6,11 +6,9 @@ function Invoke-FFUPostBuildCleanup {
|
|||||||
[string]$AppsPath,
|
[string]$AppsPath,
|
||||||
[string]$DriversPath,
|
[string]$DriversPath,
|
||||||
[string]$FFUCapturePath,
|
[string]$FFUCapturePath,
|
||||||
[string]$CaptureISOPath,
|
|
||||||
[string]$DeployISOPath,
|
[string]$DeployISOPath,
|
||||||
[string]$AppsISOPath,
|
[string]$AppsISOPath,
|
||||||
[string]$KBPath,
|
[string]$KBPath,
|
||||||
[bool]$RemoveCaptureISO = $false,
|
|
||||||
[bool]$RemoveDeployISO = $false,
|
[bool]$RemoveDeployISO = $false,
|
||||||
[bool]$RemoveAppsISO = $false,
|
[bool]$RemoveAppsISO = $false,
|
||||||
[bool]$RemoveDrivers = $false,
|
[bool]$RemoveDrivers = $false,
|
||||||
@@ -22,13 +20,9 @@ function Invoke-FFUPostBuildCleanup {
|
|||||||
$originalProgressPreference = $ProgressPreference
|
$originalProgressPreference = $ProgressPreference
|
||||||
$ProgressPreference = 'SilentlyContinue'
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
try {
|
try {
|
||||||
WriteLog "CommonCleanup: Starting cleanup (CaptureISO=$RemoveCaptureISO DeployISO=$RemoveDeployISO AppsISO=$RemoveAppsISO Drivers=$RemoveDrivers FFU=$RemoveFFU Apps=$RemoveApps Updates=$RemoveUpdates RemoveDownloadedESD=$RemoveDownloadedESD KBPath=$KBPath)."
|
WriteLog "CommonCleanup: Starting cleanup (DeployISO=$RemoveDeployISO AppsISO=$RemoveAppsISO Drivers=$RemoveDrivers FFU=$RemoveFFU Apps=$RemoveApps Updates=$RemoveUpdates RemoveDownloadedESD=$RemoveDownloadedESD KBPath=$KBPath)."
|
||||||
|
|
||||||
# Primary ISO paths (new naming/location)
|
# Primary ISO paths (new naming/location)
|
||||||
if ($RemoveCaptureISO -and -not [string]::IsNullOrWhiteSpace($CaptureISOPath) -and (Test-Path -LiteralPath $CaptureISOPath)) {
|
|
||||||
WriteLog "CommonCleanup: Removing $CaptureISOPath"
|
|
||||||
try { Remove-Item -LiteralPath $CaptureISOPath -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $CaptureISOPath : $($_.Exception.Message)" }
|
|
||||||
}
|
|
||||||
if ($RemoveDeployISO -and -not [string]::IsNullOrWhiteSpace($DeployISOPath) -and (Test-Path -LiteralPath $DeployISOPath)) {
|
if ($RemoveDeployISO -and -not [string]::IsNullOrWhiteSpace($DeployISOPath) -and (Test-Path -LiteralPath $DeployISOPath)) {
|
||||||
WriteLog "CommonCleanup: Removing $DeployISOPath"
|
WriteLog "CommonCleanup: Removing $DeployISOPath"
|
||||||
try { Remove-Item -LiteralPath $DeployISOPath -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $DeployISOPath : $($_.Exception.Message)" }
|
try { Remove-Item -LiteralPath $DeployISOPath -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $DeployISOPath : $($_.Exception.Message)" }
|
||||||
@@ -39,11 +33,6 @@ function Invoke-FFUPostBuildCleanup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Legacy / root-level WinPE ISOs (pattern-based)
|
# Legacy / root-level WinPE ISOs (pattern-based)
|
||||||
if ($RemoveCaptureISO) {
|
|
||||||
Get-ChildItem -LiteralPath $RootPath -Filter 'WinPE_FFU_Capture*.iso' -ErrorAction SilentlyContinue | ForEach-Object {
|
|
||||||
try { WriteLog "CommonCleanup: Removing legacy capture ISO $($_.FullName)"; Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing legacy capture ISO $($_.FullName) : $($_.Exception.Message)" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($RemoveDeployISO) {
|
if ($RemoveDeployISO) {
|
||||||
Get-ChildItem -LiteralPath $RootPath -Filter 'WinPE_FFU_Deploy*.iso' -ErrorAction SilentlyContinue | ForEach-Object {
|
Get-ChildItem -LiteralPath $RootPath -Filter 'WinPE_FFU_Deploy*.iso' -ErrorAction SilentlyContinue | ForEach-Object {
|
||||||
try { WriteLog "CommonCleanup: Removing legacy deploy ISO $($_.FullName)"; Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing legacy deploy ISO $($_.FullName) : $($_.Exception.Message)" }
|
try { WriteLog "CommonCleanup: Removing legacy deploy ISO $($_.FullName)"; Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing legacy deploy ISO $($_.FullName) : $($_.Exception.Message)" }
|
||||||
|
|||||||
@@ -161,12 +161,181 @@ function ConvertTo-SurfaceComparableName {
|
|||||||
|
|
||||||
# Cleanup: remove orphaned "with" left behind by earlier removals (e.g., "Surface Pro 9 with Intel Processor")
|
# Cleanup: remove orphaned "with" left behind by earlier removals (e.g., "Surface Pro 9 with Intel Processor")
|
||||||
$value = $value -replace '(?i)\bwith\b', ''
|
$value = $value -replace '(?i)\bwith\b', ''
|
||||||
$value = $value -replace '\s+', ' '
|
$value = $value -replace '\s+', ' '
|
||||||
|
|
||||||
return $value.Trim().ToUpperInvariant()
|
return $value.Trim().ToUpperInvariant()
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-SurfaceSystemSkuReferenceIndex {
|
function ConvertTo-SurfaceHtmlText {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[string]$HtmlFragment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Normalize HTML fragments from the Learn table into plain text values.
|
||||||
|
$textValue = $HtmlFragment -replace '<br\s*/?>', ' '
|
||||||
|
$textValue = $textValue -replace '<[^>]+>', ' '
|
||||||
|
$textValue = [System.Net.WebUtility]::HtmlDecode($textValue)
|
||||||
|
$textValue = $textValue -replace '\s+', ' '
|
||||||
|
|
||||||
|
return $textValue.Trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-SurfaceDownloadCenterLink {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$LinkValue
|
||||||
|
)
|
||||||
|
|
||||||
|
# Normalize Learn links down to the canonical Download Center details URL.
|
||||||
|
$decodedLink = [System.Net.WebUtility]::HtmlDecode($LinkValue).Trim()
|
||||||
|
if ([string]::IsNullOrWhiteSpace($decodedLink)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($decodedLink.StartsWith('/')) {
|
||||||
|
$decodedLink = "https://www.microsoft.com$decodedLink"
|
||||||
|
}
|
||||||
|
|
||||||
|
$downloadCenterMatch = [regex]::Match(
|
||||||
|
$decodedLink,
|
||||||
|
'https://www\.microsoft\.com(?:/en-us)?/download/details\.aspx\?id=\d+',
|
||||||
|
[System.Text.RegularExpressions.RegexOptions]::IgnoreCase
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $downloadCenterMatch.Success) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($downloadCenterMatch.Value -replace '/en-us/', '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SurfaceDriverModelIndex {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$DriversFolder
|
||||||
|
)
|
||||||
|
|
||||||
|
$url = 'https://learn.microsoft.com/en-us/surface/manage-surface-driver-and-firmware-updates'
|
||||||
|
$minimumExpectedModelCount = 10
|
||||||
|
|
||||||
|
# Load the cached model list first to keep Microsoft model discovery fast.
|
||||||
|
try {
|
||||||
|
$cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder
|
||||||
|
if (@($cache.ModelIndex).Count -gt 0) {
|
||||||
|
WriteLog "Surface cache: Using cached Microsoft model list ($(@($cache.ModelIndex).Count) models)."
|
||||||
|
return @($cache.ModelIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Surface cache: Failed to load cached Microsoft model list. Falling back to the Learn source. Error: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
# Download the Learn article that now contains the authoritative Surface package table.
|
||||||
|
WriteLog "Surface cache: Downloading Microsoft model index from $url"
|
||||||
|
$headers = @{
|
||||||
|
'User-Agent' = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
|
||||||
|
}
|
||||||
|
$webContent = Invoke-WebRequest -Uri $url -UseBasicParsing -Headers $headers
|
||||||
|
$html = $webContent.Content
|
||||||
|
|
||||||
|
# Parse each table row and keep only Download Center package links.
|
||||||
|
$rowMatches = [regex]::Matches($html, '<tr[^>]*>(.*?)</tr>', [System.Text.RegularExpressions.RegexOptions]::Singleline)
|
||||||
|
$models = [System.Collections.Generic.List[pscustomobject]]::new()
|
||||||
|
$seenModelKeys = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
|
||||||
|
foreach ($rowMatch in $rowMatches) {
|
||||||
|
$rowContent = $rowMatch.Groups[1].Value
|
||||||
|
$cellMatches = [regex]::Matches($rowContent, '<td[^>]*>\s*(.*?)\s*</td>', [System.Text.RegularExpressions.RegexOptions]::Singleline)
|
||||||
|
if ($cellMatches.Count -lt 2) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$rowLabel = ConvertTo-SurfaceHtmlText -HtmlFragment $cellMatches[0].Groups[1].Value
|
||||||
|
if ([string]::IsNullOrWhiteSpace($rowLabel) -or $rowLabel -notmatch '(?i)^Surface') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$downloadCellContent = $cellMatches[1].Groups[1].Value
|
||||||
|
$linkMatches = [regex]::Matches(
|
||||||
|
$downloadCellContent,
|
||||||
|
'<a[^>]+href="([^"]+)"[^>]*>(.*?)</a>',
|
||||||
|
[System.Text.RegularExpressions.RegexOptions]::Singleline -bor [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($linkMatch in $linkMatches) {
|
||||||
|
$modelName = ConvertTo-SurfaceHtmlText -HtmlFragment $linkMatch.Groups[2].Value
|
||||||
|
$modelLink = ConvertTo-SurfaceDownloadCenterLink -LinkValue $linkMatch.Groups[1].Value
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($modelName) -or [string]::IsNullOrWhiteSpace($modelLink)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$modelKey = "$modelName`n$modelLink"
|
||||||
|
if (-not $seenModelKeys.Add($modelKey)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$models.Add([pscustomobject]@{
|
||||||
|
Make = 'Microsoft'
|
||||||
|
Model = $modelName
|
||||||
|
Link = $modelLink
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($models.Count -eq 0) {
|
||||||
|
throw "No Microsoft driver models were found in the Learn table."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($models.Count -lt $minimumExpectedModelCount) {
|
||||||
|
WriteLog "Surface cache: Warning - Learn parsing returned only $($models.Count) Microsoft model entries."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "Surface cache: Parsed $($models.Count) Microsoft model entries from Learn."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save the refreshed model list into the shared cache for both UI and CLI use.
|
||||||
|
try {
|
||||||
|
$cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder
|
||||||
|
$cache.ModelIndex = @($models)
|
||||||
|
Save-SurfaceDriverIndexCache -Cache $cache -DriversFolder $DriversFolder
|
||||||
|
WriteLog "Surface cache: Saved Microsoft model list to cache."
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Surface cache: Failed to save Microsoft model list. Error: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return @($models)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Surface cache: Failed to build Microsoft model list from Learn. Error: $($_.Exception.Message)"
|
||||||
|
|
||||||
|
# Fall back to the last cached model list even if it is stale when the live request fails.
|
||||||
|
try {
|
||||||
|
$cachePath = Get-SurfaceDriverIndexCachePath -DriversFolder $DriversFolder
|
||||||
|
if (Test-Path -Path $cachePath -PathType Leaf) {
|
||||||
|
$staleCache = Get-Content -Path $cachePath -Raw | ConvertFrom-Json -ErrorAction Stop
|
||||||
|
if (@($staleCache.ModelIndex).Count -gt 0) {
|
||||||
|
WriteLog "Surface cache: Using stale Microsoft model list ($(@($staleCache.ModelIndex).Count) models) because the live Learn request failed."
|
||||||
|
return @($staleCache.ModelIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Surface cache: Failed to load stale Microsoft model list fallback. Error: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "Failed to retrieve Microsoft Surface models."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SurfaceSystemSkuReferenceIndex {
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
@@ -586,6 +755,9 @@ Export-ModuleMember -Function `
|
|||||||
Import-SurfaceDriverIndexCache, `
|
Import-SurfaceDriverIndexCache, `
|
||||||
Save-SurfaceDriverIndexCache, `
|
Save-SurfaceDriverIndexCache, `
|
||||||
ConvertTo-SurfaceComparableName, `
|
ConvertTo-SurfaceComparableName, `
|
||||||
|
ConvertTo-SurfaceHtmlText, `
|
||||||
|
ConvertTo-SurfaceDownloadCenterLink, `
|
||||||
|
Get-SurfaceDriverModelIndex, `
|
||||||
Get-SurfaceSystemSkuReferenceIndex, `
|
Get-SurfaceSystemSkuReferenceIndex, `
|
||||||
Get-SurfaceDownloadCenterDetails, `
|
Get-SurfaceDownloadCenterDetails, `
|
||||||
Get-SurfaceSystemSkuListForMicrosoftDriver
|
Get-SurfaceSystemSkuListForMicrosoftDriver
|
||||||
@@ -161,6 +161,7 @@ function Invoke-ParallelProcessing {
|
|||||||
ApplicationItemData = $currentItem
|
ApplicationItemData = $currentItem
|
||||||
AppListJsonPath = $localJobArgs['AppListJsonPath']
|
AppListJsonPath = $localJobArgs['AppListJsonPath']
|
||||||
AppsPath = $localJobArgs['AppsPath']
|
AppsPath = $localJobArgs['AppsPath']
|
||||||
|
UserAppListPath = $localJobArgs['UserAppListPath']
|
||||||
OrchestrationPath = $localJobArgs['OrchestrationPath']
|
OrchestrationPath = $localJobArgs['OrchestrationPath']
|
||||||
ProgressQueue = $localProgressQueue
|
ProgressQueue = $localProgressQueue
|
||||||
WindowsArch = $localJobArgs['WindowsArch']
|
WindowsArch = $localJobArgs['WindowsArch']
|
||||||
|
|||||||
@@ -358,6 +358,8 @@ function Start-WingetAppDownloadTask {
|
|||||||
[string]$AppListJsonPath,
|
[string]$AppListJsonPath,
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$AppsPath,
|
[string]$AppsPath,
|
||||||
|
[Parameter()]
|
||||||
|
[string]$UserAppListPath,
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$OrchestrationPath,
|
[string]$OrchestrationPath,
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
@@ -379,11 +381,11 @@ function Start-WingetAppDownloadTask {
|
|||||||
WriteLog "Starting download task for $($appName) with ID $($appId) from source $($source)."
|
WriteLog "Starting download task for $($appName) with ID $($appId) from source $($source)."
|
||||||
|
|
||||||
try {
|
try {
|
||||||
# Define paths
|
# Resolve the BYO app list path so duplicate checks honor custom file names.
|
||||||
$userAppListPath = Join-Path -Path $AppsPath -ChildPath "UserAppList.json"
|
$userAppListPath = if (-not [string]::IsNullOrWhiteSpace($UserAppListPath)) { $UserAppListPath } else { Join-Path -Path $AppsPath -ChildPath "UserAppList.json" }
|
||||||
$appFound = $false
|
$appFound = $false
|
||||||
|
|
||||||
# 1. Check UserAppList.json and content
|
# 1. Check the configured BYO app list and content
|
||||||
if (Test-Path -Path $userAppListPath) {
|
if (Test-Path -Path $userAppListPath) {
|
||||||
try {
|
try {
|
||||||
$userAppListContent = Get-Content -Path $userAppListPath -Raw | ConvertFrom-Json
|
$userAppListContent = Get-Content -Path $userAppListPath -Raw | ConvertFrom-Json
|
||||||
@@ -724,6 +726,8 @@ function Get-Apps {
|
|||||||
[string]$AppList,
|
[string]$AppList,
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$AppsPath,
|
[string]$AppsPath,
|
||||||
|
[Parameter()]
|
||||||
|
[string]$UserAppListPath,
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$WindowsArch,
|
[string]$WindowsArch,
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
@@ -787,6 +791,7 @@ function Get-Apps {
|
|||||||
# CLI builds should create WinGetWin32Apps.json, so SkipWin32Json is false
|
# CLI builds should create WinGetWin32Apps.json, so SkipWin32Json is false
|
||||||
$taskArguments = @{
|
$taskArguments = @{
|
||||||
AppsPath = $AppsPath
|
AppsPath = $AppsPath
|
||||||
|
UserAppListPath = $UserAppListPath
|
||||||
AppListJsonPath = $AppList
|
AppListJsonPath = $AppList
|
||||||
OrchestrationPath = $OrchestrationPath
|
OrchestrationPath = $OrchestrationPath
|
||||||
WindowsArch = $WindowsArch
|
WindowsArch = $WindowsArch
|
||||||
|
|||||||
@@ -27,6 +27,22 @@ function Update-BYOAppsActionButtonsState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Function to resolve the configured BYO app list path
|
||||||
|
function Get-BYOApplicationListPath {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[psobject]$State
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fall back to the legacy default path when the textbox is empty.
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($State.Controls.txtUserAppListPath.Text)) {
|
||||||
|
return $State.Controls.txtUserAppListPath.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Join-Path -Path $State.Controls.txtApplicationPath.Text -ChildPath 'UserAppList.json')
|
||||||
|
}
|
||||||
|
|
||||||
# Function to remove all selected BYO applications
|
# Function to remove all selected BYO applications
|
||||||
function Remove-SelectedBYOApplications {
|
function Remove-SelectedBYOApplications {
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
@@ -76,10 +92,10 @@ function Remove-SelectedBYOApplications {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Ask user if they want to save the changes
|
# Ask user if they want to save the changes
|
||||||
$result = [System.Windows.MessageBox]::Show("The selected applications have been removed from the list. Do you want to save these changes to UserAppList.json now?", "Save Changes", [System.Windows.MessageBoxButton]::YesNo, [System.Windows.MessageBoxImage]::Question)
|
$result = [System.Windows.MessageBox]::Show("The selected applications have been removed from the list. Do you want to save these changes to the configured BYO app list now?", "Save Changes", [System.Windows.MessageBoxButton]::YesNo, [System.Windows.MessageBoxImage]::Question)
|
||||||
|
|
||||||
if ($result -eq 'Yes') {
|
if ($result -eq 'Yes') {
|
||||||
$userAppListPath = Join-Path -Path $State.Controls.txtApplicationPath.Text -ChildPath 'UserAppList.json'
|
$userAppListPath = Get-BYOApplicationListPath -State $State
|
||||||
Save-BYOApplicationList -Path $userAppListPath -State $State
|
Save-BYOApplicationList -Path $userAppListPath -State $State
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,6 +182,7 @@ function Add-BYOApplication {
|
|||||||
|
|
||||||
# Refresh the ListView to show the changes
|
# Refresh the ListView to show the changes
|
||||||
$listView.Items.Refresh()
|
$listView.Items.Refresh()
|
||||||
|
Request-ListViewColumnAutoResize -ListView $listView
|
||||||
|
|
||||||
# Reset state
|
# Reset state
|
||||||
$State.Data.editingBYOApplication = $null
|
$State.Data.editingBYOApplication = $null
|
||||||
@@ -196,6 +213,7 @@ function Add-BYOApplication {
|
|||||||
CopyStatus = ""
|
CopyStatus = ""
|
||||||
}
|
}
|
||||||
$listView.Items.Add($application)
|
$listView.Items.Add($application)
|
||||||
|
Request-ListViewColumnAutoResize -ListView $listView
|
||||||
}
|
}
|
||||||
|
|
||||||
# Clear form and update button states for both add and update operations
|
# Clear form and update button states for both add and update operations
|
||||||
@@ -269,6 +287,7 @@ function Add-AppsScriptVariable {
|
|||||||
}
|
}
|
||||||
$State.Data.appsScriptVariablesDataList.Add($newItem)
|
$State.Data.appsScriptVariablesDataList.Add($newItem)
|
||||||
$State.Controls.lstAppsScriptVariables.ItemsSource = $State.Data.appsScriptVariablesDataList.ToArray()
|
$State.Controls.lstAppsScriptVariables.ItemsSource = $State.Data.appsScriptVariablesDataList.ToArray()
|
||||||
|
Request-ListViewColumnAutoResize -ListView $State.Controls.lstAppsScriptVariables
|
||||||
$State.Controls.txtAppsScriptKey.Clear()
|
$State.Controls.txtAppsScriptKey.Clear()
|
||||||
$State.Controls.txtAppsScriptValue.Clear()
|
$State.Controls.txtAppsScriptValue.Clear()
|
||||||
# Update the header checkbox state
|
# Update the header checkbox state
|
||||||
@@ -295,6 +314,7 @@ function Remove-SelectedAppsScriptVariable {
|
|||||||
$State.Data.appsScriptVariablesDataList.Remove($itemToRemove)
|
$State.Data.appsScriptVariablesDataList.Remove($itemToRemove)
|
||||||
}
|
}
|
||||||
$State.Controls.lstAppsScriptVariables.ItemsSource = $State.Data.appsScriptVariablesDataList.ToArray()
|
$State.Controls.lstAppsScriptVariables.ItemsSource = $State.Data.appsScriptVariablesDataList.ToArray()
|
||||||
|
Request-ListViewColumnAutoResize -ListView $State.Controls.lstAppsScriptVariables
|
||||||
|
|
||||||
# Update the header checkbox state
|
# Update the header checkbox state
|
||||||
if ($null -ne $State.Controls.chkSelectAllAppsScriptVariables) {
|
if ($null -ne $State.Controls.chkSelectAllAppsScriptVariables) {
|
||||||
@@ -391,18 +411,24 @@ function Invoke-CopyBYOApps {
|
|||||||
)
|
)
|
||||||
|
|
||||||
$localAppsPath = $State.Controls.txtApplicationPath.Text
|
$localAppsPath = $State.Controls.txtApplicationPath.Text
|
||||||
$userAppListPath = Join-Path -Path $localAppsPath -ChildPath 'UserAppList.json'
|
$userAppListPath = Get-BYOApplicationListPath -State $State
|
||||||
$listView = $State.Controls.lstApplications
|
$listView = $State.Controls.lstApplications
|
||||||
|
|
||||||
try {
|
try {
|
||||||
# Ensure items are sorted by current priority before saving
|
# Ensure the configured BYO app list folder exists before writing the manifest.
|
||||||
# Exclude CopyStatus when saving and ensure Priority is an integer; include AdditionalExitCodes and IgnoreNonZeroExitCodes for parity with Save-BYOApplicationList
|
$userAppListDirectory = Split-Path -Path $userAppListPath -Parent
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($userAppListDirectory) -and -not (Test-Path -Path $userAppListDirectory -PathType Container)) {
|
||||||
|
New-Item -Path $userAppListDirectory -ItemType Directory -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure items are sorted by current priority before saving.
|
||||||
|
# Exclude CopyStatus when saving and ensure Priority is an integer; include AdditionalExitCodes and IgnoreNonZeroExitCodes for parity with Save-BYOApplicationList.
|
||||||
$applications = $listView.Items | Sort-Object Priority | Select-Object @{N = 'Priority'; E = { [int]$_.Priority } }, Name, CommandLine, Arguments, Source, AdditionalExitCodes, IgnoreNonZeroExitCodes
|
$applications = $listView.Items | Sort-Object Priority | Select-Object @{N = 'Priority'; E = { [int]$_.Priority } }, Name, CommandLine, Arguments, Source, AdditionalExitCodes, IgnoreNonZeroExitCodes
|
||||||
$applications | ConvertTo-Json -Depth 5 | Set-Content -Path $userAppListPath -Force -Encoding UTF8
|
$applications | ConvertTo-Json -Depth 5 | Set-Content -Path $userAppListPath -Force -Encoding UTF8
|
||||||
WriteLog "Successfully updated UserAppList.json with all applications from the UI."
|
WriteLog "Successfully updated BYO app list at $userAppListPath with all applications from the UI."
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
$errorMessage = "Failed to update UserAppList.json: $_"
|
$errorMessage = "Failed to update BYO app list at $($userAppListPath): $_"
|
||||||
WriteLog $errorMessage
|
WriteLog $errorMessage
|
||||||
[System.Windows.MessageBox]::Show($errorMessage, "Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
|
[System.Windows.MessageBox]::Show($errorMessage, "Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ function Get-UIConfig {
|
|||||||
else { $null }
|
else { $null }
|
||||||
BuildUSBDrive = $State.Controls.chkBuildUSBDriveEnable.IsChecked
|
BuildUSBDrive = $State.Controls.chkBuildUSBDriveEnable.IsChecked
|
||||||
CleanupAppsISO = $State.Controls.chkCleanupAppsISO.IsChecked
|
CleanupAppsISO = $State.Controls.chkCleanupAppsISO.IsChecked
|
||||||
CleanupCaptureISO = $State.Controls.chkCleanupCaptureISO.IsChecked
|
|
||||||
CleanupDeployISO = $State.Controls.chkCleanupDeployISO.IsChecked
|
CleanupDeployISO = $State.Controls.chkCleanupDeployISO.IsChecked
|
||||||
CleanupDrivers = $State.Controls.chkCleanupDrivers.IsChecked
|
CleanupDrivers = $State.Controls.chkCleanupDrivers.IsChecked
|
||||||
CompactOS = $State.Controls.chkCompactOS.IsChecked
|
CompactOS = $State.Controls.chkCompactOS.IsChecked
|
||||||
@@ -37,15 +36,23 @@ function Get-UIConfig {
|
|||||||
UseDriversAsPEDrivers = $State.Controls.chkUseDriversAsPEDrivers.IsChecked
|
UseDriversAsPEDrivers = $State.Controls.chkUseDriversAsPEDrivers.IsChecked
|
||||||
CopyPPKG = $State.Controls.chkCopyPPKG.IsChecked
|
CopyPPKG = $State.Controls.chkCopyPPKG.IsChecked
|
||||||
CopyUnattend = $State.Controls.chkCopyUnattend.IsChecked
|
CopyUnattend = $State.Controls.chkCopyUnattend.IsChecked
|
||||||
|
DeviceNamingMode = Get-ConfiguredDeviceNamingMode -State $State
|
||||||
|
DeviceNameTemplate = $State.Controls.txtDeviceNameTemplate.Text
|
||||||
|
DeviceNamePrefixesPath = $State.Controls.txtDeviceNamePrefixesPath.Text
|
||||||
|
DeviceNamePrefixes = @(Get-DeviceNamePrefixes -State $State)
|
||||||
|
DeviceNameSerialComputerNamesPath = $State.Controls.txtDeviceNameSerialComputerNamesPath.Text
|
||||||
|
DeviceNameSerialComputerNames = @(Get-SerialComputerNamesLines -State $State)
|
||||||
CopyAdditionalFFUFiles = $State.Controls.chkCopyAdditionalFFUFiles.IsChecked
|
CopyAdditionalFFUFiles = $State.Controls.chkCopyAdditionalFFUFiles.IsChecked
|
||||||
CreateCaptureMedia = $State.Controls.chkCreateCaptureMedia.IsChecked
|
|
||||||
CreateDeploymentMedia = $State.Controls.chkCreateDeploymentMedia.IsChecked
|
CreateDeploymentMedia = $State.Controls.chkCreateDeploymentMedia.IsChecked
|
||||||
InjectUnattend = $State.Controls.chkInjectUnattend.IsChecked
|
InjectUnattend = $State.Controls.chkInjectUnattend.IsChecked
|
||||||
|
UnattendX64FilePath = $State.Controls.txtUnattendX64FilePath.Text
|
||||||
|
UnattendArm64FilePath = $State.Controls.txtUnattendArm64FilePath.Text
|
||||||
CustomFFUNameTemplate = $State.Controls.txtCustomFFUNameTemplate.Text
|
CustomFFUNameTemplate = $State.Controls.txtCustomFFUNameTemplate.Text
|
||||||
Disksize = [int64]$State.Controls.txtDiskSize.Text * 1GB
|
Disksize = [int64]$State.Controls.txtDiskSize.Text * 1GB
|
||||||
DownloadDrivers = $State.Controls.chkDownloadDrivers.IsChecked
|
DownloadDrivers = $State.Controls.chkDownloadDrivers.IsChecked
|
||||||
DriversFolder = $State.Controls.txtDriversFolder.Text
|
DriversFolder = $State.Controls.txtDriversFolder.Text
|
||||||
DriversJsonPath = $State.Controls.txtDriversJsonPath.Text
|
DriversJsonPath = $State.Controls.txtDriversJsonPath.Text
|
||||||
|
EnableVMNetworking = $State.Controls.chkEnableVMNetworking.IsChecked
|
||||||
FFUCaptureLocation = $State.Controls.txtFFUCaptureLocation.Text
|
FFUCaptureLocation = $State.Controls.txtFFUCaptureLocation.Text
|
||||||
FFUDevelopmentPath = $State.Controls.txtFFUDevPath.Text
|
FFUDevelopmentPath = $State.Controls.txtFFUDevPath.Text
|
||||||
FFUPrefix = $State.Controls.txtVMNamePrefix.Text
|
FFUPrefix = $State.Controls.txtVMNamePrefix.Text
|
||||||
@@ -54,6 +61,7 @@ function Get-UIConfig {
|
|||||||
InstallOffice = $State.Controls.chkInstallOffice.IsChecked
|
InstallOffice = $State.Controls.chkInstallOffice.IsChecked
|
||||||
InstallWingetApps = $State.Controls.chkInstallWingetApps.IsChecked
|
InstallWingetApps = $State.Controls.chkInstallWingetApps.IsChecked
|
||||||
ISOPath = $State.Controls.txtISOPath.Text
|
ISOPath = $State.Controls.txtISOPath.Text
|
||||||
|
WindowsMediaSource = if ($null -ne $State.Controls.rbProvideISO -and $State.Controls.rbProvideISO.IsChecked) { "Provide Windows ISO" } else { "Download Windows ESD" }
|
||||||
LogicalSectorSizeBytes = [int]$State.Controls.cmbLogicalSectorSize.SelectedItem.Content
|
LogicalSectorSizeBytes = [int]$State.Controls.cmbLogicalSectorSize.SelectedItem.Content
|
||||||
# Make = $null
|
# Make = $null
|
||||||
MediaType = $State.Controls.cmbMediaType.SelectedItem
|
MediaType = $State.Controls.cmbMediaType.SelectedItem
|
||||||
@@ -83,7 +91,6 @@ function Get-UIConfig {
|
|||||||
RemoveFFU = $State.Controls.chkRemoveFFU.IsChecked
|
RemoveFFU = $State.Controls.chkRemoveFFU.IsChecked
|
||||||
RemoveUpdates = $State.Controls.chkRemoveUpdates.IsChecked
|
RemoveUpdates = $State.Controls.chkRemoveUpdates.IsChecked
|
||||||
RemoveDownloadedESD = $State.Controls.chkRemoveDownloadedESD.IsChecked
|
RemoveDownloadedESD = $State.Controls.chkRemoveDownloadedESD.IsChecked
|
||||||
ShareName = $State.Controls.txtShareName.Text
|
|
||||||
UpdateADK = $State.Controls.chkUpdateADK.IsChecked
|
UpdateADK = $State.Controls.chkUpdateADK.IsChecked
|
||||||
UpdateEdge = $State.Controls.chkUpdateEdge.IsChecked
|
UpdateEdge = $State.Controls.chkUpdateEdge.IsChecked
|
||||||
UpdateLatestCU = $State.Controls.chkUpdateLatestCU.IsChecked
|
UpdateLatestCU = $State.Controls.chkUpdateLatestCU.IsChecked
|
||||||
@@ -93,14 +100,13 @@ function Get-UIConfig {
|
|||||||
UpdateLatestNet = $State.Controls.chkUpdateLatestNet.IsChecked
|
UpdateLatestNet = $State.Controls.chkUpdateLatestNet.IsChecked
|
||||||
UpdateOneDrive = $State.Controls.chkUpdateOneDrive.IsChecked
|
UpdateOneDrive = $State.Controls.chkUpdateOneDrive.IsChecked
|
||||||
UpdatePreviewCU = $State.Controls.chkUpdatePreviewCU.IsChecked
|
UpdatePreviewCU = $State.Controls.chkUpdatePreviewCU.IsChecked
|
||||||
UserAppListPath = "$($State.Controls.txtApplicationPath.Text)\UserAppList.json"
|
UserAppListPath = $State.Controls.txtUserAppListPath.Text
|
||||||
USBDriveList = @{}
|
USBDriveList = @{}
|
||||||
Username = $State.Controls.txtUsername.Text
|
|
||||||
Threads = [int]$State.Controls.txtThreads.Text
|
Threads = [int]$State.Controls.txtThreads.Text
|
||||||
BitsPriority = $State.Controls.cmbBitsPriority.SelectedItem
|
BitsPriority = $State.Controls.cmbBitsPriority.SelectedItem
|
||||||
MaxUSBDrives = [int]$State.Controls.txtMaxUSBDrives.Text
|
MaxUSBDrives = [int]$State.Controls.txtMaxUSBDrives.Text
|
||||||
|
ThemeMode = if ($null -ne $State.Controls.cmbThemeMode -and $null -ne $State.Controls.cmbThemeMode.SelectedItem) { $State.Controls.cmbThemeMode.SelectedItem } else { "System" }
|
||||||
Verbose = $State.Controls.chkVerbose.IsChecked
|
Verbose = $State.Controls.chkVerbose.IsChecked
|
||||||
VMHostIPAddress = $State.Controls.txtVMHostIPAddress.Text
|
|
||||||
VMLocation = $State.Controls.txtVMLocation.Text
|
VMLocation = $State.Controls.txtVMLocation.Text
|
||||||
VMSwitchName = if ($State.Controls.cmbVMSwitchName.SelectedItem -eq 'Other') {
|
VMSwitchName = if ($State.Controls.cmbVMSwitchName.SelectedItem -eq 'Other') {
|
||||||
$State.Controls.txtCustomVMSwitchName.Text
|
$State.Controls.txtCustomVMSwitchName.Text
|
||||||
@@ -412,7 +418,6 @@ function Select-VMSwitchFromConfig {
|
|||||||
$State.Controls.txtCustomVMSwitchName.Visibility = 'Visible'
|
$State.Controls.txtCustomVMSwitchName.Visibility = 'Visible'
|
||||||
$State.Controls.txtCustomVMSwitchName.Text = $configSwitch
|
$State.Controls.txtCustomVMSwitchName.Text = $configSwitch
|
||||||
$State.Data.customVMSwitchName = $configSwitch
|
$State.Data.customVMSwitchName = $configSwitch
|
||||||
$State.Data.customVMHostIP = $ConfigContent.VMHostIPAddress
|
|
||||||
WriteLog "LoadConfig: VMSwitchName '$configSwitch' not found. Selected 'Other' and populated custom VM Switch Name textbox."
|
WriteLog "LoadConfig: VMSwitchName '$configSwitch' not found. Selected 'Other' and populated custom VM Switch Name textbox."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -427,12 +432,19 @@ function Update-UIFromConfig {
|
|||||||
|
|
||||||
WriteLog "Applying loaded configuration to the UI."
|
WriteLog "Applying loaded configuration to the UI."
|
||||||
|
|
||||||
|
# Apply theme mode from config (must be done before other controls load for proper styling)
|
||||||
|
if ($null -ne $ConfigContent.PSObject.Properties.Item('ThemeMode') -and $State.Flags.isFluentSupported) {
|
||||||
|
$configTheme = $ConfigContent.ThemeMode
|
||||||
|
if ($configTheme -in @("Light", "Dark", "System")) {
|
||||||
|
Initialize-FluentTheme -Window $State.Window -ThemeMode $configTheme -State $State
|
||||||
|
Set-UIValue -ControlName 'cmbThemeMode' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'ThemeMode' -State $State
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Update Build tab values
|
# Update Build tab values
|
||||||
Set-UIValue -ControlName 'txtFFUDevPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'FFUDevelopmentPath' -State $State
|
Set-UIValue -ControlName 'txtFFUDevPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'FFUDevelopmentPath' -State $State
|
||||||
Set-UIValue -ControlName 'txtCustomFFUNameTemplate' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'CustomFFUNameTemplate' -State $State
|
Set-UIValue -ControlName 'txtCustomFFUNameTemplate' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'CustomFFUNameTemplate' -State $State
|
||||||
Set-UIValue -ControlName 'txtFFUCaptureLocation' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'FFUCaptureLocation' -State $State
|
Set-UIValue -ControlName 'txtFFUCaptureLocation' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'FFUCaptureLocation' -State $State
|
||||||
Set-UIValue -ControlName 'txtShareName' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'ShareName' -State $State
|
|
||||||
Set-UIValue -ControlName 'txtUsername' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Username' -State $State
|
|
||||||
Set-UIValue -ControlName 'txtThreads' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Threads' -State $State
|
Set-UIValue -ControlName 'txtThreads' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Threads' -State $State
|
||||||
Set-UIValue -ControlName 'cmbBitsPriority' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'BitsPriority' -State $State
|
Set-UIValue -ControlName 'cmbBitsPriority' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'BitsPriority' -State $State
|
||||||
Set-UIValue -ControlName 'txtMaxUSBDrives' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'MaxUSBDrives' -State $State
|
Set-UIValue -ControlName 'txtMaxUSBDrives' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'MaxUSBDrives' -State $State
|
||||||
@@ -444,19 +456,58 @@ function Update-UIFromConfig {
|
|||||||
Set-UIValue -ControlName 'chkAllowExternalHardDiskMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'AllowExternalHardDiskMedia' -State $State
|
Set-UIValue -ControlName 'chkAllowExternalHardDiskMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'AllowExternalHardDiskMedia' -State $State
|
||||||
Set-UIValue -ControlName 'chkPromptExternalHardDiskMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'PromptExternalHardDiskMedia' -State $State
|
Set-UIValue -ControlName 'chkPromptExternalHardDiskMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'PromptExternalHardDiskMedia' -State $State
|
||||||
Set-UIValue -ControlName 'chkCopyAdditionalFFUFiles' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyAdditionalFFUFiles' -State $State
|
Set-UIValue -ControlName 'chkCopyAdditionalFFUFiles' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyAdditionalFFUFiles' -State $State
|
||||||
Set-UIValue -ControlName 'chkCreateCaptureMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CreateCaptureMedia' -State $State
|
|
||||||
Set-UIValue -ControlName 'chkCreateDeploymentMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CreateDeploymentMedia' -State $State
|
Set-UIValue -ControlName 'chkCreateDeploymentMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CreateDeploymentMedia' -State $State
|
||||||
Set-UIValue -ControlName 'chkInjectUnattend' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'InjectUnattend' -State $State
|
Set-UIValue -ControlName 'chkInjectUnattend' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'InjectUnattend' -State $State
|
||||||
|
Set-UIValue -ControlName 'txtUnattendX64FilePath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'UnattendX64FilePath' -State $State
|
||||||
|
Set-UIValue -ControlName 'txtUnattendArm64FilePath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'UnattendArm64FilePath' -State $State
|
||||||
Set-UIValue -ControlName 'chkVerbose' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'Verbose' -State $State
|
Set-UIValue -ControlName 'chkVerbose' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'Verbose' -State $State
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($State.Controls.txtUnattendX64FilePath.Text)) {
|
||||||
|
$State.Controls.txtUnattendX64FilePath.Text = Get-DefaultUnattendFilePath -FFUDevelopmentPath $State.Controls.txtFFUDevPath.Text -WindowsArch 'x64'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($State.Controls.txtUnattendArm64FilePath.Text)) {
|
||||||
|
$State.Controls.txtUnattendArm64FilePath.Text = Get-DefaultUnattendFilePath -FFUDevelopmentPath $State.Controls.txtFFUDevPath.Text -WindowsArch 'arm64'
|
||||||
|
}
|
||||||
|
|
||||||
# USB Drive Modification group (Build Tab)
|
# USB Drive Modification group (Build Tab)
|
||||||
Set-UIValue -ControlName 'chkCopyAutopilot' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyAutopilot' -State $State
|
Set-UIValue -ControlName 'chkCopyAutopilot' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyAutopilot' -State $State
|
||||||
Set-UIValue -ControlName 'chkCopyUnattend' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyUnattend' -State $State
|
Set-UIValue -ControlName 'chkCopyUnattend' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyUnattend' -State $State
|
||||||
Set-UIValue -ControlName 'chkCopyPPKG' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyPPKG' -State $State
|
Set-UIValue -ControlName 'chkCopyPPKG' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyPPKG' -State $State
|
||||||
|
Set-UIValue -ControlName 'txtDeviceNamePrefixesPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DeviceNamePrefixesPath' -State $State
|
||||||
|
Set-UIValue -ControlName 'txtDeviceNameSerialComputerNamesPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DeviceNameSerialComputerNamesPath' -State $State
|
||||||
|
Set-UIValue -ControlName 'txtDeviceNameTemplate' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DeviceNameTemplate' -State $State
|
||||||
|
Set-UIValue -ControlName 'txtDeviceNamePrefixes' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DeviceNamePrefixes' -TransformValue { param($val) if ($val -is [System.Array]) { $val -join [System.Environment]::NewLine } else { [string]$val } } -State $State
|
||||||
|
Set-UIValue -ControlName 'txtDeviceNameSerialComputerNames' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DeviceNameSerialComputerNames' -TransformValue { param($val) if ($val -is [System.Array]) { $val -join [System.Environment]::NewLine } else { [string]$val } } -State $State
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($State.Controls.txtDeviceNamePrefixesPath.Text)) {
|
||||||
|
$State.Controls.txtDeviceNamePrefixesPath.Text = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $State.Controls.txtFFUDevPath.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($State.Controls.txtDeviceNameSerialComputerNamesPath.Text)) {
|
||||||
|
$State.Controls.txtDeviceNameSerialComputerNamesPath.Text = Get-DefaultSerialComputerNamesPath -FFUDevelopmentPath $State.Controls.txtFFUDevPath.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
$loadedDeviceNamingMode = $null
|
||||||
|
if ($ConfigContent.PSObject.Properties.Name -contains 'DeviceNamingMode') {
|
||||||
|
$candidateDeviceNamingMode = [string]$ConfigContent.DeviceNamingMode
|
||||||
|
if ($candidateDeviceNamingMode -in @('Legacy', 'None', 'Prompt', 'Template', 'Prefixes', 'SerialComputerNames')) {
|
||||||
|
$loadedDeviceNamingMode = $candidateDeviceNamingMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$displayDeviceNamingMode = if ($loadedDeviceNamingMode -in @('Prompt', 'Template', 'Prefixes', 'SerialComputerNames')) {
|
||||||
|
$loadedDeviceNamingMode
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
'None'
|
||||||
|
}
|
||||||
|
Set-DeviceNamingModeState -State $State -DisplayMode $displayDeviceNamingMode -LoadedMode $loadedDeviceNamingMode
|
||||||
|
Import-DeviceNamePrefixesFromConfiguredPath -State $State
|
||||||
|
Import-SerialComputerNamesFromConfiguredPath -State $State
|
||||||
|
Update-DeviceNamingControls -State $State
|
||||||
|
|
||||||
# Post Build Cleanup group (Build Tab)
|
# Post Build Cleanup group (Build Tab)
|
||||||
Set-UIValue -ControlName 'chkCleanupAppsISO' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CleanupAppsISO' -State $State
|
Set-UIValue -ControlName 'chkCleanupAppsISO' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CleanupAppsISO' -State $State
|
||||||
Set-UIValue -ControlName 'chkCleanupCaptureISO' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CleanupCaptureISO' -State $State
|
|
||||||
Set-UIValue -ControlName 'chkCleanupDeployISO' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CleanupDeployISO' -State $State
|
Set-UIValue -ControlName 'chkCleanupDeployISO' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CleanupDeployISO' -State $State
|
||||||
Set-UIValue -ControlName 'chkCleanupDrivers' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CleanupDrivers' -State $State
|
Set-UIValue -ControlName 'chkCleanupDrivers' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CleanupDrivers' -State $State
|
||||||
Set-UIValue -ControlName 'chkRemoveFFU' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'RemoveFFU' -State $State
|
Set-UIValue -ControlName 'chkRemoveFFU' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'RemoveFFU' -State $State
|
||||||
@@ -465,17 +516,30 @@ function Update-UIFromConfig {
|
|||||||
Set-UIValue -ControlName 'chkRemoveDownloadedESD' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'RemoveDownloadedESD' -State $State
|
Set-UIValue -ControlName 'chkRemoveDownloadedESD' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'RemoveDownloadedESD' -State $State
|
||||||
|
|
||||||
# Hyper-V Settings
|
# Hyper-V Settings
|
||||||
|
Set-UIValue -ControlName 'chkEnableVMNetworking' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'EnableVMNetworking' -State $State
|
||||||
Select-VMSwitchFromConfig -State $State -ConfigContent $ConfigContent
|
Select-VMSwitchFromConfig -State $State -ConfigContent $ConfigContent
|
||||||
Set-UIValue -ControlName 'txtVMHostIPAddress' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'VMHostIPAddress' -State $State
|
|
||||||
Set-UIValue -ControlName 'txtDiskSize' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Disksize' -TransformValue { param($val) $val / 1GB } -State $State
|
Set-UIValue -ControlName 'txtDiskSize' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Disksize' -TransformValue { param($val) $val / 1GB } -State $State
|
||||||
Set-UIValue -ControlName 'txtMemory' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Memory' -TransformValue { param($val) $val / 1GB } -State $State
|
Set-UIValue -ControlName 'txtMemory' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Memory' -TransformValue { param($val) $val / 1GB } -State $State
|
||||||
Set-UIValue -ControlName 'txtProcessors' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Processors' -State $State
|
Set-UIValue -ControlName 'txtProcessors' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Processors' -State $State
|
||||||
Set-UIValue -ControlName 'txtVMLocation' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'VMLocation' -State $State
|
Set-UIValue -ControlName 'txtVMLocation' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'VMLocation' -State $State
|
||||||
Set-UIValue -ControlName 'txtVMNamePrefix' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'FFUPrefix' -State $State
|
Set-UIValue -ControlName 'txtVMNamePrefix' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'FFUPrefix' -State $State
|
||||||
Set-UIValue -ControlName 'cmbLogicalSectorSize' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'LogicalSectorSizeBytes' -TransformValue { param($val) $val.ToString() } -State $State
|
Set-UIValue -ControlName 'cmbLogicalSectorSize' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'LogicalSectorSizeBytes' -TransformValue { param($val) $val.ToString() } -State $State
|
||||||
|
$State.Controls.spVMNetworkingSettings.IsEnabled = $true -eq $State.Controls.chkEnableVMNetworking.IsChecked
|
||||||
|
if (-not ($true -eq $State.Controls.chkEnableVMNetworking.IsChecked)) {
|
||||||
|
$State.Controls.txtCustomVMSwitchName.Visibility = 'Collapsed'
|
||||||
|
}
|
||||||
|
|
||||||
# Windows Settings
|
# Windows Settings
|
||||||
Set-UIValue -ControlName 'txtISOPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'ISOPath' -State $State
|
Set-UIValue -ControlName 'txtISOPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'ISOPath' -State $State
|
||||||
|
# Load Windows Media Source setting
|
||||||
|
if ($null -ne $ConfigContent.PSObject.Properties.Item('WindowsMediaSource')) {
|
||||||
|
if ($ConfigContent.WindowsMediaSource -eq 'Provide Windows ISO') {
|
||||||
|
$State.Controls.rbProvideISO.IsChecked = $true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$State.Controls.rbDownloadESD.IsChecked = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Special handling for Windows Release and SKU due to value collision (e.g., 2019 for Server and LTSC)
|
# Special handling for Windows Release and SKU due to value collision (e.g., 2019 for Server and LTSC)
|
||||||
if (($null -ne $ConfigContent.PSObject.Properties.Item('WindowsRelease')) -and ($null -ne $ConfigContent.PSObject.Properties.Item('WindowsSKU'))) {
|
if (($null -ne $ConfigContent.PSObject.Properties.Item('WindowsRelease')) -and ($null -ne $ConfigContent.PSObject.Properties.Item('WindowsSKU'))) {
|
||||||
@@ -585,6 +649,7 @@ function Update-UIFromConfig {
|
|||||||
Set-UIValue -ControlName 'chkBringYourOwnApps' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'BringYourOwnApps' -State $State
|
Set-UIValue -ControlName 'chkBringYourOwnApps' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'BringYourOwnApps' -State $State
|
||||||
Set-UIValue -ControlName 'txtApplicationPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'AppsPath' -State $State
|
Set-UIValue -ControlName 'txtApplicationPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'AppsPath' -State $State
|
||||||
Set-UIValue -ControlName 'txtAppListJsonPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'AppListPath' -State $State
|
Set-UIValue -ControlName 'txtAppListJsonPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'AppListPath' -State $State
|
||||||
|
Set-UIValue -ControlName 'txtUserAppListPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'UserAppListPath' -State $State
|
||||||
|
|
||||||
# Handle AppsScriptVariables
|
# Handle AppsScriptVariables
|
||||||
$appsScriptVarsKeyExists = $false
|
$appsScriptVarsKeyExists = $false
|
||||||
@@ -648,6 +713,7 @@ function Update-UIFromConfig {
|
|||||||
}
|
}
|
||||||
# Update the ListView's ItemsSource after populating the data list
|
# Update the ListView's ItemsSource after populating the data list
|
||||||
$lstAppsScriptVars.ItemsSource = $State.Data.appsScriptVariablesDataList.ToArray()
|
$lstAppsScriptVars.ItemsSource = $State.Data.appsScriptVariablesDataList.ToArray()
|
||||||
|
Request-ListViewColumnAutoResize -ListView $lstAppsScriptVars
|
||||||
# Update the header checkbox state
|
# Update the header checkbox state
|
||||||
if ($null -ne $State.Controls.chkSelectAllAppsScriptVariables) {
|
if ($null -ne $State.Controls.chkSelectAllAppsScriptVariables) {
|
||||||
Update-SelectAllHeaderCheckBoxState -ListView $lstAppsScriptVars -HeaderCheckBox $State.Controls.chkSelectAllAppsScriptVariables
|
Update-SelectAllHeaderCheckBoxState -ListView $lstAppsScriptVars -HeaderCheckBox $State.Controls.chkSelectAllAppsScriptVariables
|
||||||
@@ -716,6 +782,7 @@ function Update-UIFromConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$State.Controls.lstUSBDrives.Items.Refresh()
|
$State.Controls.lstUSBDrives.Items.Refresh()
|
||||||
|
Request-ListViewColumnAutoResize -ListView $State.Controls.lstUSBDrives
|
||||||
|
|
||||||
# Update the Select All header checkbox state
|
# Update the Select All header checkbox state
|
||||||
$headerChk = $State.Controls.chkSelectAllUSBDrivesHeader
|
$headerChk = $State.Controls.chkSelectAllUSBDrivesHeader
|
||||||
@@ -776,6 +843,7 @@ function Update-UIFromConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$State.Controls.lstAdditionalFFUs.Items.Refresh()
|
$State.Controls.lstAdditionalFFUs.Items.Refresh()
|
||||||
|
Request-ListViewColumnAutoResize -ListView $State.Controls.lstAdditionalFFUs
|
||||||
$headerChk = $State.Controls.chkSelectAllAdditionalFFUs
|
$headerChk = $State.Controls.chkSelectAllAdditionalFFUs
|
||||||
if ($null -ne $headerChk) {
|
if ($null -ne $headerChk) {
|
||||||
Update-SelectAllHeaderCheckBoxState -ListView $State.Controls.lstAdditionalFFUs -HeaderCheckBox $headerChk
|
Update-SelectAllHeaderCheckBoxState -ListView $State.Controls.lstAdditionalFFUs -HeaderCheckBox $headerChk
|
||||||
@@ -835,7 +903,7 @@ function Invoke-RestoreDefaults {
|
|||||||
$rootPath = $State.FFUDevelopmentPath
|
$rootPath = $State.FFUDevelopmentPath
|
||||||
|
|
||||||
# Normalize potential array values to single strings
|
# Normalize potential array values to single strings
|
||||||
function Normalize-PathScalar {
|
function Get-PathScalar {
|
||||||
param([object]$value)
|
param([object]$value)
|
||||||
if ($null -eq $value) { return $null }
|
if ($null -eq $value) { return $null }
|
||||||
if ($value -is [System.Array]) {
|
if ($value -is [System.Array]) {
|
||||||
@@ -850,21 +918,20 @@ function Invoke-RestoreDefaults {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$appsPath = Join-Path $rootPath 'Apps'
|
$appsPath = Join-Path $rootPath 'Apps'
|
||||||
$driversRaw = Normalize-PathScalar -value $State.Controls.txtDriversFolder.Text
|
$driversRaw = Get-PathScalar -value $State.Controls.txtDriversFolder.Text
|
||||||
if ([string]::IsNullOrWhiteSpace($driversRaw)) {
|
if ([string]::IsNullOrWhiteSpace($driversRaw)) {
|
||||||
$driversPath = Join-Path $rootPath 'Drivers'
|
$driversPath = Join-Path $rootPath 'Drivers'
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$driversPath = $driversRaw
|
$driversPath = $driversRaw
|
||||||
}
|
}
|
||||||
$ffuCaptureRaw = Normalize-PathScalar -value $State.Controls.txtFFUCaptureLocation.Text
|
$ffuCaptureRaw = Get-PathScalar -value $State.Controls.txtFFUCaptureLocation.Text
|
||||||
$ffuCapturePath = if ([string]::IsNullOrWhiteSpace($ffuCaptureRaw)) { Join-Path $rootPath 'FFU' } else { $ffuCaptureRaw }
|
$ffuCapturePath = if ([string]::IsNullOrWhiteSpace($ffuCaptureRaw)) { Join-Path $rootPath 'FFU' } else { $ffuCaptureRaw }
|
||||||
|
|
||||||
$captureISOPath = Join-Path $rootPath 'WinPECaptureFFUFiles\WinPE-Capture.iso'
|
|
||||||
$deployISOPath = Join-Path $rootPath 'WinPEDeployFFUFiles\WinPE-Deploy.iso'
|
$deployISOPath = Join-Path $rootPath 'WinPEDeployFFUFiles\WinPE-Deploy.iso'
|
||||||
$appsISOPath = Join-Path $rootPath 'Apps.iso'
|
$appsISOPath = Join-Path $rootPath 'Apps.iso'
|
||||||
|
|
||||||
$msg = "Restore Defaults will:`n`n- Delete generated config and app/driver list JSON files`n- Remove ISO files (Capture, Deploy, Apps) if present`n- Remove Apps/Update/downloaded artifacts`n- Remove driver folder contents (not the folder)`n- Remove FFU files in the capture folder`n`nSample/template files and VM/VHDX cache are NOT removed.`n`nProceed?"
|
$msg = "Restore Defaults will:`n`n- Delete generated config and app/driver list JSON files`n- Remove ISO files (Deploy, Apps) if present`n- Remove Apps/Update/downloaded artifacts`n- Remove driver folder contents (not the folder)`n- Remove FFU files in the capture folder`n`nSample/template files and VM/VHDX cache are NOT removed.`n`nProceed?"
|
||||||
$result = [System.Windows.MessageBox]::Show($msg, "Confirm Restore Defaults", "YesNo", "Warning")
|
$result = [System.Windows.MessageBox]::Show($msg, "Confirm Restore Defaults", "YesNo", "Warning")
|
||||||
if ($result -ne [System.Windows.MessageBoxResult]::Yes) {
|
if ($result -ne [System.Windows.MessageBoxResult]::Yes) {
|
||||||
WriteLog "RestoreDefaults: User cancelled."
|
WriteLog "RestoreDefaults: User cancelled."
|
||||||
@@ -900,11 +967,9 @@ function Invoke-RestoreDefaults {
|
|||||||
-AppsPath $appsPath `
|
-AppsPath $appsPath `
|
||||||
-DriversPath $driversPath `
|
-DriversPath $driversPath `
|
||||||
-FFUCapturePath $ffuCapturePath `
|
-FFUCapturePath $ffuCapturePath `
|
||||||
-CaptureISOPath $captureISOPath `
|
|
||||||
-DeployISOPath $deployISOPath `
|
-DeployISOPath $deployISOPath `
|
||||||
-AppsISOPath $appsISOPath `
|
-AppsISOPath $appsISOPath `
|
||||||
-KBPath (Join-Path $rootPath 'KB') `
|
-KBPath (Join-Path $rootPath 'KB') `
|
||||||
-RemoveCaptureISO:$true `
|
|
||||||
-RemoveDeployISO:$true `
|
-RemoveDeployISO:$true `
|
||||||
-RemoveAppsISO:$true `
|
-RemoveAppsISO:$true `
|
||||||
-RemoveDrivers:$true `
|
-RemoveDrivers:$true `
|
||||||
@@ -1039,6 +1104,7 @@ function Import-ConfigSupplementalAssets {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
$State.Controls.lstWingetResults.ItemsSource = $appsBuffer.ToArray()
|
$State.Controls.lstWingetResults.ItemsSource = $appsBuffer.ToArray()
|
||||||
|
Request-ListViewColumnAutoResize -ListView $State.Controls.lstWingetResults
|
||||||
$loadedWinget = $true
|
$loadedWinget = $true
|
||||||
if ($null -ne $State.Controls.wingetSearchPanel) {
|
if ($null -ne $State.Controls.wingetSearchPanel) {
|
||||||
$State.Controls.wingetSearchPanel.Visibility = 'Visible'
|
$State.Controls.wingetSearchPanel.Visibility = 'Visible'
|
||||||
@@ -1173,6 +1239,7 @@ function Import-ConfigSupplementalAssets {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$State.Controls.lstDriverModels.ItemsSource = $State.Data.allDriverModels
|
$State.Controls.lstDriverModels.ItemsSource = $State.Data.allDriverModels
|
||||||
|
Request-ListViewColumnAutoResize -ListView $State.Controls.lstDriverModels
|
||||||
if (Get-Command -Name Update-SelectAllHeaderCheckBoxState -ErrorAction SilentlyContinue) {
|
if (Get-Command -Name Update-SelectAllHeaderCheckBoxState -ErrorAction SilentlyContinue) {
|
||||||
$headerChk = $State.Controls.chkSelectAllDriverModels
|
$headerChk = $State.Controls.chkSelectAllDriverModels
|
||||||
if ($null -ne $headerChk) {
|
if ($null -ne $headerChk) {
|
||||||
|
|||||||
@@ -23,10 +23,11 @@ function Get-LenovoDriversModelList {
|
|||||||
# If anyone knows how to reliably get the the model and machine type information from Lenovo, let me know.
|
# If anyone knows how to reliably get the the model and machine type information from Lenovo, let me know.
|
||||||
# https://download.lenovo.com/cdrt/td/catalogv2.xml only provides a subset of the information available from PSREF (e.g. it's missing 300w, 500w, and other consumer models).
|
# https://download.lenovo.com/cdrt/td/catalogv2.xml only provides a subset of the information available from PSREF (e.g. it's missing 300w, 500w, and other consumer models).
|
||||||
|
|
||||||
# $lenovoCookie = "X-PSREF-USER-TOKEN=eyJ0eXAiOiJKV1QifQ.bjVTdWk0YklZeUc2WnFzL0lXU0pTeU1JcFo0aExzRXl1UGxHN3lnS1BtckI0ZVU5WEJyVGkvaFE0NmVNU2U1ZjNrK3ZqTEVIZ29nTk1TNS9DQmIwQ0pTN1Q1VytlY1RpNzZTUldXbm4wZ1g2RGJuQWg4MXRkTmxKT2YrOW9LRjBzQUZzV05HM3NpcU92WFVTM0o0blM1SDQyUlVXNThIV1VBS2R0c1B2NjJyQjIrUGxNZ2x6RTRhUjY5UDZWclBX.ZDBmM2EyMWRjZTg2N2JmYWMxZDIxY2NiYjQzMWFhNjg1YjEzZTAxNmU2M2RmN2M5ZjIyZWJhMzZkOWI1OWJhZg"
|
$lenovoCookie = "X-PSREF-USER-TOKEN=eyJ0eXAiOiJKV1QifQ.bjVTdWk0YklZeUc2WnFzL0lXU0pTeU1JcFo0aExzRXl1UGxHN3lnS1BtckI0ZVU5WEJyVGkvaFE0NmVNU2U1ZjNrK3ZqTEVIZ29nTk1TNS9DQmIwQ0pTN1Q1VytlY1RpNzZTUldXbm4wZ1g2RGJuQWg4MXRkTmxKT2YrOW9LRjBzQUZzV05HM3NpcU92WFVTM0o0blM1SDQyUlVXNThIV1VBS2R0c1B2NjJyQjIrUGxNZ2x6RTRhUjY5UDZWclBX.ZDBmM2EyMWRjZTg2N2JmYWMxZDIxY2NiYjQzMWFhNjg1YjEzZTAxNmU2M2RmN2M5ZjIyZWJhMzZkOWI1OWJhZg"
|
||||||
|
|
||||||
# Wrote a separate function to grab the token. Check the function notes for more details. Keep the above comment for now to see if the cookie ever changes.
|
# Wrote a separate function to grab the token. Check the function notes for more details. Keep the above comment for now to see if the cookie ever changes.
|
||||||
$lenovoCookie = Get-LenovoPSREFToken
|
# 3/25/2026 - The cookie is still the same after 8 months, but we'll keep the retrieval function in case it changes in the future or if we need to get a new one.
|
||||||
|
# $lenovoCookie = Get-LenovoPSREFToken
|
||||||
|
|
||||||
# Add the cookie to the headers
|
# Add the cookie to the headers
|
||||||
$Headers["Cookie"] = $lenovoCookie
|
$Headers["Cookie"] = $lenovoCookie
|
||||||
|
|||||||
@@ -15,100 +15,8 @@ function Get-MicrosoftDriversModelList {
|
|||||||
[string]$DriversFolder
|
[string]$DriversFolder
|
||||||
)
|
)
|
||||||
|
|
||||||
$url = "https://support.microsoft.com/en-us/surface/download-drivers-and-firmware-for-surface-09bb2e09-2a4b-cb69-0951-078a7739e120"
|
# Keep the UI signature unchanged while using the shared Learn-based source.
|
||||||
$models = @()
|
return @(Get-SurfaceDriverModelIndex -DriversFolder $DriversFolder)
|
||||||
|
|
||||||
# Load cached model list first (Source B) to keep the UI fast.
|
|
||||||
# The cache is refreshed automatically when missing or invalid.
|
|
||||||
try {
|
|
||||||
$cachePath = Get-SurfaceDriverIndexCachePath -DriversFolder $DriversFolder
|
|
||||||
if (Test-Path -Path $cachePath -PathType Leaf) {
|
|
||||||
$cacheAgeDays = ((Get-Date) - (Get-Item -Path $cachePath).LastWriteTime).TotalDays
|
|
||||||
if ($cacheAgeDays -lt 7) {
|
|
||||||
$cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder
|
|
||||||
if ($cache.ModelIndex -and $cache.ModelIndex.Count -gt 0) {
|
|
||||||
WriteLog "Surface cache: Using cached Microsoft model list ($($cache.ModelIndex.Count) models)."
|
|
||||||
return @($cache.ModelIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
WriteLog "Surface cache: Failed to load cached Microsoft model list. Falling back to online parse. Error: $($_.Exception.Message)"
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
WriteLog "Getting Surface driver information from $url"
|
|
||||||
$OriginalVerbosePreference = $VerbosePreference
|
|
||||||
$VerbosePreference = 'SilentlyContinue'
|
|
||||||
# Use passed-in UserAgent and Headers
|
|
||||||
$webContent = Invoke-WebRequest -Uri $url -UseBasicParsing -Headers $Headers -UserAgent $UserAgent
|
|
||||||
$VerbosePreference = $OriginalVerbosePreference
|
|
||||||
WriteLog "Complete"
|
|
||||||
|
|
||||||
WriteLog "Parsing web content for models and download links"
|
|
||||||
$html = $webContent.Content
|
|
||||||
$divPattern = '<div[^>]*class="selectable-content-options__option-content(?: ocHidden)?"[^>]*>(.*?)</div>'
|
|
||||||
$divMatches = [regex]::Matches($html, $divPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
|
|
||||||
|
|
||||||
foreach ($divMatch in $divMatches) {
|
|
||||||
$divContent = $divMatch.Groups[1].Value
|
|
||||||
$tablePattern = '<table[^>]*>(.*?)</table>'
|
|
||||||
$tableMatches = [regex]::Matches($divContent, $tablePattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
|
|
||||||
|
|
||||||
foreach ($tableMatch in $tableMatches) {
|
|
||||||
$tableContent = $tableMatch.Groups[1].Value
|
|
||||||
$rowPattern = '<tr[^>]*>(.*?)</tr>'
|
|
||||||
$rowMatches = [regex]::Matches($tableContent, $rowPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
|
|
||||||
|
|
||||||
foreach ($rowMatch in $rowMatches) {
|
|
||||||
$rowContent = $rowMatch.Groups[1].Value
|
|
||||||
$cellPattern = '<td[^>]*>\s*(?:<p[^>]*>)?(.*?)(?:</p>)?\s*</td>'
|
|
||||||
$cellMatches = [regex]::Matches($rowContent, $cellPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
|
|
||||||
|
|
||||||
if ($cellMatches.Count -ge 2) {
|
|
||||||
$modelName = ([System.Net.WebUtility]::HtmlDecode(($cellMatches[0].Groups[1].Value).Trim()))
|
|
||||||
$secondTdContent = $cellMatches[1].Groups[1].Value.Trim()
|
|
||||||
# $linkPattern = '<a[^>]+href="([^"]+)"[^>]*>'
|
|
||||||
# Change linkPattern to match https://www.microsoft.com/download/details.aspx?id=
|
|
||||||
$linkPattern = '<a[^>]+href="(https://www\.microsoft\.com/download/details\.aspx\?id=\d+)"[^>]*>'
|
|
||||||
$linkMatch = [regex]::Match($secondTdContent, $linkPattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
|
|
||||||
|
|
||||||
if ($linkMatch.Success) {
|
|
||||||
$modelLink = $linkMatch.Groups[1].Value
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
$models += [PSCustomObject]@{
|
|
||||||
Make = 'Microsoft'
|
|
||||||
Model = $modelName
|
|
||||||
Link = $modelLink
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WriteLog "Parsing complete. Found $($models.Count) models."
|
|
||||||
|
|
||||||
# Persist model list (Source B) into the local cache for fast UI population.
|
|
||||||
try {
|
|
||||||
$cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder
|
|
||||||
$cache.ModelIndex = @($models)
|
|
||||||
Save-SurfaceDriverIndexCache -Cache $cache -DriversFolder $DriversFolder
|
|
||||||
WriteLog "Surface cache: Saved Microsoft model list to cache."
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
WriteLog "Surface cache: Failed to save Microsoft model list. Error: $($_.Exception.Message)"
|
|
||||||
}
|
|
||||||
|
|
||||||
return $models
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
WriteLog "Error getting Microsoft models: $($_.Exception.Message)"
|
|
||||||
throw "Failed to retrieve Microsoft Surface models."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
# Function to download and extract drivers for a specific Microsoft model (Modified for ForEach-Object -Parallel)
|
# Function to download and extract drivers for a specific Microsoft model (Modified for ForEach-Object -Parallel)
|
||||||
function Save-MicrosoftDriversTask {
|
function Save-MicrosoftDriversTask {
|
||||||
|
|||||||
@@ -373,6 +373,7 @@ function Search-DriverModels {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# The view will automatically refresh. No need to call .Refresh() explicitly for filtering.
|
# The view will automatically refresh. No need to call .Refresh() explicitly for filtering.
|
||||||
|
Request-ListViewColumnAutoResize -ListView $State.Controls.lstDriverModels
|
||||||
$filteredCount = 0
|
$filteredCount = 0
|
||||||
if ($null -ne $collectionView) {
|
if ($null -ne $collectionView) {
|
||||||
foreach ($item in $collectionView) { $filteredCount++ }
|
foreach ($item in $collectionView) { $filteredCount++ }
|
||||||
@@ -715,6 +716,7 @@ function Import-DriversJson {
|
|||||||
|
|
||||||
# Update the UI and apply any existing filter
|
# Update the UI and apply any existing filter
|
||||||
$State.Controls.lstDriverModels.ItemsSource = $State.Data.allDriverModels
|
$State.Controls.lstDriverModels.ItemsSource = $State.Data.allDriverModels
|
||||||
|
Request-ListViewColumnAutoResize -ListView $State.Controls.lstDriverModels
|
||||||
Search-DriverModels -filterText $State.Controls.txtModelFilter.Text -State $State
|
Search-DriverModels -filterText $State.Controls.txtModelFilter.Text -State $State
|
||||||
|
|
||||||
$message = "Driver import complete.`nNew models added: $newModelsAdded`nExisting models updated: $existingModelsUpdated"
|
$message = "Driver import complete.`nNew models added: $newModelsAdded`nExisting models updated: $existingModelsUpdated"
|
||||||
@@ -786,6 +788,7 @@ function Invoke-GetModels {
|
|||||||
|
|
||||||
# Update the UI ItemsSource to point to the new list and clear the filter
|
# Update the UI ItemsSource to point to the new list and clear the filter
|
||||||
$State.Controls.lstDriverModels.ItemsSource = $State.Data.allDriverModels
|
$State.Controls.lstDriverModels.ItemsSource = $State.Data.allDriverModels
|
||||||
|
Request-ListViewColumnAutoResize -ListView $State.Controls.lstDriverModels
|
||||||
$State.Controls.txtModelFilter.Text = ""
|
$State.Controls.txtModelFilter.Text = ""
|
||||||
|
|
||||||
if ($State.Data.allDriverModels.Count -gt 0) {
|
if ($State.Data.allDriverModels.Count -gt 0) {
|
||||||
|
|||||||
@@ -5,6 +5,341 @@
|
|||||||
This module is dedicated to managing user interactions within the FFU Builder UI. It contains the Register-EventHandlers function, which connects UI controls defined in the XAML to their corresponding actions in the PowerShell backend. This includes handling button clicks, text input validation, checkbox state changes, and list view interactions across all tabs, effectively wiring up the application's front-end to its core logic.
|
This module is dedicated to managing user interactions within the FFU Builder UI. It contains the Register-EventHandlers function, which connects UI controls defined in the XAML to their corresponding actions in the PowerShell backend. This includes handling button clicks, text input validation, checkbox state changes, and list view interactions across all tabs, effectively wiring up the application's front-end to its core logic.
|
||||||
#>
|
#>
|
||||||
|
|
||||||
|
function Update-VMNetworkingControls {
|
||||||
|
param([PSCustomObject]$State)
|
||||||
|
|
||||||
|
$isVmNetworkingEnabled = $true -eq $State.Controls.chkEnableVMNetworking.IsChecked
|
||||||
|
$State.Controls.spVMNetworkingSettings.IsEnabled = $isVmNetworkingEnabled
|
||||||
|
|
||||||
|
if (-not $isVmNetworkingEnabled) {
|
||||||
|
$State.Controls.txtCustomVMSwitchName.Visibility = 'Collapsed'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($State.Controls.cmbVMSwitchName.SelectedItem -eq 'Other') {
|
||||||
|
$State.Controls.txtCustomVMSwitchName.Visibility = 'Visible'
|
||||||
|
if ([string]::IsNullOrWhiteSpace($State.Controls.txtCustomVMSwitchName.Text) -and $null -ne $State.Data.customVMSwitchName) {
|
||||||
|
$State.Controls.txtCustomVMSwitchName.Text = $State.Data.customVMSwitchName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$State.Controls.txtCustomVMSwitchName.Visibility = 'Collapsed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SelectedDeviceNamingMode {
|
||||||
|
param([PSCustomObject]$State)
|
||||||
|
|
||||||
|
if ($true -eq $State.Controls.rbDeviceNamingPrompt.IsChecked) {
|
||||||
|
return 'Prompt'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($true -eq $State.Controls.rbDeviceNamingTemplate.IsChecked) {
|
||||||
|
return 'Template'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($true -eq $State.Controls.rbDeviceNamingPrefixes.IsChecked) {
|
||||||
|
return 'Prefixes'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($true -eq $State.Controls.rbDeviceNamingSerialComputerNames.IsChecked) {
|
||||||
|
return 'SerialComputerNames'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'None'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-DeviceNamingMode {
|
||||||
|
param(
|
||||||
|
[PSCustomObject]$State,
|
||||||
|
[ValidateSet('None', 'Prompt', 'Template', 'Prefixes', 'SerialComputerNames')]
|
||||||
|
[string]$Mode
|
||||||
|
)
|
||||||
|
|
||||||
|
$State.Controls.rbDeviceNamingNone.IsChecked = $Mode -eq 'None'
|
||||||
|
$State.Controls.rbDeviceNamingPrompt.IsChecked = $Mode -eq 'Prompt'
|
||||||
|
$State.Controls.rbDeviceNamingTemplate.IsChecked = $Mode -eq 'Template'
|
||||||
|
$State.Controls.rbDeviceNamingPrefixes.IsChecked = $Mode -eq 'Prefixes'
|
||||||
|
$State.Controls.rbDeviceNamingSerialComputerNames.IsChecked = $Mode -eq 'SerialComputerNames'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-DeviceNamingModeState {
|
||||||
|
param(
|
||||||
|
[PSCustomObject]$State,
|
||||||
|
[ValidateSet('None', 'Prompt', 'Template', 'Prefixes', 'SerialComputerNames')]
|
||||||
|
[string]$DisplayMode,
|
||||||
|
[AllowNull()]
|
||||||
|
[string]$LoadedMode
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $State.Flags) {
|
||||||
|
$State.Flags = @{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -eq $State.Data) {
|
||||||
|
$State.Data = @{}
|
||||||
|
}
|
||||||
|
|
||||||
|
$previousSuppressionState = $true -eq $State.Flags.suppressDeviceNamingChangeTracking
|
||||||
|
$State.Flags.suppressDeviceNamingChangeTracking = $true
|
||||||
|
try {
|
||||||
|
Set-DeviceNamingMode -State $State -Mode $DisplayMode
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$State.Flags.suppressDeviceNamingChangeTracking = $previousSuppressionState
|
||||||
|
}
|
||||||
|
|
||||||
|
$State.Data.loadedDeviceNamingMode = if ([string]::IsNullOrWhiteSpace($LoadedMode)) {
|
||||||
|
$null
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$LoadedMode.Trim()
|
||||||
|
}
|
||||||
|
$State.Flags.deviceNamingModeWasExplicitlyChanged = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ConfiguredDeviceNamingMode {
|
||||||
|
param([PSCustomObject]$State)
|
||||||
|
|
||||||
|
if (($null -ne $State.Flags) -and ($true -eq $State.Flags.deviceNamingModeWasExplicitlyChanged)) {
|
||||||
|
return Get-SelectedDeviceNamingMode -State $State
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($null -ne $State.Data) -and -not [string]::IsNullOrWhiteSpace([string]$State.Data.loadedDeviceNamingMode)) {
|
||||||
|
return [string]$State.Data.loadedDeviceNamingMode
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-DeviceNamePrefixes {
|
||||||
|
param([PSCustomObject]$State)
|
||||||
|
|
||||||
|
if ($null -eq $State.Controls.txtDeviceNamePrefixes) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
return @(
|
||||||
|
$State.Controls.txtDeviceNamePrefixes.Text -split "\r?\n" |
|
||||||
|
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
|
||||||
|
ForEach-Object { $_.Trim() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SerialComputerNamesLines {
|
||||||
|
param([PSCustomObject]$State)
|
||||||
|
|
||||||
|
if ($null -eq $State.Controls.txtDeviceNameSerialComputerNames) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
return @(
|
||||||
|
$State.Controls.txtDeviceNameSerialComputerNames.Text -split "\r?\n" |
|
||||||
|
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
|
||||||
|
ForEach-Object { $_.Trim() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Import-DeviceNamePrefixesFile {
|
||||||
|
param(
|
||||||
|
[PSCustomObject]$State,
|
||||||
|
[string]$FilePath
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($FilePath) -or -not (Test-Path -Path $FilePath -PathType Leaf)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$prefixLines = @(Get-Content -Path $FilePath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() })
|
||||||
|
if ($null -ne $State.Controls.txtDeviceNamePrefixesPath) {
|
||||||
|
$State.Controls.txtDeviceNamePrefixesPath.Text = $FilePath
|
||||||
|
}
|
||||||
|
$State.Controls.txtDeviceNamePrefixes.Text = $prefixLines -join [System.Environment]::NewLine
|
||||||
|
WriteLog "Imported device name prefixes from $FilePath"
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
function Import-SerialComputerNamesFile {
|
||||||
|
param(
|
||||||
|
[PSCustomObject]$State,
|
||||||
|
[string]$FilePath
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($FilePath) -or -not (Test-Path -Path $FilePath -PathType Leaf)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$serialMappingLines = @(Get-Content -Path $FilePath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() })
|
||||||
|
if ($null -ne $State.Controls.txtDeviceNameSerialComputerNamesPath) {
|
||||||
|
$State.Controls.txtDeviceNameSerialComputerNamesPath.Text = $FilePath
|
||||||
|
}
|
||||||
|
$State.Controls.txtDeviceNameSerialComputerNames.Text = $serialMappingLines -join [System.Environment]::NewLine
|
||||||
|
WriteLog "Imported serial computer-name mappings from $FilePath"
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-DefaultDeviceNamePrefixesPath {
|
||||||
|
param([string]$FFUDevelopmentPath)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($FFUDevelopmentPath)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
return Join-Path (Join-Path $FFUDevelopmentPath 'unattend') 'prefixes.txt'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-DefaultSerialComputerNamesPath {
|
||||||
|
param([string]$FFUDevelopmentPath)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($FFUDevelopmentPath)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
return Join-Path (Join-Path $FFUDevelopmentPath 'unattend') 'SerialComputerNames.csv'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-DefaultUnattendFilePath {
|
||||||
|
param(
|
||||||
|
[string]$FFUDevelopmentPath,
|
||||||
|
[ValidateSet('x64', 'arm64')]
|
||||||
|
[string]$WindowsArch
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($FFUDevelopmentPath)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileName = if ($WindowsArch -ieq 'arm64') { 'unattend_arm64.xml' } else { 'unattend_x64.xml' }
|
||||||
|
return Join-Path (Join-Path $FFUDevelopmentPath 'unattend') $fileName
|
||||||
|
}
|
||||||
|
|
||||||
|
function Import-DeviceNamePrefixesFromConfiguredPath {
|
||||||
|
param(
|
||||||
|
[PSCustomObject]$State,
|
||||||
|
[switch]$SkipIfTextPresent
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($SkipIfTextPresent -and -not [string]::IsNullOrWhiteSpace($State.Controls.txtDeviceNamePrefixes.Text)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$prefixFilePath = $State.Controls.txtDeviceNamePrefixesPath.Text
|
||||||
|
if ([string]::IsNullOrWhiteSpace($prefixFilePath)) {
|
||||||
|
$prefixFilePath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $State.Controls.txtFFUDevPath.Text
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($prefixFilePath) -and $null -ne $State.Controls.txtDeviceNamePrefixesPath) {
|
||||||
|
$State.Controls.txtDeviceNamePrefixesPath.Text = $prefixFilePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path -Path $prefixFilePath -PathType Leaf) {
|
||||||
|
Import-DeviceNamePrefixesFile -State $State -FilePath $prefixFilePath | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Import-SerialComputerNamesFromConfiguredPath {
|
||||||
|
param(
|
||||||
|
[PSCustomObject]$State,
|
||||||
|
[switch]$SkipIfTextPresent
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($SkipIfTextPresent -and -not [string]::IsNullOrWhiteSpace($State.Controls.txtDeviceNameSerialComputerNames.Text)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$serialComputerNamesPath = $State.Controls.txtDeviceNameSerialComputerNamesPath.Text
|
||||||
|
if ([string]::IsNullOrWhiteSpace($serialComputerNamesPath)) {
|
||||||
|
$serialComputerNamesPath = Get-DefaultSerialComputerNamesPath -FFUDevelopmentPath $State.Controls.txtFFUDevPath.Text
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($serialComputerNamesPath) -and $null -ne $State.Controls.txtDeviceNameSerialComputerNamesPath) {
|
||||||
|
$State.Controls.txtDeviceNameSerialComputerNamesPath.Text = $serialComputerNamesPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path -Path $serialComputerNamesPath -PathType Leaf) {
|
||||||
|
Import-SerialComputerNamesFile -State $State -FilePath $serialComputerNamesPath | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-DeviceNameTemplateUsesSerialToken {
|
||||||
|
param([PSCustomObject]$State)
|
||||||
|
|
||||||
|
return ((Get-SelectedDeviceNamingMode -State $State) -eq 'Template') -and ($State.Controls.txtDeviceNameTemplate.Text -match '(?i)%serial%')
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-UnattendSelectionControls {
|
||||||
|
param([PSCustomObject]$State)
|
||||||
|
|
||||||
|
$selectedDeviceNamingMode = Get-SelectedDeviceNamingMode -State $State
|
||||||
|
$isCopyUnattendSelected = $true -eq $State.Controls.chkCopyUnattend.IsChecked
|
||||||
|
$isInjectUnattendSelected = $true -eq $State.Controls.chkInjectUnattend.IsChecked
|
||||||
|
$deviceNameTemplateUsesSerialToken = Test-DeviceNameTemplateUsesSerialToken -State $State
|
||||||
|
$requiresCopiedUnattend = ($selectedDeviceNamingMode -in @('Prompt', 'Prefixes', 'SerialComputerNames')) -or $deviceNameTemplateUsesSerialToken
|
||||||
|
|
||||||
|
if ($isCopyUnattendSelected -and $isInjectUnattendSelected) {
|
||||||
|
if ($requiresCopiedUnattend) {
|
||||||
|
$State.Controls.chkInjectUnattend.IsChecked = $false
|
||||||
|
$isInjectUnattendSelected = $false
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$State.Controls.chkCopyUnattend.IsChecked = $false
|
||||||
|
$isCopyUnattendSelected = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($requiresCopiedUnattend) {
|
||||||
|
if (-not $isCopyUnattendSelected) {
|
||||||
|
$State.Controls.chkCopyUnattend.IsChecked = $true
|
||||||
|
$isCopyUnattendSelected = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isInjectUnattendSelected) {
|
||||||
|
$State.Controls.chkInjectUnattend.IsChecked = $false
|
||||||
|
$isInjectUnattendSelected = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$State.Controls.chkCopyUnattend.IsEnabled = $false
|
||||||
|
$State.Controls.chkInjectUnattend.IsEnabled = $false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isCopyUnattendSelected) {
|
||||||
|
$State.Controls.chkCopyUnattend.IsEnabled = $true
|
||||||
|
$State.Controls.chkInjectUnattend.IsEnabled = $false
|
||||||
|
}
|
||||||
|
elseif ($isInjectUnattendSelected) {
|
||||||
|
$State.Controls.chkCopyUnattend.IsEnabled = $false
|
||||||
|
$State.Controls.chkInjectUnattend.IsEnabled = $true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$State.Controls.chkCopyUnattend.IsEnabled = $true
|
||||||
|
$State.Controls.chkInjectUnattend.IsEnabled = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-DeviceNamingControls {
|
||||||
|
param([PSCustomObject]$State)
|
||||||
|
|
||||||
|
if (($true -eq $State.Controls.chkInjectUnattend.IsChecked) -and (($true -eq $State.Controls.rbDeviceNamingPrompt.IsChecked) -or ($true -eq $State.Controls.rbDeviceNamingPrefixes.IsChecked) -or ($true -eq $State.Controls.rbDeviceNamingSerialComputerNames.IsChecked))) {
|
||||||
|
$State.Controls.rbDeviceNamingNone.IsChecked = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedDeviceNamingMode = Get-SelectedDeviceNamingMode -State $State
|
||||||
|
$State.Controls.deviceNameTemplatePanel.Visibility = if ($selectedDeviceNamingMode -eq 'Template') { 'Visible' } else { 'Collapsed' }
|
||||||
|
$State.Controls.deviceNamePrefixesPanel.Visibility = if ($selectedDeviceNamingMode -eq 'Prefixes') { 'Visible' } else { 'Collapsed' }
|
||||||
|
$State.Controls.deviceNameSerialComputerNamesPanel.Visibility = if ($selectedDeviceNamingMode -eq 'SerialComputerNames') { 'Visible' } else { 'Collapsed' }
|
||||||
|
$State.Controls.rbDeviceNamingPrompt.IsEnabled = -not ($true -eq $State.Controls.chkInjectUnattend.IsChecked)
|
||||||
|
$State.Controls.rbDeviceNamingPrefixes.IsEnabled = -not ($true -eq $State.Controls.chkInjectUnattend.IsChecked)
|
||||||
|
$State.Controls.rbDeviceNamingSerialComputerNames.IsEnabled = -not ($true -eq $State.Controls.chkInjectUnattend.IsChecked)
|
||||||
|
|
||||||
|
if ($selectedDeviceNamingMode -eq 'Prefixes') {
|
||||||
|
Import-DeviceNamePrefixesFromConfiguredPath -State $State -SkipIfTextPresent
|
||||||
|
}
|
||||||
|
elseif ($selectedDeviceNamingMode -eq 'SerialComputerNames') {
|
||||||
|
Import-SerialComputerNamesFromConfiguredPath -State $State -SkipIfTextPresent
|
||||||
|
}
|
||||||
|
|
||||||
|
Update-UnattendSelectionControls -State $State
|
||||||
|
}
|
||||||
|
|
||||||
function Register-EventHandlers {
|
function Register-EventHandlers {
|
||||||
param([PSCustomObject]$State)
|
param([PSCustomObject]$State)
|
||||||
WriteLog "Registering UI event handlers..."
|
WriteLog "Registering UI event handlers..."
|
||||||
@@ -24,7 +359,7 @@ function Register-EventHandlers {
|
|||||||
|
|
||||||
# Define a handler to validate pasted text, ensuring it's only integers
|
# Define a handler to validate pasted text, ensuring it's only integers
|
||||||
$integerPastingHandler = {
|
$integerPastingHandler = {
|
||||||
param($sender, $pastingEventArgs)
|
param($eventSource, $pastingEventArgs)
|
||||||
if ($pastingEventArgs.DataObject.GetDataPresent([string])) {
|
if ($pastingEventArgs.DataObject.GetDataPresent([string])) {
|
||||||
$pastedText = $pastingEventArgs.DataObject.GetData([string])
|
$pastedText = $pastingEventArgs.DataObject.GetData([string])
|
||||||
# Check if the pasted text consists ONLY of one or more digits.
|
# Check if the pasted text consists ONLY of one or more digits.
|
||||||
@@ -87,6 +422,132 @@ function Register-EventHandlers {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Navigation Sidebar Event Handlers
|
||||||
|
# Main navigation list - switches content pages based on selected nav item
|
||||||
|
if ($null -ne $State.Controls.lstNavigation) {
|
||||||
|
$State.Controls.lstNavigation.Add_SelectionChanged({
|
||||||
|
param($eventSource, $selectionChangedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
if ($null -eq $window -or $null -eq $window.Tag) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$localState = $window.Tag
|
||||||
|
$selectedIndex = $eventSource.SelectedIndex
|
||||||
|
if ($selectedIndex -lt 0) { return }
|
||||||
|
|
||||||
|
# Clear Settings selection when main nav is used
|
||||||
|
if ($null -ne $localState.Controls.lstNavSettings) {
|
||||||
|
$localState.Controls.lstNavSettings.SelectedIndex = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hide all content pages
|
||||||
|
foreach ($page in $localState.Controls.navigationPages) {
|
||||||
|
if ($null -ne $page) { $page.Visibility = 'Collapsed' }
|
||||||
|
}
|
||||||
|
if ($null -ne $localState.Controls.pageSettings) {
|
||||||
|
$localState.Controls.pageSettings.Visibility = 'Collapsed'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show the selected page
|
||||||
|
if ($selectedIndex -lt $localState.Controls.navigationPages.Count) {
|
||||||
|
$localState.Controls.navigationPages[$selectedIndex].Visibility = 'Visible'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update the shared page title to match the selected navigation item
|
||||||
|
if ($null -ne $localState.Controls.txtPageTitle) {
|
||||||
|
$selectedNavigationItem = $eventSource.SelectedItem
|
||||||
|
if ($null -ne $selectedNavigationItem -and -not [string]::IsNullOrWhiteSpace([string]$selectedNavigationItem.Tag)) {
|
||||||
|
$localState.Controls.txtPageTitle.Text = [string]$selectedNavigationItem.Tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
# Settings navigation item
|
||||||
|
if ($null -ne $State.Controls.lstNavSettings) {
|
||||||
|
$State.Controls.lstNavSettings.Add_SelectionChanged({
|
||||||
|
param($eventSource, $selectionChangedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
if ($null -eq $window -or $null -eq $window.Tag) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$localState = $window.Tag
|
||||||
|
if ($eventSource.SelectedIndex -lt 0) { return }
|
||||||
|
|
||||||
|
# Clear main navigation selection
|
||||||
|
if ($null -ne $localState.Controls.lstNavigation) {
|
||||||
|
$localState.Controls.lstNavigation.SelectedIndex = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hide all content pages
|
||||||
|
foreach ($page in $localState.Controls.navigationPages) {
|
||||||
|
if ($null -ne $page) { $page.Visibility = 'Collapsed' }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show Settings page
|
||||||
|
if ($null -ne $localState.Controls.pageSettings) {
|
||||||
|
$localState.Controls.pageSettings.Visibility = 'Visible'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update the shared page title to match the selected navigation item
|
||||||
|
if ($null -ne $localState.Controls.txtPageTitle) {
|
||||||
|
$selectedNavigationItem = $eventSource.SelectedItem
|
||||||
|
if ($null -ne $selectedNavigationItem -and -not [string]::IsNullOrWhiteSpace([string]$selectedNavigationItem.Tag)) {
|
||||||
|
$localState.Controls.txtPageTitle.Text = [string]$selectedNavigationItem.Tag
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$localState.Controls.txtPageTitle.Text = 'Settings'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hyperlink navigation handlers for Home page links
|
||||||
|
$hyperlinkNames = @(
|
||||||
|
'linkQuickStart',
|
||||||
|
'linkDocs',
|
||||||
|
'linkGitHub',
|
||||||
|
'linkReleases',
|
||||||
|
'linkChangelog',
|
||||||
|
'linkVideo1',
|
||||||
|
'linkDiscussion1',
|
||||||
|
'linkDiscussion2',
|
||||||
|
'linkDiscussion3',
|
||||||
|
'linkDiscussion4',
|
||||||
|
'linkDiscussion5',
|
||||||
|
'linkDiscussions'
|
||||||
|
)
|
||||||
|
foreach ($linkName in $hyperlinkNames) {
|
||||||
|
$link = $State.Window.FindName($linkName)
|
||||||
|
if ($null -ne $link) {
|
||||||
|
$link.Add_RequestNavigate({
|
||||||
|
param($eventSource, $requestNavigateEventArgs)
|
||||||
|
Start-Process $requestNavigateEventArgs.Uri.AbsoluteUri
|
||||||
|
$requestNavigateEventArgs.Handled = $true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Settings Page Event Handlers
|
||||||
|
# Theme mode selector - switches between Light, Dark, and System Fluent themes
|
||||||
|
if ($null -ne $State.Controls.cmbThemeMode) {
|
||||||
|
$State.Controls.cmbThemeMode.Add_SelectionChanged({
|
||||||
|
param($eventSource, $selectionChangedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
if ($null -eq $window -or $null -eq $window.Tag) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$localState = $window.Tag
|
||||||
|
if (-not $localState.Flags.isFluentSupported) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$selectedTheme = $eventSource.SelectedItem
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($selectedTheme)) {
|
||||||
|
Initialize-FluentTheme -Window $window -ThemeMode $selectedTheme -State $localState
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
# Build Tab Event Handlers
|
# Build Tab Event Handlers
|
||||||
$State.Controls.btnBrowseFFUDevPath.Add_Click({
|
$State.Controls.btnBrowseFFUDevPath.Add_Click({
|
||||||
param($eventSource, $routedEventArgs)
|
param($eventSource, $routedEventArgs)
|
||||||
@@ -94,7 +555,34 @@ function Register-EventHandlers {
|
|||||||
$localState = $window.Tag
|
$localState = $window.Tag
|
||||||
$selectedPath = Invoke-BrowseAction -Type 'Folder' -Title "Select FFU Development Path"
|
$selectedPath = Invoke-BrowseAction -Type 'Folder' -Title "Select FFU Development Path"
|
||||||
if ($selectedPath) {
|
if ($selectedPath) {
|
||||||
|
$currentPrefixesPath = $localState.Controls.txtDeviceNamePrefixesPath.Text
|
||||||
|
$currentSerialComputerNamesPath = $localState.Controls.txtDeviceNameSerialComputerNamesPath.Text
|
||||||
|
$currentUnattendX64FilePath = $localState.Controls.txtUnattendX64FilePath.Text
|
||||||
|
$currentUnattendArm64FilePath = $localState.Controls.txtUnattendArm64FilePath.Text
|
||||||
|
$previousDefaultPrefixesPath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text
|
||||||
|
$previousDefaultSerialComputerNamesPath = Get-DefaultSerialComputerNamesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text
|
||||||
|
$previousDefaultUnattendX64FilePath = Get-DefaultUnattendFilePath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text -WindowsArch 'x64'
|
||||||
|
$previousDefaultUnattendArm64FilePath = Get-DefaultUnattendFilePath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text -WindowsArch 'arm64'
|
||||||
$localState.Controls.txtFFUDevPath.Text = $selectedPath
|
$localState.Controls.txtFFUDevPath.Text = $selectedPath
|
||||||
|
$newDefaultPrefixesPath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $selectedPath
|
||||||
|
$newDefaultSerialComputerNamesPath = Get-DefaultSerialComputerNamesPath -FFUDevelopmentPath $selectedPath
|
||||||
|
$newDefaultUnattendX64FilePath = Get-DefaultUnattendFilePath -FFUDevelopmentPath $selectedPath -WindowsArch 'x64'
|
||||||
|
$newDefaultUnattendArm64FilePath = Get-DefaultUnattendFilePath -FFUDevelopmentPath $selectedPath -WindowsArch 'arm64'
|
||||||
|
if ([string]::IsNullOrWhiteSpace($currentPrefixesPath) -or $currentPrefixesPath -ieq $previousDefaultPrefixesPath) {
|
||||||
|
$localState.Controls.txtDeviceNamePrefixesPath.Text = $newDefaultPrefixesPath
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath) -or $currentSerialComputerNamesPath -ieq $previousDefaultSerialComputerNamesPath) {
|
||||||
|
$localState.Controls.txtDeviceNameSerialComputerNamesPath.Text = $newDefaultSerialComputerNamesPath
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($currentUnattendX64FilePath) -or $currentUnattendX64FilePath -ieq $previousDefaultUnattendX64FilePath) {
|
||||||
|
$localState.Controls.txtUnattendX64FilePath.Text = $newDefaultUnattendX64FilePath
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($currentUnattendArm64FilePath) -or $currentUnattendArm64FilePath -ieq $previousDefaultUnattendArm64FilePath) {
|
||||||
|
$localState.Controls.txtUnattendArm64FilePath.Text = $newDefaultUnattendArm64FilePath
|
||||||
|
}
|
||||||
|
Import-DeviceNamePrefixesFromConfiguredPath -State $localState
|
||||||
|
Import-SerialComputerNamesFromConfiguredPath -State $localState
|
||||||
|
Update-DeviceNamingControls -State $localState
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -108,19 +596,246 @@ function Register-EventHandlers {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$State.Controls.rbDeviceNamingNone.Add_Checked({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
if (-not ($true -eq $localState.Flags.suppressDeviceNamingChangeTracking)) {
|
||||||
|
$localState.Flags.deviceNamingModeWasExplicitlyChanged = $true
|
||||||
|
$localState.Data.loadedDeviceNamingMode = $null
|
||||||
|
}
|
||||||
|
Update-DeviceNamingControls -State $localState
|
||||||
|
})
|
||||||
|
$State.Controls.rbDeviceNamingPrompt.Add_Checked({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
if (-not ($true -eq $localState.Flags.suppressDeviceNamingChangeTracking)) {
|
||||||
|
$localState.Flags.deviceNamingModeWasExplicitlyChanged = $true
|
||||||
|
$localState.Data.loadedDeviceNamingMode = $null
|
||||||
|
}
|
||||||
|
Update-DeviceNamingControls -State $localState
|
||||||
|
})
|
||||||
|
$State.Controls.rbDeviceNamingTemplate.Add_Checked({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
if (-not ($true -eq $localState.Flags.suppressDeviceNamingChangeTracking)) {
|
||||||
|
$localState.Flags.deviceNamingModeWasExplicitlyChanged = $true
|
||||||
|
$localState.Data.loadedDeviceNamingMode = $null
|
||||||
|
}
|
||||||
|
Update-DeviceNamingControls -State $localState
|
||||||
|
})
|
||||||
|
$State.Controls.txtDeviceNameTemplate.Add_TextChanged({
|
||||||
|
param($eventSource, $textChangedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
if ($null -ne $window -and $null -ne $window.Tag) {
|
||||||
|
Update-DeviceNamingControls -State $window.Tag
|
||||||
|
}
|
||||||
|
})
|
||||||
|
$State.Controls.rbDeviceNamingPrefixes.Add_Checked({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
if (-not ($true -eq $localState.Flags.suppressDeviceNamingChangeTracking)) {
|
||||||
|
$localState.Flags.deviceNamingModeWasExplicitlyChanged = $true
|
||||||
|
$localState.Data.loadedDeviceNamingMode = $null
|
||||||
|
}
|
||||||
|
Update-DeviceNamingControls -State $localState
|
||||||
|
})
|
||||||
|
$State.Controls.rbDeviceNamingSerialComputerNames.Add_Checked({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
if (-not ($true -eq $localState.Flags.suppressDeviceNamingChangeTracking)) {
|
||||||
|
$localState.Flags.deviceNamingModeWasExplicitlyChanged = $true
|
||||||
|
$localState.Data.loadedDeviceNamingMode = $null
|
||||||
|
}
|
||||||
|
Update-DeviceNamingControls -State $localState
|
||||||
|
})
|
||||||
|
$State.Controls.btnBrowseDeviceNamePrefixesPath.Add_Click({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
$currentPrefixesPath = $localState.Controls.txtDeviceNamePrefixesPath.Text
|
||||||
|
if ([string]::IsNullOrWhiteSpace($currentPrefixesPath)) {
|
||||||
|
$currentPrefixesPath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text
|
||||||
|
}
|
||||||
|
$initialDirectory = if ([string]::IsNullOrWhiteSpace($currentPrefixesPath)) {
|
||||||
|
$null
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Split-Path $currentPrefixesPath -Parent
|
||||||
|
}
|
||||||
|
$fileName = if ([string]::IsNullOrWhiteSpace($currentPrefixesPath)) { 'prefixes.txt' } else { Split-Path $currentPrefixesPath -Leaf }
|
||||||
|
$selectedPath = Invoke-BrowseAction -Type 'OpenFile' -Title 'Select prefixes file path' -Filter 'Text files (*.txt)|*.txt|All files (*.*)|*.*' -InitialDirectory $initialDirectory -FileName $fileName
|
||||||
|
if (Import-DeviceNamePrefixesFile -State $localState -FilePath $selectedPath) {
|
||||||
|
Update-DeviceNamingControls -State $localState
|
||||||
|
}
|
||||||
|
})
|
||||||
|
$State.Controls.btnBrowseDeviceNameSerialComputerNamesPath.Add_Click({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
$currentSerialComputerNamesPath = $localState.Controls.txtDeviceNameSerialComputerNamesPath.Text
|
||||||
|
if ([string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath)) {
|
||||||
|
$currentSerialComputerNamesPath = Get-DefaultSerialComputerNamesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text
|
||||||
|
}
|
||||||
|
$initialDirectory = if ([string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath)) {
|
||||||
|
$null
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Split-Path $currentSerialComputerNamesPath -Parent
|
||||||
|
}
|
||||||
|
$fileName = if ([string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath)) { 'SerialComputerNames.csv' } else { Split-Path $currentSerialComputerNamesPath -Leaf }
|
||||||
|
$selectedPath = Invoke-BrowseAction -Type 'OpenFile' -Title 'Select Serial Computer Names CSV Mapping File Path' -Filter 'CSV files (*.csv)|*.csv|All files (*.*)|*.*' -InitialDirectory $initialDirectory -FileName $fileName
|
||||||
|
if (Import-SerialComputerNamesFile -State $localState -FilePath $selectedPath) {
|
||||||
|
Update-DeviceNamingControls -State $localState
|
||||||
|
}
|
||||||
|
})
|
||||||
|
$State.Controls.btnBrowseUnattendX64FilePath.Add_Click({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
$currentUnattendX64FilePath = $localState.Controls.txtUnattendX64FilePath.Text
|
||||||
|
if ([string]::IsNullOrWhiteSpace($currentUnattendX64FilePath)) {
|
||||||
|
$currentUnattendX64FilePath = Get-DefaultUnattendFilePath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text -WindowsArch 'x64'
|
||||||
|
}
|
||||||
|
$initialDirectory = if ([string]::IsNullOrWhiteSpace($currentUnattendX64FilePath)) {
|
||||||
|
$null
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Split-Path $currentUnattendX64FilePath -Parent
|
||||||
|
}
|
||||||
|
$fileName = if ([string]::IsNullOrWhiteSpace($currentUnattendX64FilePath)) { 'unattend_x64.xml' } else { Split-Path $currentUnattendX64FilePath -Leaf }
|
||||||
|
$selectedPath = Invoke-BrowseAction -Type 'OpenFile' -Title 'Select x64 unattend XML file' -Filter 'XML files (*.xml)|*.xml|All files (*.*)|*.*' -InitialDirectory $initialDirectory -FileName $fileName
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($selectedPath)) {
|
||||||
|
$localState.Controls.txtUnattendX64FilePath.Text = $selectedPath
|
||||||
|
}
|
||||||
|
})
|
||||||
|
$State.Controls.btnBrowseUnattendArm64FilePath.Add_Click({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
$currentUnattendArm64FilePath = $localState.Controls.txtUnattendArm64FilePath.Text
|
||||||
|
if ([string]::IsNullOrWhiteSpace($currentUnattendArm64FilePath)) {
|
||||||
|
$currentUnattendArm64FilePath = Get-DefaultUnattendFilePath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text -WindowsArch 'arm64'
|
||||||
|
}
|
||||||
|
$initialDirectory = if ([string]::IsNullOrWhiteSpace($currentUnattendArm64FilePath)) {
|
||||||
|
$null
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Split-Path $currentUnattendArm64FilePath -Parent
|
||||||
|
}
|
||||||
|
$fileName = if ([string]::IsNullOrWhiteSpace($currentUnattendArm64FilePath)) { 'unattend_arm64.xml' } else { Split-Path $currentUnattendArm64FilePath -Leaf }
|
||||||
|
$selectedPath = Invoke-BrowseAction -Type 'OpenFile' -Title 'Select arm64 unattend XML file' -Filter 'XML files (*.xml)|*.xml|All files (*.*)|*.*' -InitialDirectory $initialDirectory -FileName $fileName
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($selectedPath)) {
|
||||||
|
$localState.Controls.txtUnattendArm64FilePath.Text = $selectedPath
|
||||||
|
}
|
||||||
|
})
|
||||||
|
$State.Controls.btnSaveDeviceNamePrefixes.Add_Click({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
$prefixLines = @(Get-DeviceNamePrefixes -State $localState)
|
||||||
|
|
||||||
|
if ($prefixLines.Count -eq 0) {
|
||||||
|
[System.Windows.MessageBox]::Show("Enter at least one prefix before saving the prefixes file.", "Prefixes Required", "OK", "Warning") | Out-Null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentPrefixesPath = $localState.Controls.txtDeviceNamePrefixesPath.Text
|
||||||
|
if ([string]::IsNullOrWhiteSpace($currentPrefixesPath)) {
|
||||||
|
$currentPrefixesPath = Get-DefaultDeviceNamePrefixesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($currentPrefixesPath)) {
|
||||||
|
$localState.Controls.txtDeviceNamePrefixesPath.Text = $currentPrefixesPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($currentPrefixesPath)) {
|
||||||
|
[System.Windows.MessageBox]::Show("Select a valid Prefixes File Path before saving prefixes.", "Prefixes File Path Required", "OK", "Warning") | Out-Null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$prefixLines | Set-Content -Path $currentPrefixesPath -Encoding UTF8
|
||||||
|
$localState.Controls.txtDeviceNamePrefixesPath.Text = $currentPrefixesPath
|
||||||
|
WriteLog "Saved device name prefixes to $currentPrefixesPath"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
[System.Windows.MessageBox]::Show("Saving prefixes failed for '$currentPrefixesPath'. $($_.Exception.Message)", "Save Prefixes Failed", "OK", "Error") | Out-Null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
$State.Controls.btnSaveDeviceNameSerialComputerNames.Add_Click({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
$serialComputerNameLines = @(Get-SerialComputerNamesLines -State $localState)
|
||||||
|
|
||||||
|
if ($serialComputerNameLines.Count -eq 0) {
|
||||||
|
[System.Windows.MessageBox]::Show("Enter CSV content before saving the serial mapping file.", "Serial Mapping Required", "OK", "Warning") | Out-Null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentSerialComputerNamesPath = $localState.Controls.txtDeviceNameSerialComputerNamesPath.Text
|
||||||
|
if ([string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath)) {
|
||||||
|
$currentSerialComputerNamesPath = Get-DefaultSerialComputerNamesPath -FFUDevelopmentPath $localState.Controls.txtFFUDevPath.Text
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath)) {
|
||||||
|
$localState.Controls.txtDeviceNameSerialComputerNamesPath.Text = $currentSerialComputerNamesPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($currentSerialComputerNamesPath)) {
|
||||||
|
[System.Windows.MessageBox]::Show("Select a valid Serial Computer Names CSV Mapping File Path before saving the serial mapping.", "Serial Mapping File Path Required", "OK", "Warning") | Out-Null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$serialComputerNameLines | Set-Content -Path $currentSerialComputerNamesPath -Encoding UTF8
|
||||||
|
$localState.Controls.txtDeviceNameSerialComputerNamesPath.Text = $currentSerialComputerNamesPath
|
||||||
|
WriteLog "Saved serial computer-name mappings to $currentSerialComputerNamesPath"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
[System.Windows.MessageBox]::Show("Saving serial mapping failed for '$currentSerialComputerNamesPath'. $($_.Exception.Message)", "Save Serial Mapping Failed", "OK", "Error") | Out-Null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
$State.Controls.chkCopyUnattend.Add_Checked({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
$localState.Controls.chkInjectUnattend.IsChecked = $false
|
||||||
|
Update-DeviceNamingControls -State $localState
|
||||||
|
})
|
||||||
|
$State.Controls.chkCopyUnattend.Add_Unchecked({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
Update-DeviceNamingControls -State $window.Tag
|
||||||
|
})
|
||||||
|
$State.Controls.chkInjectUnattend.Add_Checked({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
$localState.Controls.chkCopyUnattend.IsChecked = $false
|
||||||
|
Update-DeviceNamingControls -State $localState
|
||||||
|
})
|
||||||
|
$State.Controls.chkInjectUnattend.Add_Unchecked({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
Update-DeviceNamingControls -State $window.Tag
|
||||||
|
})
|
||||||
|
|
||||||
# Build USB Drive Settings Event Handlers
|
# Build USB Drive Settings Event Handlers
|
||||||
|
# The USB Expander is always visible; the checkbox controls child settings only
|
||||||
$State.Controls.chkBuildUSBDriveEnable.Add_Checked({
|
$State.Controls.chkBuildUSBDriveEnable.Add_Checked({
|
||||||
param($eventSource, $routedEventArgs)
|
param($eventSource, $routedEventArgs)
|
||||||
$window = [System.Windows.Window]::GetWindow($eventSource)
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
$localState = $window.Tag
|
$localState = $window.Tag
|
||||||
$localState.Controls.usbSection.Visibility = 'Visible'
|
|
||||||
$localState.Controls.chkSelectSpecificUSBDrives.IsEnabled = $true
|
$localState.Controls.chkSelectSpecificUSBDrives.IsEnabled = $true
|
||||||
})
|
})
|
||||||
$State.Controls.chkBuildUSBDriveEnable.Add_Unchecked({
|
$State.Controls.chkBuildUSBDriveEnable.Add_Unchecked({
|
||||||
param($eventSource, $routedEventArgs)
|
param($eventSource, $routedEventArgs)
|
||||||
$window = [System.Windows.Window]::GetWindow($eventSource)
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
$localState = $window.Tag
|
$localState = $window.Tag
|
||||||
$localState.Controls.usbSection.Visibility = 'Collapsed'
|
|
||||||
$localState.Controls.chkSelectSpecificUSBDrives.IsEnabled = $false
|
$localState.Controls.chkSelectSpecificUSBDrives.IsEnabled = $false
|
||||||
$localState.Controls.chkSelectSpecificUSBDrives.IsChecked = $false
|
$localState.Controls.chkSelectSpecificUSBDrives.IsChecked = $false
|
||||||
$localState.Controls.lstUSBDrives.Items.Clear()
|
$localState.Controls.lstUSBDrives.Items.Clear()
|
||||||
@@ -220,6 +935,7 @@ function Register-EventHandlers {
|
|||||||
$driveObject | Add-Member -MemberType NoteProperty -Name 'IsSelected' -Value $false -Force
|
$driveObject | Add-Member -MemberType NoteProperty -Name 'IsSelected' -Value $false -Force
|
||||||
$localState.Controls.lstUSBDrives.Items.Add($driveObject)
|
$localState.Controls.lstUSBDrives.Items.Add($driveObject)
|
||||||
}
|
}
|
||||||
|
Request-ListViewColumnAutoResize -ListView $localState.Controls.lstUSBDrives
|
||||||
if ($localState.Controls.lstUSBDrives.Items.Count -gt 0) {
|
if ($localState.Controls.lstUSBDrives.Items.Count -gt 0) {
|
||||||
$localState.Controls.lstUSBDrives.SelectedIndex = 0
|
$localState.Controls.lstUSBDrives.SelectedIndex = 0
|
||||||
}
|
}
|
||||||
@@ -253,42 +969,30 @@ function Register-EventHandlers {
|
|||||||
})
|
})
|
||||||
|
|
||||||
# Hyper-V tab event handlers
|
# Hyper-V tab event handlers
|
||||||
|
$State.Controls.chkEnableVMNetworking.Add_Checked({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
Update-VMNetworkingControls -State $localState
|
||||||
|
})
|
||||||
|
|
||||||
|
$State.Controls.chkEnableVMNetworking.Add_Unchecked({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
Update-VMNetworkingControls -State $localState
|
||||||
|
})
|
||||||
|
|
||||||
$State.Controls.cmbVMSwitchName.Add_SelectionChanged({
|
$State.Controls.cmbVMSwitchName.Add_SelectionChanged({
|
||||||
param($eventSource, $selectionChangedEventArgs)
|
param($eventSource, $selectionChangedEventArgs)
|
||||||
# The state object is available via the parent window's Tag property
|
# The state object is available via the parent window's Tag property
|
||||||
$window = [System.Windows.Window]::GetWindow($eventSource)
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
$localState = $window.Tag
|
$localState = $window.Tag
|
||||||
|
|
||||||
$selectedItem = $eventSource.SelectedItem
|
Update-VMNetworkingControls -State $localState
|
||||||
if ($selectedItem -eq 'Other') {
|
|
||||||
$localState.Controls.txtCustomVMSwitchName.Visibility = 'Visible'
|
|
||||||
if ([string]::IsNullOrWhiteSpace($localState.Controls.txtCustomVMSwitchName.Text) -and $null -ne $localState.Data.customVMSwitchName) {
|
|
||||||
$localState.Controls.txtCustomVMSwitchName.Text = $localState.Data.customVMSwitchName
|
|
||||||
}
|
|
||||||
if ($null -ne $localState.Data.customVMHostIP -and -not [string]::IsNullOrWhiteSpace($localState.Data.customVMHostIP)) {
|
|
||||||
$localState.Controls.txtVMHostIPAddress.Text = $localState.Data.customVMHostIP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$localState.Controls.txtCustomVMSwitchName.Visibility = 'Collapsed'
|
|
||||||
if ($null -ne $selectedItem -and $localState.Data.vmSwitchMap.ContainsKey($selectedItem)) {
|
|
||||||
$localState.Controls.txtVMHostIPAddress.Text = $localState.Data.vmSwitchMap[$selectedItem]
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$localState.Controls.txtVMHostIPAddress.Text = '' # Clear IP if not found or key null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
# Persist custom VM switch name/IP when user edits them while 'Other' is selected
|
# Persist custom VM switch name when user edits it while 'Other' is selected
|
||||||
$State.Controls.txtVMHostIPAddress.Add_LostFocus({
|
|
||||||
param($eventSource, $routedEventArgs)
|
|
||||||
$window = [System.Windows.Window]::GetWindow($eventSource)
|
|
||||||
$localState = $window.Tag
|
|
||||||
if ($localState.Controls.cmbVMSwitchName.SelectedItem -eq 'Other') {
|
|
||||||
$localState.Data.customVMHostIP = $localState.Controls.txtVMHostIPAddress.Text
|
|
||||||
}
|
|
||||||
})
|
|
||||||
$State.Controls.txtCustomVMSwitchName.Add_LostFocus({
|
$State.Controls.txtCustomVMSwitchName.Add_LostFocus({
|
||||||
param($eventSource, $routedEventArgs)
|
param($eventSource, $routedEventArgs)
|
||||||
$window = [System.Windows.Window]::GetWindow($eventSource)
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
@@ -299,12 +1003,31 @@ function Register-EventHandlers {
|
|||||||
})
|
})
|
||||||
|
|
||||||
# Windows Settings tab Event Handlers
|
# Windows Settings tab Event Handlers
|
||||||
$State.Controls.txtISOPath.Add_TextChanged({
|
# Windows Media Source radio buttons
|
||||||
param($eventSource, $textChangedEventArgs)
|
if ($null -ne $State.Controls.rbProvideISO) {
|
||||||
$window = [System.Windows.Window]::GetWindow($eventSource)
|
$State.Controls.rbProvideISO.Add_Checked({
|
||||||
$localState = $window.Tag
|
param($eventSource, $routedEventArgs)
|
||||||
Get-WindowsSettingsCombos -isoPath $localState.Controls.txtISOPath.Text -State $localState
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
})
|
if ($null -eq $window -or $null -eq $window.Tag) { return }
|
||||||
|
$localState = $window.Tag
|
||||||
|
$localState.Controls.isoPathPanel.Visibility = 'Visible'
|
||||||
|
# Use a placeholder .iso path to trigger ISO mode even before a real path is provided
|
||||||
|
$isoPath = $localState.Controls.txtISOPath.Text
|
||||||
|
if ([string]::IsNullOrWhiteSpace($isoPath)) {
|
||||||
|
$isoPath = 'placeholder.iso'
|
||||||
|
}
|
||||||
|
Get-WindowsSettingsCombos -isoPath $isoPath -State $localState
|
||||||
|
})
|
||||||
|
$State.Controls.rbProvideISO.Add_Unchecked({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
if ($null -eq $window -or $null -eq $window.Tag) { return }
|
||||||
|
$localState = $window.Tag
|
||||||
|
$localState.Controls.isoPathPanel.Visibility = 'Collapsed'
|
||||||
|
$localState.Controls.txtISOPath.Text = ''
|
||||||
|
Get-WindowsSettingsCombos -isoPath '' -State $localState
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
$State.Controls.cmbWindowsRelease.Add_SelectionChanged({
|
$State.Controls.cmbWindowsRelease.Add_SelectionChanged({
|
||||||
param($eventSource, $selectionChangedEventArgs)
|
param($eventSource, $selectionChangedEventArgs)
|
||||||
@@ -314,7 +1037,13 @@ function Register-EventHandlers {
|
|||||||
if ($null -ne $localState.Controls.cmbWindowsRelease.SelectedItem) {
|
if ($null -ne $localState.Controls.cmbWindowsRelease.SelectedItem) {
|
||||||
$selectedReleaseValue = $localState.Controls.cmbWindowsRelease.SelectedItem.Value
|
$selectedReleaseValue = $localState.Controls.cmbWindowsRelease.SelectedItem.Value
|
||||||
}
|
}
|
||||||
Update-WindowsVersionCombo -selectedRelease $selectedReleaseValue -isoPath $localState.Controls.txtISOPath.Text -State $localState
|
# Determine ISO path based on radio button state
|
||||||
|
$isoPath = ''
|
||||||
|
if ($null -ne $localState.Controls.rbProvideISO -and $localState.Controls.rbProvideISO.IsChecked) {
|
||||||
|
$isoPath = $localState.Controls.txtISOPath.Text
|
||||||
|
if ([string]::IsNullOrWhiteSpace($isoPath)) { $isoPath = 'placeholder.iso' }
|
||||||
|
}
|
||||||
|
Update-WindowsVersionCombo -selectedRelease $selectedReleaseValue -isoPath $isoPath -State $localState
|
||||||
Update-WindowsSkuCombo -State $localState
|
Update-WindowsSkuCombo -State $localState
|
||||||
Update-WindowsArchCombo -State $localState
|
Update-WindowsArchCombo -State $localState
|
||||||
|
|
||||||
@@ -435,10 +1164,18 @@ function Register-EventHandlers {
|
|||||||
param($eventSource, $routedEventArgs)
|
param($eventSource, $routedEventArgs)
|
||||||
$window = [System.Windows.Window]::GetWindow($eventSource)
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
$localState = $window.Tag
|
$localState = $window.Tag
|
||||||
$selectedPath = Invoke-BrowseAction -Type 'OpenFile' -Title "Select AppList.json File" -Filter "JSON files (*.json)|*.json" -AllowNewFile
|
$selectedPath = Invoke-BrowseAction -Type 'OpenFile' -Title "Select Winget AppList File" -Filter "JSON files (*.json)|*.json" -AllowNewFile
|
||||||
if ($selectedPath) { $localState.Controls.txtAppListJsonPath.Text = $selectedPath }
|
if ($selectedPath) { $localState.Controls.txtAppListJsonPath.Text = $selectedPath }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$State.Controls.btnBrowseUserAppListPath.Add_Click({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
$selectedPath = Invoke-BrowseAction -Type 'OpenFile' -Title "Select BYO AppList File" -Filter "JSON files (*.json)|*.json" -AllowNewFile
|
||||||
|
if ($selectedPath) { $localState.Controls.txtUserAppListPath.Text = $selectedPath }
|
||||||
|
})
|
||||||
|
|
||||||
$State.Controls.btnBrowseAppSource.Add_Click({
|
$State.Controls.btnBrowseAppSource.Add_Click({
|
||||||
param($eventSource, $routedEventArgs)
|
param($eventSource, $routedEventArgs)
|
||||||
$window = [System.Windows.Window]::GetWindow($eventSource)
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
@@ -466,17 +1203,23 @@ function Register-EventHandlers {
|
|||||||
$window = [System.Windows.Window]::GetWindow($eventSource)
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
$localState = $window.Tag
|
$localState = $window.Tag
|
||||||
|
|
||||||
$initialDir = $localState.Controls.txtApplicationPath.Text
|
# Default the save dialog to the configured BYO app list path.
|
||||||
|
$currentPath = $localState.Controls.txtUserAppListPath.Text
|
||||||
|
$initialDir = if (-not [string]::IsNullOrWhiteSpace($currentPath)) { Split-Path -Path $currentPath -Parent } else { $localState.Controls.txtApplicationPath.Text }
|
||||||
if ([string]::IsNullOrWhiteSpace($initialDir) -or -not (Test-Path $initialDir)) { $initialDir = $localState.FFUDevelopmentPath }
|
if ([string]::IsNullOrWhiteSpace($initialDir) -or -not (Test-Path $initialDir)) { $initialDir = $localState.FFUDevelopmentPath }
|
||||||
|
$fileName = if (-not [string]::IsNullOrWhiteSpace($currentPath)) { Split-Path -Path $currentPath -Leaf } else { "UserAppList.json" }
|
||||||
|
|
||||||
$savePath = Invoke-BrowseAction -Type 'SaveFile' `
|
$savePath = Invoke-BrowseAction -Type 'SaveFile' `
|
||||||
-Title "Save Application List" `
|
-Title "Save BYO App List" `
|
||||||
-Filter "JSON files (*.json)|*.json|All files (*.*)|*.*" `
|
-Filter "JSON files (*.json)|*.json|All files (*.*)|*.*" `
|
||||||
-InitialDirectory $initialDir `
|
-InitialDirectory $initialDir `
|
||||||
-FileName "UserAppList.json" `
|
-FileName $fileName `
|
||||||
-DefaultExt ".json"
|
-DefaultExt ".json"
|
||||||
|
|
||||||
if ($savePath) { Save-BYOApplicationList -Path $savePath -State $localState }
|
if ($savePath) {
|
||||||
|
$localState.Controls.txtUserAppListPath.Text = $savePath
|
||||||
|
Save-BYOApplicationList -Path $savePath -State $localState
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$State.Controls.btnLoadBYOApplications.Add_Click({
|
$State.Controls.btnLoadBYOApplications.Add_Click({
|
||||||
@@ -484,15 +1227,18 @@ function Register-EventHandlers {
|
|||||||
$window = [System.Windows.Window]::GetWindow($eventSource)
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
$localState = $window.Tag
|
$localState = $window.Tag
|
||||||
|
|
||||||
$initialDir = $localState.Controls.txtApplicationPath.Text
|
# Default the import dialog to the configured BYO app list path.
|
||||||
|
$currentPath = $localState.Controls.txtUserAppListPath.Text
|
||||||
|
$initialDir = if (-not [string]::IsNullOrWhiteSpace($currentPath)) { Split-Path -Path $currentPath -Parent } else { $localState.Controls.txtApplicationPath.Text }
|
||||||
if ([string]::IsNullOrWhiteSpace($initialDir) -or -not (Test-Path $initialDir)) { $initialDir = $localState.FFUDevelopmentPath }
|
if ([string]::IsNullOrWhiteSpace($initialDir) -or -not (Test-Path $initialDir)) { $initialDir = $localState.FFUDevelopmentPath }
|
||||||
|
|
||||||
$loadPath = Invoke-BrowseAction -Type 'OpenFile' `
|
$loadPath = Invoke-BrowseAction -Type 'OpenFile' `
|
||||||
-Title "Import Application List" `
|
-Title "Import BYO App List" `
|
||||||
-Filter "JSON files (*.json)|*.json|All files (*.*)|*.*" `
|
-Filter "JSON files (*.json)|*.json|All files (*.*)|*.*" `
|
||||||
-InitialDirectory $initialDir
|
-InitialDirectory $initialDir
|
||||||
|
|
||||||
if ($loadPath) {
|
if ($loadPath) {
|
||||||
|
$localState.Controls.txtUserAppListPath.Text = $loadPath
|
||||||
Import-BYOApplicationList -Path $loadPath -State $localState
|
Import-BYOApplicationList -Path $loadPath -State $localState
|
||||||
Update-CopyButtonState -State $localState
|
Update-CopyButtonState -State $localState
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,97 @@
|
|||||||
This module is critical for setting up the initial state of the application window when it first loads.
|
This module is critical for setting up the initial state of the application window when it first loads.
|
||||||
#>
|
#>
|
||||||
|
|
||||||
|
function Initialize-FluentTheme {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[string]$ThemeMode = "System",
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[PSCustomObject]$State
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the current .NET runtime supports Window.ThemeMode (requires .NET 9+ / PowerShell 7.5+)
|
||||||
|
$themeModeProperty = [System.Windows.Window].GetProperty("ThemeMode")
|
||||||
|
if ($null -eq $themeModeProperty) {
|
||||||
|
WriteLog "Fluent theme not available. Window.ThemeMode requires PowerShell 7.5+ (.NET 9+). Using default Aero2 theme."
|
||||||
|
if ($null -ne $State) {
|
||||||
|
$State.Flags.isFluentSupported = $false
|
||||||
|
}
|
||||||
|
# Still create tooltip styles for non-Fluent mode so Tag-to-ToolTip binding works
|
||||||
|
$controlTypes = @(
|
||||||
|
[System.Windows.Controls.TextBox],
|
||||||
|
[System.Windows.Controls.TextBlock],
|
||||||
|
[System.Windows.Controls.CheckBox]
|
||||||
|
)
|
||||||
|
foreach ($controlType in $controlTypes) {
|
||||||
|
$newStyle = New-Object System.Windows.Style($controlType)
|
||||||
|
$toolTipBinding = New-Object System.Windows.Data.Binding("Tag")
|
||||||
|
$toolTipBinding.RelativeSource = [System.Windows.Data.RelativeSource]::new([System.Windows.Data.RelativeSourceMode]::Self)
|
||||||
|
$toolTipSetter = New-Object System.Windows.Setter([System.Windows.FrameworkElement]::ToolTipProperty, $toolTipBinding)
|
||||||
|
$newStyle.Setters.Add($toolTipSetter)
|
||||||
|
if ($Window.Resources.Contains($controlType)) {
|
||||||
|
$Window.Resources.Remove($controlType)
|
||||||
|
}
|
||||||
|
$Window.Resources.Add($controlType, $newStyle)
|
||||||
|
}
|
||||||
|
WriteLog "Tooltip styles created for non-Fluent mode."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mark Fluent as supported in state
|
||||||
|
if ($null -ne $State) {
|
||||||
|
$State.Flags.isFluentSupported = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve the ThemeMode enum value using reflection to avoid compile-time experimental attribute issues
|
||||||
|
$themeModeType = [System.Windows.Window].GetProperty("ThemeMode").PropertyType
|
||||||
|
$themeModeValue = $null
|
||||||
|
switch ($ThemeMode) {
|
||||||
|
"Light" { $themeModeValue = $themeModeType::Light }
|
||||||
|
"Dark" { $themeModeValue = $themeModeType::Dark }
|
||||||
|
"System" { $themeModeValue = $themeModeType::System }
|
||||||
|
default { $themeModeValue = $themeModeType::System }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Apply the Fluent theme mode to the window
|
||||||
|
$themeModeProperty.SetValue($Window, $themeModeValue)
|
||||||
|
WriteLog "Applied Fluent theme: $ThemeMode"
|
||||||
|
|
||||||
|
# Re-create implicit tooltip styles with BasedOn pointing to the Fluent base style
|
||||||
|
# This preserves the Tag-to-ToolTip binding while inheriting Fluent visual styling
|
||||||
|
$controlTypes = @(
|
||||||
|
[System.Windows.Controls.TextBox],
|
||||||
|
[System.Windows.Controls.TextBlock],
|
||||||
|
[System.Windows.Controls.CheckBox]
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($controlType in $controlTypes) {
|
||||||
|
# Get the Fluent base style that was loaded by ThemeMode
|
||||||
|
$fluentBaseStyle = $Window.TryFindResource($controlType)
|
||||||
|
|
||||||
|
# Create a new implicit style with ToolTip binding
|
||||||
|
$newStyle = New-Object System.Windows.Style($controlType)
|
||||||
|
if ($null -ne $fluentBaseStyle) {
|
||||||
|
$newStyle.BasedOn = $fluentBaseStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add the ToolTip setter that binds to the Tag property
|
||||||
|
$toolTipBinding = New-Object System.Windows.Data.Binding("Tag")
|
||||||
|
$toolTipBinding.RelativeSource = [System.Windows.Data.RelativeSource]::new([System.Windows.Data.RelativeSourceMode]::Self)
|
||||||
|
$toolTipSetter = New-Object System.Windows.Setter([System.Windows.FrameworkElement]::ToolTipProperty, $toolTipBinding)
|
||||||
|
$newStyle.Setters.Add($toolTipSetter)
|
||||||
|
|
||||||
|
# Remove any existing implicit style for this type before adding the new one
|
||||||
|
if ($Window.Resources.Contains($controlType)) {
|
||||||
|
$Window.Resources.Remove($controlType)
|
||||||
|
}
|
||||||
|
$Window.Resources.Add($controlType, $newStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteLog "Tooltip styles updated with Fluent base styles."
|
||||||
|
}
|
||||||
|
|
||||||
function Initialize-UIControls {
|
function Initialize-UIControls {
|
||||||
param([PSCustomObject]$State)
|
param([PSCustomObject]$State)
|
||||||
WriteLog "Initializing UI control references..."
|
WriteLog "Initializing UI control references..."
|
||||||
@@ -21,6 +112,9 @@ function Initialize-UIControls {
|
|||||||
$State.Controls.cmbWindowsRelease = $window.FindName('cmbWindowsRelease')
|
$State.Controls.cmbWindowsRelease = $window.FindName('cmbWindowsRelease')
|
||||||
$State.Controls.cmbWindowsVersion = $window.FindName('cmbWindowsVersion')
|
$State.Controls.cmbWindowsVersion = $window.FindName('cmbWindowsVersion')
|
||||||
$State.Controls.txtISOPath = $window.FindName('txtISOPath')
|
$State.Controls.txtISOPath = $window.FindName('txtISOPath')
|
||||||
|
$State.Controls.rbDownloadESD = $window.FindName('rbDownloadESD')
|
||||||
|
$State.Controls.rbProvideISO = $window.FindName('rbProvideISO')
|
||||||
|
$State.Controls.isoPathPanel = $window.FindName('isoPathPanel')
|
||||||
$State.Controls.btnBrowseISO = $window.FindName('btnBrowseISO')
|
$State.Controls.btnBrowseISO = $window.FindName('btnBrowseISO')
|
||||||
$State.Controls.cmbWindowsArch = $window.FindName('cmbWindowsArch')
|
$State.Controls.cmbWindowsArch = $window.FindName('cmbWindowsArch')
|
||||||
$State.Controls.cmbWindowsLang = $window.FindName('cmbWindowsLang')
|
$State.Controls.cmbWindowsLang = $window.FindName('cmbWindowsLang')
|
||||||
@@ -70,8 +164,10 @@ function Initialize-UIControls {
|
|||||||
$State.Controls.txtWingetModuleVersion = $window.FindName('txtWingetModuleVersion')
|
$State.Controls.txtWingetModuleVersion = $window.FindName('txtWingetModuleVersion')
|
||||||
$State.Controls.applicationPathPanel = $window.FindName('applicationPathPanel')
|
$State.Controls.applicationPathPanel = $window.FindName('applicationPathPanel')
|
||||||
$State.Controls.appListJsonPathPanel = $window.FindName('appListJsonPathPanel')
|
$State.Controls.appListJsonPathPanel = $window.FindName('appListJsonPathPanel')
|
||||||
|
$State.Controls.userAppListPathPanel = $window.FindName('userAppListPathPanel')
|
||||||
$State.Controls.btnBrowseApplicationPath = $window.FindName('btnBrowseApplicationPath')
|
$State.Controls.btnBrowseApplicationPath = $window.FindName('btnBrowseApplicationPath')
|
||||||
$State.Controls.btnBrowseAppListJsonPath = $window.FindName('btnBrowseAppListJsonPath')
|
$State.Controls.btnBrowseAppListJsonPath = $window.FindName('btnBrowseAppListJsonPath')
|
||||||
|
$State.Controls.btnBrowseUserAppListPath = $window.FindName('btnBrowseUserAppListPath')
|
||||||
$State.Controls.chkBringYourOwnApps = $window.FindName('chkBringYourOwnApps')
|
$State.Controls.chkBringYourOwnApps = $window.FindName('chkBringYourOwnApps')
|
||||||
$State.Controls.byoApplicationPanel = $window.FindName('byoApplicationPanel')
|
$State.Controls.byoApplicationPanel = $window.FindName('byoApplicationPanel')
|
||||||
$State.Controls.wingetSearchPanel = $window.FindName('wingetSearchPanel')
|
$State.Controls.wingetSearchPanel = $window.FindName('wingetSearchPanel')
|
||||||
@@ -109,29 +205,47 @@ function Initialize-UIControls {
|
|||||||
$State.Controls.txtStatus = $window.FindName('txtStatus')
|
$State.Controls.txtStatus = $window.FindName('txtStatus')
|
||||||
$State.Controls.pbOverallProgress = $window.FindName('progressBar')
|
$State.Controls.pbOverallProgress = $window.FindName('progressBar')
|
||||||
$State.Controls.txtOverallStatus = $window.FindName('txtStatus')
|
$State.Controls.txtOverallStatus = $window.FindName('txtStatus')
|
||||||
|
$State.Controls.chkEnableVMNetworking = $window.FindName('chkEnableVMNetworking')
|
||||||
|
$State.Controls.spVMNetworkingSettings = $window.FindName('spVMNetworkingSettings')
|
||||||
$State.Controls.cmbVMSwitchName = $window.FindName('cmbVMSwitchName')
|
$State.Controls.cmbVMSwitchName = $window.FindName('cmbVMSwitchName')
|
||||||
$State.Controls.txtVMHostIPAddress = $window.FindName('txtVMHostIPAddress')
|
|
||||||
$State.Controls.txtCustomVMSwitchName = $window.FindName('txtCustomVMSwitchName')
|
$State.Controls.txtCustomVMSwitchName = $window.FindName('txtCustomVMSwitchName')
|
||||||
$State.Controls.txtFFUDevPath = $window.FindName('txtFFUDevPath')
|
$State.Controls.txtFFUDevPath = $window.FindName('txtFFUDevPath')
|
||||||
$State.Controls.txtCustomFFUNameTemplate = $window.FindName('txtCustomFFUNameTemplate')
|
$State.Controls.txtCustomFFUNameTemplate = $window.FindName('txtCustomFFUNameTemplate')
|
||||||
$State.Controls.txtFFUCaptureLocation = $window.FindName('txtFFUCaptureLocation')
|
$State.Controls.txtFFUCaptureLocation = $window.FindName('txtFFUCaptureLocation')
|
||||||
$State.Controls.txtShareName = $window.FindName('txtShareName')
|
|
||||||
$State.Controls.txtUsername = $window.FindName('txtUsername')
|
|
||||||
$State.Controls.txtThreads = $window.FindName('txtThreads')
|
$State.Controls.txtThreads = $window.FindName('txtThreads')
|
||||||
$State.Controls.cmbBitsPriority = $window.FindName('cmbBitsPriority')
|
$State.Controls.cmbBitsPriority = $window.FindName('cmbBitsPriority')
|
||||||
$State.Controls.txtMaxUSBDrives = $window.FindName('txtMaxUSBDrives')
|
$State.Controls.txtMaxUSBDrives = $window.FindName('txtMaxUSBDrives')
|
||||||
$State.Controls.chkCompactOS = $window.FindName('chkCompactOS')
|
$State.Controls.chkCompactOS = $window.FindName('chkCompactOS')
|
||||||
$State.Controls.chkOptimize = $window.FindName('chkOptimize')
|
$State.Controls.chkOptimize = $window.FindName('chkOptimize')
|
||||||
$State.Controls.chkAllowVHDXCaching = $window.FindName('chkAllowVHDXCaching')
|
$State.Controls.chkAllowVHDXCaching = $window.FindName('chkAllowVHDXCaching')
|
||||||
$State.Controls.chkCreateCaptureMedia = $window.FindName('chkCreateCaptureMedia')
|
|
||||||
$State.Controls.chkCreateDeploymentMedia = $window.FindName('chkCreateDeploymentMedia')
|
$State.Controls.chkCreateDeploymentMedia = $window.FindName('chkCreateDeploymentMedia')
|
||||||
$State.Controls.chkInjectUnattend = $window.FindName('chkInjectUnattend')
|
$State.Controls.chkInjectUnattend = $window.FindName('chkInjectUnattend')
|
||||||
|
$State.Controls.txtUnattendX64FilePath = $window.FindName('txtUnattendX64FilePath')
|
||||||
|
$State.Controls.btnBrowseUnattendX64FilePath = $window.FindName('btnBrowseUnattendX64FilePath')
|
||||||
|
$State.Controls.txtUnattendArm64FilePath = $window.FindName('txtUnattendArm64FilePath')
|
||||||
|
$State.Controls.btnBrowseUnattendArm64FilePath = $window.FindName('btnBrowseUnattendArm64FilePath')
|
||||||
|
$State.Controls.rbDeviceNamingNone = $window.FindName('rbDeviceNamingNone')
|
||||||
|
$State.Controls.rbDeviceNamingPrompt = $window.FindName('rbDeviceNamingPrompt')
|
||||||
|
$State.Controls.rbDeviceNamingTemplate = $window.FindName('rbDeviceNamingTemplate')
|
||||||
|
$State.Controls.rbDeviceNamingPrefixes = $window.FindName('rbDeviceNamingPrefixes')
|
||||||
|
$State.Controls.rbDeviceNamingSerialComputerNames = $window.FindName('rbDeviceNamingSerialComputerNames')
|
||||||
|
$State.Controls.deviceNameTemplatePanel = $window.FindName('deviceNameTemplatePanel')
|
||||||
|
$State.Controls.deviceNamePrefixesPanel = $window.FindName('deviceNamePrefixesPanel')
|
||||||
|
$State.Controls.deviceNameSerialComputerNamesPanel = $window.FindName('deviceNameSerialComputerNamesPanel')
|
||||||
|
$State.Controls.txtDeviceNameTemplate = $window.FindName('txtDeviceNameTemplate')
|
||||||
|
$State.Controls.txtDeviceNamePrefixesPath = $window.FindName('txtDeviceNamePrefixesPath')
|
||||||
|
$State.Controls.btnBrowseDeviceNamePrefixesPath = $window.FindName('btnBrowseDeviceNamePrefixesPath')
|
||||||
|
$State.Controls.txtDeviceNamePrefixes = $window.FindName('txtDeviceNamePrefixes')
|
||||||
|
$State.Controls.btnSaveDeviceNamePrefixes = $window.FindName('btnSaveDeviceNamePrefixes')
|
||||||
|
$State.Controls.txtDeviceNameSerialComputerNamesPath = $window.FindName('txtDeviceNameSerialComputerNamesPath')
|
||||||
|
$State.Controls.btnBrowseDeviceNameSerialComputerNamesPath = $window.FindName('btnBrowseDeviceNameSerialComputerNamesPath')
|
||||||
|
$State.Controls.txtDeviceNameSerialComputerNames = $window.FindName('txtDeviceNameSerialComputerNames')
|
||||||
|
$State.Controls.btnSaveDeviceNameSerialComputerNames = $window.FindName('btnSaveDeviceNameSerialComputerNames')
|
||||||
$State.Controls.chkVerbose = $window.FindName('chkVerbose')
|
$State.Controls.chkVerbose = $window.FindName('chkVerbose')
|
||||||
$State.Controls.chkCopyAutopilot = $window.FindName('chkCopyAutopilot')
|
$State.Controls.chkCopyAutopilot = $window.FindName('chkCopyAutopilot')
|
||||||
$State.Controls.chkCopyUnattend = $window.FindName('chkCopyUnattend')
|
$State.Controls.chkCopyUnattend = $window.FindName('chkCopyUnattend')
|
||||||
$State.Controls.chkCopyPPKG = $window.FindName('chkCopyPPKG')
|
$State.Controls.chkCopyPPKG = $window.FindName('chkCopyPPKG')
|
||||||
$State.Controls.chkCleanupAppsISO = $window.FindName('chkCleanupAppsISO')
|
$State.Controls.chkCleanupAppsISO = $window.FindName('chkCleanupAppsISO')
|
||||||
$State.Controls.chkCleanupCaptureISO = $window.FindName('chkCleanupCaptureISO')
|
|
||||||
$State.Controls.chkCleanupDeployISO = $window.FindName('chkCleanupDeployISO')
|
$State.Controls.chkCleanupDeployISO = $window.FindName('chkCleanupDeployISO')
|
||||||
$State.Controls.chkCleanupDrivers = $window.FindName('chkCleanupDrivers')
|
$State.Controls.chkCleanupDrivers = $window.FindName('chkCleanupDrivers')
|
||||||
$State.Controls.chkRemoveFFU = $window.FindName('chkRemoveFFU')
|
$State.Controls.chkRemoveFFU = $window.FindName('chkRemoveFFU')
|
||||||
@@ -159,6 +273,7 @@ function Initialize-UIControls {
|
|||||||
$State.Controls.chkUpdatePreviewCU = $window.FindName('chkUpdatePreviewCU')
|
$State.Controls.chkUpdatePreviewCU = $window.FindName('chkUpdatePreviewCU')
|
||||||
$State.Controls.txtApplicationPath = $window.FindName('txtApplicationPath')
|
$State.Controls.txtApplicationPath = $window.FindName('txtApplicationPath')
|
||||||
$State.Controls.txtAppListJsonPath = $window.FindName('txtAppListJsonPath')
|
$State.Controls.txtAppListJsonPath = $window.FindName('txtAppListJsonPath')
|
||||||
|
$State.Controls.txtUserAppListPath = $window.FindName('txtUserAppListPath')
|
||||||
$State.Controls.chkInstallDrivers = $window.FindName('chkInstallDrivers')
|
$State.Controls.chkInstallDrivers = $window.FindName('chkInstallDrivers')
|
||||||
$State.Controls.chkCopyDrivers = $window.FindName('chkCopyDrivers')
|
$State.Controls.chkCopyDrivers = $window.FindName('chkCopyDrivers')
|
||||||
$State.Controls.chkCompressDriversToWIM = $window.FindName('chkCompressDriversToWIM')
|
$State.Controls.chkCompressDriversToWIM = $window.FindName('chkCompressDriversToWIM')
|
||||||
@@ -180,11 +295,59 @@ function Initialize-UIControls {
|
|||||||
$State.Controls.btnRestoreDefaults = $window.FindName('btnRestoreDefaults')
|
$State.Controls.btnRestoreDefaults = $window.FindName('btnRestoreDefaults')
|
||||||
$State.Controls.btnBuildConfig = $window.FindName('btnBuildConfig')
|
$State.Controls.btnBuildConfig = $window.FindName('btnBuildConfig')
|
||||||
|
|
||||||
# Monitor Tab
|
# Home page
|
||||||
$State.Controls.MainTabControl = $window.FindName('MainTabControl')
|
$State.Controls.txtHomeCurrentBuildValue = $window.FindName('txtHomeCurrentBuildValue')
|
||||||
$State.Controls.MonitorTab = $window.FindName('MonitorTab')
|
$State.Controls.txtHomeLatestReleaseValue = $window.FindName('txtHomeLatestReleaseValue')
|
||||||
|
$State.Controls.txtHomeReleaseStatusValue = $window.FindName('txtHomeReleaseStatusValue')
|
||||||
|
$State.Controls.spHomeReleaseNotesSections = $window.FindName('spHomeReleaseNotesSections')
|
||||||
|
$State.Controls.ellipseHomeDiskSpaceStatus = $window.FindName('ellipseHomeDiskSpaceStatus')
|
||||||
|
$State.Controls.txtHomeDiskSpaceStatusValue = $window.FindName('txtHomeDiskSpaceStatusValue')
|
||||||
|
$State.Controls.ellipseHomeHyperVStatus = $window.FindName('ellipseHomeHyperVStatus')
|
||||||
|
$State.Controls.txtHomeHyperVStatusValue = $window.FindName('txtHomeHyperVStatusValue')
|
||||||
|
$State.Controls.txtHomeDiscussionsStatusValue = $window.FindName('txtHomeDiscussionsStatusValue')
|
||||||
|
$State.Controls.tbDiscussion1 = $window.FindName('tbDiscussion1')
|
||||||
|
$State.Controls.linkDiscussion1 = $window.FindName('linkDiscussion1')
|
||||||
|
$State.Controls.runDiscussion1 = $window.FindName('runDiscussion1')
|
||||||
|
$State.Controls.tbDiscussion2 = $window.FindName('tbDiscussion2')
|
||||||
|
$State.Controls.linkDiscussion2 = $window.FindName('linkDiscussion2')
|
||||||
|
$State.Controls.runDiscussion2 = $window.FindName('runDiscussion2')
|
||||||
|
$State.Controls.tbDiscussion3 = $window.FindName('tbDiscussion3')
|
||||||
|
$State.Controls.linkDiscussion3 = $window.FindName('linkDiscussion3')
|
||||||
|
$State.Controls.runDiscussion3 = $window.FindName('runDiscussion3')
|
||||||
|
$State.Controls.tbDiscussion4 = $window.FindName('tbDiscussion4')
|
||||||
|
$State.Controls.linkDiscussion4 = $window.FindName('linkDiscussion4')
|
||||||
|
$State.Controls.runDiscussion4 = $window.FindName('runDiscussion4')
|
||||||
|
$State.Controls.tbDiscussion5 = $window.FindName('tbDiscussion5')
|
||||||
|
$State.Controls.linkDiscussion5 = $window.FindName('linkDiscussion5')
|
||||||
|
$State.Controls.runDiscussion5 = $window.FindName('runDiscussion5')
|
||||||
|
$State.Controls.tbDiscussionsLink = $window.FindName('tbDiscussionsLink')
|
||||||
|
$State.Controls.linkDiscussions = $window.FindName('linkDiscussions')
|
||||||
|
|
||||||
|
# Settings page
|
||||||
|
$State.Controls.cmbThemeMode = $window.FindName('cmbThemeMode')
|
||||||
|
|
||||||
|
# Shared page shell
|
||||||
|
$State.Controls.txtPageTitle = $window.FindName('txtPageTitle')
|
||||||
|
|
||||||
|
# Navigation controls
|
||||||
|
$State.Controls.lstNavigation = $window.FindName('lstNavigation')
|
||||||
|
$State.Controls.lstNavSettings = $window.FindName('lstNavSettings')
|
||||||
$State.Controls.lstLogOutput = $window.FindName('lstLogOutput')
|
$State.Controls.lstLogOutput = $window.FindName('lstLogOutput')
|
||||||
|
|
||||||
|
# Content pages (for navigation visibility toggling)
|
||||||
|
$State.Controls.navigationPages = @(
|
||||||
|
$window.FindName('pageHome'),
|
||||||
|
$window.FindName('pageHyperV'),
|
||||||
|
$window.FindName('pageWindows'),
|
||||||
|
$window.FindName('pageUpdates'),
|
||||||
|
$window.FindName('pageApplications'),
|
||||||
|
$window.FindName('pageOffice'),
|
||||||
|
$window.FindName('pageDrivers'),
|
||||||
|
$window.FindName('pageBuild'),
|
||||||
|
$window.FindName('pageMonitor')
|
||||||
|
)
|
||||||
|
$State.Controls.pageSettings = $window.FindName('pageSettings')
|
||||||
|
|
||||||
# Initialize and bind the log data collection
|
# Initialize and bind the log data collection
|
||||||
$State.Data.logData = New-Object System.Collections.ObjectModel.ObservableCollection[string]
|
$State.Data.logData = New-Object System.Collections.ObjectModel.ObservableCollection[string]
|
||||||
$State.Controls.lstLogOutput.ItemsSource = $State.Data.logData
|
$State.Controls.lstLogOutput.ItemsSource = $State.Data.logData
|
||||||
@@ -205,19 +368,11 @@ function Initialize-VMSwitchData {
|
|||||||
$State.Controls.cmbVMSwitchName.Items.Add('Other') | Out-Null
|
$State.Controls.cmbVMSwitchName.Items.Add('Other') | Out-Null
|
||||||
if ($State.Controls.cmbVMSwitchName.Items.Count -gt 1) {
|
if ($State.Controls.cmbVMSwitchName.Items.Count -gt 1) {
|
||||||
$State.Controls.cmbVMSwitchName.SelectedIndex = 0
|
$State.Controls.cmbVMSwitchName.SelectedIndex = 0
|
||||||
$firstSwitch = $State.Controls.cmbVMSwitchName.SelectedItem
|
|
||||||
if ($null -ne $firstSwitch -and $State.Data.vmSwitchMap.ContainsKey($firstSwitch)) {
|
|
||||||
$State.Controls.txtVMHostIPAddress.Text = $State.Data.vmSwitchMap[$firstSwitch]
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$State.Controls.txtVMHostIPAddress.Text = $State.Defaults.generalDefaults.VMHostIPAddress # Use default if IP not found or key null
|
|
||||||
}
|
|
||||||
$State.Controls.txtCustomVMSwitchName.Visibility = 'Collapsed'
|
$State.Controls.txtCustomVMSwitchName.Visibility = 'Collapsed'
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$State.Controls.cmbVMSwitchName.SelectedItem = 'Other'
|
$State.Controls.cmbVMSwitchName.SelectedItem = 'Other'
|
||||||
$State.Controls.txtCustomVMSwitchName.Visibility = 'Visible'
|
$State.Controls.txtCustomVMSwitchName.Visibility = 'Visible'
|
||||||
$State.Controls.txtVMHostIPAddress.Text = $State.Defaults.generalDefaults.VMHostIPAddress # Use default
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,8 +388,6 @@ function Initialize-UIDefaults {
|
|||||||
$State.Controls.txtFFUDevPath.Text = $State.FFUDevelopmentPath
|
$State.Controls.txtFFUDevPath.Text = $State.FFUDevelopmentPath
|
||||||
$State.Controls.txtCustomFFUNameTemplate.Text = $State.Defaults.generalDefaults.CustomFFUNameTemplate
|
$State.Controls.txtCustomFFUNameTemplate.Text = $State.Defaults.generalDefaults.CustomFFUNameTemplate
|
||||||
$State.Controls.txtFFUCaptureLocation.Text = $State.Defaults.generalDefaults.FFUCaptureLocation
|
$State.Controls.txtFFUCaptureLocation.Text = $State.Defaults.generalDefaults.FFUCaptureLocation
|
||||||
$State.Controls.txtShareName.Text = $State.Defaults.generalDefaults.ShareName
|
|
||||||
$State.Controls.txtUsername.Text = $State.Defaults.generalDefaults.Username
|
|
||||||
$State.Controls.txtThreads.Text = $State.Defaults.generalDefaults.Threads
|
$State.Controls.txtThreads.Text = $State.Defaults.generalDefaults.Threads
|
||||||
$State.Controls.cmbBitsPriority.SelectedItem = $State.Defaults.generalDefaults.BitsPriority
|
$State.Controls.cmbBitsPriority.SelectedItem = $State.Defaults.generalDefaults.BitsPriority
|
||||||
$State.Controls.txtMaxUSBDrives.Text = $State.Defaults.generalDefaults.MaxUSBDrives
|
$State.Controls.txtMaxUSBDrives.Text = $State.Defaults.generalDefaults.MaxUSBDrives
|
||||||
@@ -244,7 +397,8 @@ function Initialize-UIDefaults {
|
|||||||
$State.Controls.chkOptimize.IsChecked = $State.Defaults.generalDefaults.Optimize
|
$State.Controls.chkOptimize.IsChecked = $State.Defaults.generalDefaults.Optimize
|
||||||
$State.Controls.chkAllowVHDXCaching.IsChecked = $State.Defaults.generalDefaults.AllowVHDXCaching
|
$State.Controls.chkAllowVHDXCaching.IsChecked = $State.Defaults.generalDefaults.AllowVHDXCaching
|
||||||
$State.Controls.chkInjectUnattend.IsChecked = $State.Defaults.generalDefaults.InjectUnattend
|
$State.Controls.chkInjectUnattend.IsChecked = $State.Defaults.generalDefaults.InjectUnattend
|
||||||
$State.Controls.chkCreateCaptureMedia.IsChecked = $State.Defaults.generalDefaults.CreateCaptureMedia
|
$State.Controls.txtUnattendX64FilePath.Text = $State.Defaults.generalDefaults.UnattendX64FilePath
|
||||||
|
$State.Controls.txtUnattendArm64FilePath.Text = $State.Defaults.generalDefaults.UnattendArm64FilePath
|
||||||
$State.Controls.chkCreateDeploymentMedia.IsChecked = $State.Defaults.generalDefaults.CreateDeploymentMedia
|
$State.Controls.chkCreateDeploymentMedia.IsChecked = $State.Defaults.generalDefaults.CreateDeploymentMedia
|
||||||
$State.Controls.chkAllowExternalHardDiskMedia.IsChecked = $State.Defaults.generalDefaults.AllowExternalHardDiskMedia
|
$State.Controls.chkAllowExternalHardDiskMedia.IsChecked = $State.Defaults.generalDefaults.AllowExternalHardDiskMedia
|
||||||
$State.Controls.chkPromptExternalHardDiskMedia.IsChecked = $State.Defaults.generalDefaults.PromptExternalHardDiskMedia
|
$State.Controls.chkPromptExternalHardDiskMedia.IsChecked = $State.Defaults.generalDefaults.PromptExternalHardDiskMedia
|
||||||
@@ -252,8 +406,22 @@ function Initialize-UIDefaults {
|
|||||||
$State.Controls.chkCopyAutopilot.IsChecked = $State.Defaults.generalDefaults.CopyAutopilot
|
$State.Controls.chkCopyAutopilot.IsChecked = $State.Defaults.generalDefaults.CopyAutopilot
|
||||||
$State.Controls.chkCopyUnattend.IsChecked = $State.Defaults.generalDefaults.CopyUnattend
|
$State.Controls.chkCopyUnattend.IsChecked = $State.Defaults.generalDefaults.CopyUnattend
|
||||||
$State.Controls.chkCopyPPKG.IsChecked = $State.Defaults.generalDefaults.CopyPPKG
|
$State.Controls.chkCopyPPKG.IsChecked = $State.Defaults.generalDefaults.CopyPPKG
|
||||||
|
$defaultDeviceNamingMode = if ($State.Defaults.generalDefaults.DeviceNamingMode -in @('None', 'Prompt', 'Template', 'Prefixes', 'SerialComputerNames')) {
|
||||||
|
$State.Defaults.generalDefaults.DeviceNamingMode
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
'None'
|
||||||
|
}
|
||||||
|
Set-DeviceNamingModeState -State $State -DisplayMode $defaultDeviceNamingMode -LoadedMode $null
|
||||||
|
$State.Controls.txtDeviceNameTemplate.Text = $State.Defaults.generalDefaults.DeviceNameTemplate
|
||||||
|
$State.Controls.txtDeviceNamePrefixesPath.Text = $State.Defaults.generalDefaults.DeviceNamePrefixesPath
|
||||||
|
$State.Controls.txtDeviceNamePrefixes.Text = ($State.Defaults.generalDefaults.DeviceNamePrefixes -join [System.Environment]::NewLine)
|
||||||
|
$State.Controls.txtDeviceNameSerialComputerNamesPath.Text = $State.Defaults.generalDefaults.DeviceNameSerialComputerNamesPath
|
||||||
|
$State.Controls.txtDeviceNameSerialComputerNames.Text = ($State.Defaults.generalDefaults.DeviceNameSerialComputerNames -join [System.Environment]::NewLine)
|
||||||
|
Import-DeviceNamePrefixesFromConfiguredPath -State $State
|
||||||
|
Import-SerialComputerNamesFromConfiguredPath -State $State
|
||||||
|
Update-DeviceNamingControls -State $State
|
||||||
$State.Controls.chkCleanupAppsISO.IsChecked = $State.Defaults.generalDefaults.CleanupAppsISO
|
$State.Controls.chkCleanupAppsISO.IsChecked = $State.Defaults.generalDefaults.CleanupAppsISO
|
||||||
$State.Controls.chkCleanupCaptureISO.IsChecked = $State.Defaults.generalDefaults.CleanupCaptureISO
|
|
||||||
$State.Controls.chkCleanupDeployISO.IsChecked = $State.Defaults.generalDefaults.CleanupDeployISO
|
$State.Controls.chkCleanupDeployISO.IsChecked = $State.Defaults.generalDefaults.CleanupDeployISO
|
||||||
$State.Controls.chkCleanupDrivers.IsChecked = $State.Defaults.generalDefaults.CleanupDrivers
|
$State.Controls.chkCleanupDrivers.IsChecked = $State.Defaults.generalDefaults.CleanupDrivers
|
||||||
$State.Controls.chkRemoveFFU.IsChecked = $State.Defaults.generalDefaults.RemoveFFU
|
$State.Controls.chkRemoveFFU.IsChecked = $State.Defaults.generalDefaults.RemoveFFU
|
||||||
@@ -261,7 +429,6 @@ function Initialize-UIDefaults {
|
|||||||
$State.Controls.chkRemoveUpdates.IsChecked = $State.Defaults.generalDefaults.RemoveUpdates
|
$State.Controls.chkRemoveUpdates.IsChecked = $State.Defaults.generalDefaults.RemoveUpdates
|
||||||
$State.Controls.chkRemoveDownloadedESD.IsChecked = $State.Defaults.generalDefaults.RemoveDownloadedESD
|
$State.Controls.chkRemoveDownloadedESD.IsChecked = $State.Defaults.generalDefaults.RemoveDownloadedESD
|
||||||
$State.Controls.chkVerbose.IsChecked = $State.Defaults.generalDefaults.Verbose
|
$State.Controls.chkVerbose.IsChecked = $State.Defaults.generalDefaults.Verbose
|
||||||
$State.Controls.usbSection.Visibility = if ($State.Controls.chkBuildUSBDriveEnable.IsChecked) { 'Visible' } else { 'Collapsed' }
|
|
||||||
$State.Controls.usbSelectionPanel.Visibility = if ($State.Controls.chkSelectSpecificUSBDrives.IsChecked) { 'Visible' } else { 'Collapsed' }
|
$State.Controls.usbSelectionPanel.Visibility = if ($State.Controls.chkSelectSpecificUSBDrives.IsChecked) { 'Visible' } else { 'Collapsed' }
|
||||||
$State.Controls.chkSelectSpecificUSBDrives.IsEnabled = $State.Controls.chkBuildUSBDriveEnable.IsChecked
|
$State.Controls.chkSelectSpecificUSBDrives.IsEnabled = $State.Controls.chkBuildUSBDriveEnable.IsChecked
|
||||||
$State.Controls.chkPromptExternalHardDiskMedia.IsEnabled = $State.Controls.chkAllowExternalHardDiskMedia.IsChecked
|
$State.Controls.chkPromptExternalHardDiskMedia.IsEnabled = $State.Controls.chkAllowExternalHardDiskMedia.IsChecked
|
||||||
@@ -270,7 +437,9 @@ function Initialize-UIDefaults {
|
|||||||
Update-BitsPrioritySetting -State $State
|
Update-BitsPrioritySetting -State $State
|
||||||
|
|
||||||
# Hyper-V Settings defaults from General Defaults
|
# Hyper-V Settings defaults from General Defaults
|
||||||
|
$State.Controls.chkEnableVMNetworking.IsChecked = $State.Defaults.generalDefaults.EnableVMNetworking
|
||||||
Initialize-VMSwitchData -State $State
|
Initialize-VMSwitchData -State $State
|
||||||
|
$State.Controls.spVMNetworkingSettings.IsEnabled = $true -eq $State.Controls.chkEnableVMNetworking.IsChecked
|
||||||
$State.Controls.txtDiskSize.Text = $State.Defaults.generalDefaults.DiskSizeGB
|
$State.Controls.txtDiskSize.Text = $State.Defaults.generalDefaults.DiskSizeGB
|
||||||
$State.Controls.txtMemory.Text = $State.Defaults.generalDefaults.MemoryGB
|
$State.Controls.txtMemory.Text = $State.Defaults.generalDefaults.MemoryGB
|
||||||
$State.Controls.txtProcessors.Text = $State.Defaults.generalDefaults.Processors
|
$State.Controls.txtProcessors.Text = $State.Defaults.generalDefaults.Processors
|
||||||
@@ -279,7 +448,12 @@ function Initialize-UIDefaults {
|
|||||||
$State.Controls.cmbLogicalSectorSize.SelectedItem = ($State.Controls.cmbLogicalSectorSize.Items | Where-Object { $_.Content -eq $State.Defaults.generalDefaults.LogicalSectorSize.ToString() })
|
$State.Controls.cmbLogicalSectorSize.SelectedItem = ($State.Controls.cmbLogicalSectorSize.Items | Where-Object { $_.Content -eq $State.Defaults.generalDefaults.LogicalSectorSize.ToString() })
|
||||||
|
|
||||||
# Populate Windows Release, Version, and SKU comboboxes
|
# Populate Windows Release, Version, and SKU comboboxes
|
||||||
Get-WindowsSettingsCombos -isoPath $State.Defaults.windowsSettingsDefaults.DefaultISOPath -State $State
|
# Initialize Windows settings combos based on media source mode
|
||||||
|
$initIsoPath = $State.Defaults.windowsSettingsDefaults.DefaultISOPath
|
||||||
|
if ($null -ne $State.Controls.rbProvideISO -and -not $State.Controls.rbProvideISO.IsChecked) {
|
||||||
|
$initIsoPath = ''
|
||||||
|
}
|
||||||
|
Get-WindowsSettingsCombos -isoPath $initIsoPath -State $State
|
||||||
|
|
||||||
# Windows Settings tab defaults
|
# Windows Settings tab defaults
|
||||||
$State.Controls.cmbWindowsLang.ItemsSource = $State.Defaults.windowsSettingsDefaults.AllowedLanguages
|
$State.Controls.cmbWindowsLang.ItemsSource = $State.Defaults.windowsSettingsDefaults.AllowedLanguages
|
||||||
@@ -305,6 +479,7 @@ function Initialize-UIDefaults {
|
|||||||
$State.Controls.chkInstallApps.IsChecked = $State.Defaults.generalDefaults.InstallApps
|
$State.Controls.chkInstallApps.IsChecked = $State.Defaults.generalDefaults.InstallApps
|
||||||
$State.Controls.txtApplicationPath.Text = $State.Defaults.generalDefaults.ApplicationPath
|
$State.Controls.txtApplicationPath.Text = $State.Defaults.generalDefaults.ApplicationPath
|
||||||
$State.Controls.txtAppListJsonPath.Text = $State.Defaults.generalDefaults.AppListJsonPath
|
$State.Controls.txtAppListJsonPath.Text = $State.Defaults.generalDefaults.AppListJsonPath
|
||||||
|
$State.Controls.txtUserAppListPath.Text = $State.Defaults.generalDefaults.UserAppListPath
|
||||||
$State.Controls.chkInstallWingetApps.IsChecked = $State.Defaults.generalDefaults.InstallWingetApps
|
$State.Controls.chkInstallWingetApps.IsChecked = $State.Defaults.generalDefaults.InstallWingetApps
|
||||||
$State.Controls.chkBringYourOwnApps.IsChecked = $State.Defaults.generalDefaults.BringYourOwnApps
|
$State.Controls.chkBringYourOwnApps.IsChecked = $State.Defaults.generalDefaults.BringYourOwnApps
|
||||||
|
|
||||||
@@ -345,23 +520,73 @@ function Initialize-UIDefaults {
|
|||||||
# Set initial state for InstallApps checkbox based on updates
|
# Set initial state for InstallApps checkbox based on updates
|
||||||
Update-InstallAppsState -State $State
|
Update-InstallAppsState -State $State
|
||||||
|
|
||||||
|
# Set default theme mode and disable if Fluent is not supported
|
||||||
|
if ($null -ne $State.Controls.cmbThemeMode) {
|
||||||
|
$State.Controls.cmbThemeMode.SelectedItem = "System"
|
||||||
|
if (-not $State.Flags.isFluentSupported) {
|
||||||
|
$State.Controls.cmbThemeMode.IsEnabled = $false
|
||||||
|
$State.Controls.cmbThemeMode.Tag = "Fluent theme requires PowerShell 7.5+ (.NET 9+). Best experience on PowerShell 7.6+ (.NET 10)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set default navigation selection to Home and initialize the shared page title
|
||||||
|
if ($null -ne $State.Controls.lstNavigation) {
|
||||||
|
$State.Controls.lstNavigation.SelectedIndex = 0
|
||||||
|
|
||||||
|
# Keep the shell header aligned with the selected navigation item on first render
|
||||||
|
if ($null -ne $State.Controls.txtPageTitle) {
|
||||||
|
$selectedNavigationItem = $State.Controls.lstNavigation.SelectedItem
|
||||||
|
if ($null -ne $selectedNavigationItem -and -not [string]::IsNullOrWhiteSpace([string]$selectedNavigationItem.Tag)) {
|
||||||
|
$State.Controls.txtPageTitle.Text = [string]$selectedNavigationItem.Tag
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$State.Controls.txtPageTitle.Text = 'Home'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Set initial state for Office panel visibility
|
# Set initial state for Office panel visibility
|
||||||
Update-OfficePanelVisibility -State $State
|
Update-OfficePanelVisibility -State $State
|
||||||
|
|
||||||
# Set initial state for Application panel visibility
|
# Set initial state for Application panel visibility
|
||||||
Update-ApplicationPanelVisibility -State $State
|
Update-ApplicationPanelVisibility -State $State
|
||||||
|
|
||||||
# Set initial state for BYO Apps copy button
|
# Set initial state for BYO Apps copy button
|
||||||
Update-CopyButtonState -State $State
|
Update-CopyButtonState -State $State
|
||||||
}
|
|
||||||
|
# Apply accent color to primary action button only (per Windows design guidance)
|
||||||
|
if ($State.Flags.isFluentSupported) {
|
||||||
|
try {
|
||||||
|
$State.Controls.btnRun = $State.Window.FindName('btnRun')
|
||||||
|
if ($null -ne $State.Controls.btnRun) {
|
||||||
|
# Use SetResourceReference for live accent color updates when user changes Windows theme
|
||||||
|
$State.Controls.btnRun.SetResourceReference(
|
||||||
|
[System.Windows.Controls.Control]::BackgroundProperty,
|
||||||
|
[System.Windows.SystemColors]::AccentColorBrushKey
|
||||||
|
)
|
||||||
|
$State.Controls.btnRun.Foreground = [System.Windows.Media.Brushes]::White
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Could not apply accent color to Build FFU button: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Initialize-DynamicUIElements {
|
function Initialize-DynamicUIElements {
|
||||||
param([PSCustomObject]$State)
|
param([PSCustomObject]$State)
|
||||||
WriteLog "Initializing dynamic UI elements (Grids, Columns)..."
|
WriteLog "Initializing dynamic UI elements (Grids, Columns)..."
|
||||||
|
|
||||||
|
# Get the Fluent base style for ListViewItem in GridView mode
|
||||||
|
# Must use GridViewItemContainerStyleKey (not the generic ListViewItem type key) because the
|
||||||
|
# generic Fluent ListViewItem style has a template without GridViewRowPresenter, which breaks
|
||||||
|
# column-based rendering and causes items to display their ToString() representation.
|
||||||
|
$listViewItemBaseStyle = $State.Window.TryFindResource([System.Windows.Controls.GridView]::GridViewItemContainerStyleKey)
|
||||||
|
|
||||||
# Driver Models ListView setup
|
# Driver Models ListView setup
|
||||||
# Set ListViewItem style to stretch content horizontally so cell templates fill the cell
|
# Set ListViewItem style to stretch content horizontally so cell templates fill the cell
|
||||||
$itemStyleDriverModels = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem])
|
$itemStyleDriverModels = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem])
|
||||||
|
if ($null -ne $listViewItemBaseStyle) { $itemStyleDriverModels.BasedOn = $listViewItemBaseStyle }
|
||||||
$itemStyleDriverModels.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)))
|
$itemStyleDriverModels.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)))
|
||||||
$State.Controls.lstDriverModels.ItemContainerStyle = $itemStyleDriverModels
|
$State.Controls.lstDriverModels.ItemContainerStyle = $itemStyleDriverModels
|
||||||
|
|
||||||
@@ -391,12 +616,16 @@ function Initialize-DynamicUIElements {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Keep driver model columns sized to the current visible content.
|
||||||
|
Enable-ListViewColumnAutoResize -ListView $State.Controls.lstDriverModels -FixedColumnIndexes @(0)
|
||||||
|
|
||||||
# Winget Search ListView setup
|
# Winget Search ListView setup
|
||||||
$wingetGridView = New-Object System.Windows.Controls.GridView
|
$wingetGridView = New-Object System.Windows.Controls.GridView
|
||||||
$State.Controls.lstWingetResults.View = $wingetGridView # Assign GridView to ListView first
|
$State.Controls.lstWingetResults.View = $wingetGridView # Assign GridView to ListView first
|
||||||
|
|
||||||
# Set ListViewItem style to stretch content horizontally so cell templates fill the cell
|
# Set ListViewItem style to stretch content horizontally so cell templates fill the cell
|
||||||
$itemStyleWingetResults = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem])
|
$itemStyleWingetResults = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem])
|
||||||
|
if ($null -ne $listViewItemBaseStyle) { $itemStyleWingetResults.BasedOn = $listViewItemBaseStyle }
|
||||||
$itemStyleWingetResults.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)))
|
$itemStyleWingetResults.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)))
|
||||||
$State.Controls.lstWingetResults.ItemContainerStyle = $itemStyleWingetResults
|
$State.Controls.lstWingetResults.ItemContainerStyle = $itemStyleWingetResults
|
||||||
|
|
||||||
@@ -444,9 +673,11 @@ function Initialize-DynamicUIElements {
|
|||||||
$binding.Mode = [System.Windows.Data.BindingMode]::TwoWay
|
$binding.Mode = [System.Windows.Data.BindingMode]::TwoWay
|
||||||
$comboBoxFactory.SetBinding([System.Windows.Controls.ComboBox]::TextProperty, $binding)
|
$comboBoxFactory.SetBinding([System.Windows.Controls.ComboBox]::TextProperty, $binding)
|
||||||
|
|
||||||
# Create a style to disable the ComboBox for 'msstore' source
|
# Create a style to disable the ComboBox for 'msstore' source, inheriting the Fluent base style
|
||||||
$comboBoxStyle = New-Object System.Windows.Style
|
$comboBoxStyle = New-Object System.Windows.Style
|
||||||
$comboBoxStyle.TargetType = [System.Windows.Controls.ComboBox]
|
$comboBoxStyle.TargetType = [System.Windows.Controls.ComboBox]
|
||||||
|
$comboBoxBaseStyle = $State.Window.TryFindResource([System.Windows.Controls.ComboBox])
|
||||||
|
if ($null -ne $comboBoxBaseStyle) { $comboBoxStyle.BasedOn = $comboBoxBaseStyle }
|
||||||
|
|
||||||
$dataTrigger = New-Object System.Windows.DataTrigger
|
$dataTrigger = New-Object System.Windows.DataTrigger
|
||||||
$dataTrigger.Binding = New-Object System.Windows.Data.Binding("Source")
|
$dataTrigger.Binding = New-Object System.Windows.Data.Binding("Source")
|
||||||
@@ -547,12 +778,16 @@ function Initialize-DynamicUIElements {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Keep Winget result columns sized to the current visible content.
|
||||||
|
Enable-ListViewColumnAutoResize -ListView $State.Controls.lstWingetResults -FixedColumnIndexes @(0)
|
||||||
|
|
||||||
# BYO Applications ListView setup
|
# BYO Applications ListView setup
|
||||||
$byoAppsGridView = New-Object System.Windows.Controls.GridView
|
$byoAppsGridView = New-Object System.Windows.Controls.GridView
|
||||||
$State.Controls.lstApplications.View = $byoAppsGridView
|
$State.Controls.lstApplications.View = $byoAppsGridView
|
||||||
|
|
||||||
# Set ListViewItem style to stretch content horizontally
|
# Set ListViewItem style to stretch content horizontally
|
||||||
$itemStyleBYOApps = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem])
|
$itemStyleBYOApps = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem])
|
||||||
|
if ($null -ne $listViewItemBaseStyle) { $itemStyleBYOApps.BasedOn = $listViewItemBaseStyle }
|
||||||
$itemStyleBYOApps.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)))
|
$itemStyleBYOApps.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)))
|
||||||
$State.Controls.lstApplications.ItemContainerStyle = $itemStyleBYOApps
|
$State.Controls.lstApplications.ItemContainerStyle = $itemStyleBYOApps
|
||||||
|
|
||||||
@@ -569,12 +804,16 @@ function Initialize-DynamicUIElements {
|
|||||||
Add-SortableColumn -gridView $byoAppsGridView -header "Ignore Exit Codes" -binding "IgnoreExitCodes" -width 120 -headerHorizontalAlignment Left
|
Add-SortableColumn -gridView $byoAppsGridView -header "Ignore Exit Codes" -binding "IgnoreExitCodes" -width 120 -headerHorizontalAlignment Left
|
||||||
Add-SortableColumn -gridView $byoAppsGridView -header "Copy Status" -binding "CopyStatus" -width 150 -headerHorizontalAlignment Left
|
Add-SortableColumn -gridView $byoAppsGridView -header "Copy Status" -binding "CopyStatus" -width 150 -headerHorizontalAlignment Left
|
||||||
|
|
||||||
|
# Keep BYO application columns sized to the current visible content.
|
||||||
|
Enable-ListViewColumnAutoResize -ListView $State.Controls.lstApplications -FixedColumnIndexes @(0)
|
||||||
|
|
||||||
# Apps Script Variables ListView setup
|
# Apps Script Variables ListView setup
|
||||||
# Bind ItemsSource to the data list
|
# Bind ItemsSource to the data list
|
||||||
$State.Controls.lstAppsScriptVariables.ItemsSource = $State.Data.appsScriptVariablesDataList.ToArray()
|
$State.Controls.lstAppsScriptVariables.ItemsSource = $State.Data.appsScriptVariablesDataList.ToArray()
|
||||||
|
|
||||||
# Set ListViewItem style to stretch content horizontally so cell templates fill the cell
|
# Set ListViewItem style to stretch content horizontally so cell templates fill the cell
|
||||||
$itemStyleAppsScriptVars = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem])
|
$itemStyleAppsScriptVars = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem])
|
||||||
|
if ($null -ne $listViewItemBaseStyle) { $itemStyleAppsScriptVars.BasedOn = $listViewItemBaseStyle }
|
||||||
$itemStyleAppsScriptVars.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)))
|
$itemStyleAppsScriptVars.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)))
|
||||||
$State.Controls.lstAppsScriptVariables.ItemContainerStyle = $itemStyleAppsScriptVars
|
$State.Controls.lstAppsScriptVariables.ItemContainerStyle = $itemStyleAppsScriptVars
|
||||||
|
|
||||||
@@ -621,6 +860,9 @@ function Initialize-DynamicUIElements {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Keep apps script variable columns sized to the current visible content.
|
||||||
|
Enable-ListViewColumnAutoResize -ListView $State.Controls.lstAppsScriptVariables -FixedColumnIndexes @(0)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
WriteLog "Warning: lstAppsScriptVariables.View is not a GridView. Selectable column not added, and sorting cannot be enabled."
|
WriteLog "Warning: lstAppsScriptVariables.View is not a GridView. Selectable column not added, and sorting cannot be enabled."
|
||||||
@@ -637,6 +879,7 @@ function Initialize-DynamicUIElements {
|
|||||||
# USB Drives ListView setup
|
# USB Drives ListView setup
|
||||||
# Set ListViewItem style to stretch content horizontally so cell templates fill the cell
|
# Set ListViewItem style to stretch content horizontally so cell templates fill the cell
|
||||||
$itemStyleUSBDrives = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem])
|
$itemStyleUSBDrives = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem])
|
||||||
|
if ($null -ne $listViewItemBaseStyle) { $itemStyleUSBDrives.BasedOn = $listViewItemBaseStyle }
|
||||||
$itemStyleUSBDrives.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)))
|
$itemStyleUSBDrives.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)))
|
||||||
$State.Controls.lstUSBDrives.ItemContainerStyle = $itemStyleUSBDrives
|
$State.Controls.lstUSBDrives.ItemContainerStyle = $itemStyleUSBDrives
|
||||||
|
|
||||||
@@ -693,6 +936,9 @@ function Initialize-DynamicUIElements {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Keep USB drive columns sized to the current visible content.
|
||||||
|
Enable-ListViewColumnAutoResize -ListView $State.Controls.lstUSBDrives -FixedColumnIndexes @(0)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
WriteLog "Warning: lstUSBDrives.View is not a GridView. Selectable column not added, and sorting cannot be enabled."
|
WriteLog "Warning: lstUSBDrives.View is not a GridView. Selectable column not added, and sorting cannot be enabled."
|
||||||
@@ -700,6 +946,7 @@ function Initialize-DynamicUIElements {
|
|||||||
|
|
||||||
# Additional FFUs ListView setup
|
# Additional FFUs ListView setup
|
||||||
$itemStyleAdditionalFFUs = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem])
|
$itemStyleAdditionalFFUs = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem])
|
||||||
|
if ($null -ne $listViewItemBaseStyle) { $itemStyleAdditionalFFUs.BasedOn = $listViewItemBaseStyle }
|
||||||
$itemStyleAdditionalFFUs.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)))
|
$itemStyleAdditionalFFUs.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)))
|
||||||
$State.Controls.lstAdditionalFFUs.ItemContainerStyle = $itemStyleAdditionalFFUs
|
$State.Controls.lstAdditionalFFUs.ItemContainerStyle = $itemStyleAdditionalFFUs
|
||||||
|
|
||||||
@@ -738,6 +985,9 @@ function Initialize-DynamicUIElements {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Keep additional FFU columns sized to the current visible content.
|
||||||
|
Enable-ListViewColumnAutoResize -ListView $State.Controls.lstAdditionalFFUs -FixedColumnIndexes @(0)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
WriteLog "Warning: lstAdditionalFFUs.View is not a GridView. Selectable column not added, and sorting cannot be enabled."
|
WriteLog "Warning: lstAdditionalFFUs.View is not a GridView. Selectable column not added, and sorting cannot be enabled."
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ function Update-ListViewPriorities {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$ListView.Items.Refresh()
|
$ListView.Items.Refresh()
|
||||||
|
Request-ListViewColumnAutoResize -ListView $ListView
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to move selected item to the top
|
# Function to move selected item to the top
|
||||||
@@ -133,6 +134,7 @@ function Update-ListViewItemStatus {
|
|||||||
if ($null -ne $itemToUpdate) {
|
if ($null -ne $itemToUpdate) {
|
||||||
$itemToUpdate.$StatusProperty = $StatusValue
|
$itemToUpdate.$StatusProperty = $StatusValue
|
||||||
$ListView.Items.Refresh() # Refresh the view to show the change
|
$ListView.Items.Refresh() # Refresh the view to show the change
|
||||||
|
Request-ListViewColumnAutoResize -ListView $ListView
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
# Log if item not found (for debugging)
|
# Log if item not found (for debugging)
|
||||||
@@ -343,6 +345,7 @@ function Add-SelectableGridViewColumn {
|
|||||||
# Create the "Select All" CheckBox for the header
|
# Create the "Select All" CheckBox for the header
|
||||||
$headerCheckBox = New-Object System.Windows.Controls.CheckBox
|
$headerCheckBox = New-Object System.Windows.Controls.CheckBox
|
||||||
$headerCheckBox.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Center
|
$headerCheckBox.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Center
|
||||||
|
$headerCheckBox.VerticalAlignment = [System.Windows.VerticalAlignment]::Center
|
||||||
|
|
||||||
# Store header metadata, including whether select-all should only affect visible rows.
|
# Store header metadata, including whether select-all should only affect visible rows.
|
||||||
$headerTagObject = [PSCustomObject]@{
|
$headerTagObject = [PSCustomObject]@{
|
||||||
@@ -412,11 +415,33 @@ function Add-SelectableGridViewColumn {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Wrap the header checkbox in a stretched container so it centers the same way as row cells.
|
||||||
|
# Apply a small left inset to mirror the Fluent ListViewItem content padding used by data rows.
|
||||||
|
$headerBorder = New-Object System.Windows.Controls.Border
|
||||||
|
$headerBorder.Padding = New-Object System.Windows.Thickness(12, 0, 0, 0)
|
||||||
|
$headerBorder.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Stretch
|
||||||
|
$headerBorder.VerticalAlignment = [System.Windows.VerticalAlignment]::Stretch
|
||||||
|
|
||||||
|
$headerGrid = New-Object System.Windows.Controls.Grid
|
||||||
|
$headerGrid.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Stretch
|
||||||
|
$headerGrid.VerticalAlignment = [System.Windows.VerticalAlignment]::Stretch
|
||||||
|
$headerGrid.Children.Add($headerCheckBox) | Out-Null
|
||||||
|
$headerBorder.Child = $headerGrid
|
||||||
|
|
||||||
|
# Use an explicit GridViewColumnHeader so we can remove the default header padding
|
||||||
|
# and control the checkbox alignment explicitly.
|
||||||
|
$selectableHeader = New-Object System.Windows.Controls.GridViewColumnHeader
|
||||||
|
$selectableHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Stretch
|
||||||
|
$selectableHeader.VerticalContentAlignment = [System.Windows.VerticalAlignment]::Stretch
|
||||||
|
$selectableHeader.Padding = New-Object System.Windows.Thickness(0)
|
||||||
|
$selectableHeader.Margin = New-Object System.Windows.Thickness(0)
|
||||||
|
$selectableHeader.Content = $headerBorder
|
||||||
|
|
||||||
$State.Controls[$HeaderCheckBoxKeyName] = $headerCheckBox
|
$State.Controls[$HeaderCheckBoxKeyName] = $headerCheckBox
|
||||||
WriteLog "Add-SelectableGridViewColumn: Stored header checkbox in State.Controls with key '$HeaderCheckBoxKeyName'."
|
WriteLog "Add-SelectableGridViewColumn: Stored header checkbox in State.Controls with key '$HeaderCheckBoxKeyName'."
|
||||||
|
|
||||||
$selectableColumn = New-Object System.Windows.Controls.GridViewColumn
|
$selectableColumn = New-Object System.Windows.Controls.GridViewColumn
|
||||||
$selectableColumn.Header = $headerCheckBox
|
$selectableColumn.Header = $selectableHeader
|
||||||
$selectableColumn.Width = $ColumnWidth
|
$selectableColumn.Width = $ColumnWidth
|
||||||
|
|
||||||
$cellTemplate = New-Object System.Windows.DataTemplate
|
$cellTemplate = New-Object System.Windows.DataTemplate
|
||||||
@@ -471,6 +496,209 @@ function Add-SelectableGridViewColumn {
|
|||||||
WriteLog "Add-SelectableGridViewColumn: Successfully added selectable column to '$($ListView.Name)'."
|
WriteLog "Add-SelectableGridViewColumn: Successfully added selectable column to '$($ListView.Name)'."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Function to request a deferred GridView column auto-size pass
|
||||||
|
function Request-ListViewColumnAutoResize {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[System.Windows.Controls.ListView]$ListView
|
||||||
|
)
|
||||||
|
|
||||||
|
# Skip startup calls until the visual tree has finished loading.
|
||||||
|
if (-not $ListView.IsLoaded) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure the ListView has registered auto-resize metadata before scheduling work.
|
||||||
|
$autoResizeStateKey = 'FFU.ListViewColumnAutoResizeState'
|
||||||
|
if (-not $ListView.Resources.Contains($autoResizeStateKey)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not ($ListView.View -is [System.Windows.Controls.GridView])) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$autoResizeState = $ListView.Resources[$autoResizeStateKey]
|
||||||
|
if ($autoResizeState.ResizePending) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$autoResizeState.ResizePending = $true
|
||||||
|
$previousErrorActionPreference = $ErrorActionPreference
|
||||||
|
|
||||||
|
try {
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$gridView = [System.Windows.Controls.GridView]$ListView.View
|
||||||
|
$fixedColumnIndexes = @($autoResizeState.FixedColumnIndexes)
|
||||||
|
$visibleItems = [System.Collections.Generic.List[object]]::new()
|
||||||
|
|
||||||
|
if ($null -ne $ListView.ItemsSource) {
|
||||||
|
$collectionView = [System.Windows.Data.CollectionViewSource]::GetDefaultView($ListView.ItemsSource)
|
||||||
|
if ($null -ne $collectionView) {
|
||||||
|
foreach ($visibleItem in $collectionView) {
|
||||||
|
$visibleItems.Add($visibleItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
foreach ($visibleItem in $ListView.Items) {
|
||||||
|
$visibleItems.Add($visibleItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ListView.UpdateLayout()
|
||||||
|
|
||||||
|
$columnIndex = 0
|
||||||
|
foreach ($column in $gridView.Columns) {
|
||||||
|
if ($fixedColumnIndexes -contains $columnIndex) {
|
||||||
|
$columnIndex++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -eq $column) {
|
||||||
|
$columnIndex++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$headerText = ""
|
||||||
|
$propertyName = $null
|
||||||
|
|
||||||
|
if ($null -ne $column.DisplayMemberBinding -and $null -ne $column.DisplayMemberBinding.Path) {
|
||||||
|
$propertyName = [string]$column.DisplayMemberBinding.Path.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($column.Header -is [System.Windows.Controls.GridViewColumnHeader]) {
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace([string]$column.Header.Content)) {
|
||||||
|
$headerText = [string]$column.Header.Content
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($propertyName) -and -not [string]::IsNullOrWhiteSpace([string]$column.Header.Tag)) {
|
||||||
|
$propertyName = [string]$column.Header.Tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif (-not [string]::IsNullOrWhiteSpace([string]$column.Header)) {
|
||||||
|
$headerText = [string]$column.Header
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($headerText)) {
|
||||||
|
$headerText = $propertyName
|
||||||
|
}
|
||||||
|
|
||||||
|
$headerMeasureBlock = New-Object System.Windows.Controls.TextBlock
|
||||||
|
$headerMeasureBlock.Text = if ([string]::IsNullOrWhiteSpace($headerText)) { ' ' } else { $headerText }
|
||||||
|
$headerMeasureBlock.FontFamily = $ListView.FontFamily
|
||||||
|
$headerMeasureBlock.FontSize = $ListView.FontSize
|
||||||
|
$headerMeasureBlock.FontStyle = $ListView.FontStyle
|
||||||
|
$headerMeasureBlock.FontWeight = $ListView.FontWeight
|
||||||
|
$headerMeasureBlock.FontStretch = $ListView.FontStretch
|
||||||
|
$headerMeasureBlock.Measure([System.Windows.Size]::new([double]::PositiveInfinity, [double]::PositiveInfinity))
|
||||||
|
|
||||||
|
$calculatedWidth = [math]::Ceiling($headerMeasureBlock.DesiredSize.Width + 36)
|
||||||
|
|
||||||
|
foreach ($item in $visibleItems) {
|
||||||
|
if ($null -eq $item -or [string]::IsNullOrWhiteSpace($propertyName)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$itemProperty = $null
|
||||||
|
if ($null -ne $item.PSObject -and $null -ne $item.PSObject.Properties) {
|
||||||
|
$matchedProperties = $item.PSObject.Properties.Match($propertyName)
|
||||||
|
if ($null -ne $matchedProperties -and $matchedProperties.Count -gt 0) {
|
||||||
|
$itemProperty = $matchedProperties | Select-Object -First 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -eq $itemProperty) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$itemText = [string]$itemProperty.Value
|
||||||
|
$extraWidth = 28
|
||||||
|
|
||||||
|
switch ($propertyName) {
|
||||||
|
'Architecture' {
|
||||||
|
$extraWidth = 52
|
||||||
|
}
|
||||||
|
'AdditionalExitCodes' {
|
||||||
|
$extraWidth = 44
|
||||||
|
}
|
||||||
|
'IgnoreNonZeroExitCodes' {
|
||||||
|
$itemText = ' '
|
||||||
|
$extraWidth = 48
|
||||||
|
}
|
||||||
|
'IgnoreExitCodes' {
|
||||||
|
$extraWidth = 28
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$itemMeasureBlock = New-Object System.Windows.Controls.TextBlock
|
||||||
|
$itemMeasureBlock.Text = if ([string]::IsNullOrWhiteSpace($itemText)) { ' ' } else { $itemText }
|
||||||
|
$itemMeasureBlock.FontFamily = $ListView.FontFamily
|
||||||
|
$itemMeasureBlock.FontSize = $ListView.FontSize
|
||||||
|
$itemMeasureBlock.FontStyle = $ListView.FontStyle
|
||||||
|
$itemMeasureBlock.FontWeight = $ListView.FontWeight
|
||||||
|
$itemMeasureBlock.FontStretch = $ListView.FontStretch
|
||||||
|
$itemMeasureBlock.Measure([System.Windows.Size]::new([double]::PositiveInfinity, [double]::PositiveInfinity))
|
||||||
|
|
||||||
|
$itemWidth = [math]::Ceiling($itemMeasureBlock.DesiredSize.Width + $extraWidth)
|
||||||
|
if ($itemWidth -gt $calculatedWidth) {
|
||||||
|
$calculatedWidth = $itemWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($propertyName -eq 'IgnoreNonZeroExitCodes') {
|
||||||
|
$calculatedWidth = [math]::Max($calculatedWidth, 120)
|
||||||
|
}
|
||||||
|
|
||||||
|
$column.Width = [math]::Max($calculatedWidth, 40)
|
||||||
|
$columnIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Request-ListViewColumnAutoResize: Failed for '$($ListView.Name)': $($_.Exception.Message)"
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($_.InvocationInfo.PositionMessage)) {
|
||||||
|
WriteLog $_.InvocationInfo.PositionMessage
|
||||||
|
}
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($_.ScriptStackTrace)) {
|
||||||
|
WriteLog $_.ScriptStackTrace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$ErrorActionPreference = $previousErrorActionPreference
|
||||||
|
$autoResizeState.ResizePending = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to enable reusable auto-resizing for GridView-backed ListViews
|
||||||
|
function Enable-ListViewColumnAutoResize {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[System.Windows.Controls.ListView]$ListView,
|
||||||
|
|
||||||
|
[int[]]$FixedColumnIndexes = @()
|
||||||
|
)
|
||||||
|
|
||||||
|
$autoResizeStateKey = 'FFU.ListViewColumnAutoResizeState'
|
||||||
|
|
||||||
|
# Only GridView-backed lists can participate in column auto-sizing.
|
||||||
|
if (-not ($ListView.View -is [System.Windows.Controls.GridView])) {
|
||||||
|
WriteLog "Enable-ListViewColumnAutoResize: ListView '$($ListView.Name)' is not using a GridView. Skipping registration."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ListView.Resources.Contains($autoResizeStateKey)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$autoResizeState = [PSCustomObject]@{
|
||||||
|
FixedColumnIndexes = @($FixedColumnIndexes)
|
||||||
|
ResizePending = $false
|
||||||
|
}
|
||||||
|
$ListView.Resources[$autoResizeStateKey] = $autoResizeState
|
||||||
|
}
|
||||||
|
|
||||||
# Function to update the IsChecked state of a "Select All" header CheckBox
|
# Function to update the IsChecked state of a "Select All" header CheckBox
|
||||||
function Update-SelectAllHeaderCheckBoxState {
|
function Update-SelectAllHeaderCheckBoxState {
|
||||||
param(
|
param(
|
||||||
@@ -550,6 +778,7 @@ function Invoke-ListViewItemToggle {
|
|||||||
# Toggle the IsSelected property
|
# Toggle the IsSelected property
|
||||||
$selectedItem.IsSelected = -not $selectedItem.IsSelected
|
$selectedItem.IsSelected = -not $selectedItem.IsSelected
|
||||||
$ListView.Items.Refresh()
|
$ListView.Items.Refresh()
|
||||||
|
Request-ListViewColumnAutoResize -ListView $ListView
|
||||||
|
|
||||||
# Update the 'Select All' header checkbox state
|
# Update the 'Select All' header checkbox state
|
||||||
$headerChk = $State.Controls[$HeaderCheckBoxKeyName]
|
$headerChk = $State.Controls[$HeaderCheckBoxKeyName]
|
||||||
@@ -720,6 +949,8 @@ function Invoke-ListViewSort {
|
|||||||
$newView.Filter = $existingFilter
|
$newView.Filter = $existingFilter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Request-ListViewColumnAutoResize -ListView $listView
|
||||||
}
|
}
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
@@ -942,6 +1173,9 @@ function Invoke-BrowseAction {
|
|||||||
if (-not [string]::IsNullOrWhiteSpace($InitialDirectory)) {
|
if (-not [string]::IsNullOrWhiteSpace($InitialDirectory)) {
|
||||||
$dialog.InitialDirectory = $InitialDirectory
|
$dialog.InitialDirectory = $InitialDirectory
|
||||||
}
|
}
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($FileName)) {
|
||||||
|
$dialog.FileName = $FileName
|
||||||
|
}
|
||||||
if ($dialog.ShowDialog()) {
|
if ($dialog.ShowDialog()) {
|
||||||
return $dialog.FileName
|
return $dialog.FileName
|
||||||
}
|
}
|
||||||
@@ -1020,6 +1254,7 @@ function Clear-ListViewContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$ListViewControl.Items.Refresh()
|
$ListViewControl.Items.Refresh()
|
||||||
|
Request-ListViewColumnAutoResize -ListView $ListViewControl
|
||||||
|
|
||||||
# Clear any specified textboxes
|
# Clear any specified textboxes
|
||||||
if ($null -ne $TextBoxesToClear) {
|
if ($null -ne $TextBoxesToClear) {
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ function Search-WingetApps {
|
|||||||
|
|
||||||
# Update the ListView's ItemsSource using the passed-in State object
|
# Update the ListView's ItemsSource using the passed-in State object
|
||||||
$State.Controls.lstWingetResults.ItemsSource = $finalAppList.ToArray()
|
$State.Controls.lstWingetResults.ItemsSource = $finalAppList.ToArray()
|
||||||
|
Request-ListViewColumnAutoResize -ListView $State.Controls.lstWingetResults
|
||||||
|
|
||||||
# Update status text
|
# Update status text
|
||||||
$statusText = ""
|
$statusText = ""
|
||||||
@@ -108,20 +109,28 @@ function Save-WingetList {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Default the save dialog to the configured Winget app list path.
|
||||||
|
$currentPath = $State.Controls.txtAppListJsonPath.Text
|
||||||
|
$initialDirectory = if (-not [string]::IsNullOrWhiteSpace($currentPath)) { Split-Path -Path $currentPath -Parent } else { $State.Controls.txtApplicationPath.Text }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($initialDirectory) -or -not (Test-Path -Path $initialDirectory -PathType Container)) {
|
||||||
|
$initialDirectory = $State.Controls.txtApplicationPath.Text
|
||||||
|
}
|
||||||
|
$fileName = if (-not [string]::IsNullOrWhiteSpace($currentPath)) { Split-Path -Path $currentPath -Leaf } else { "AppList.json" }
|
||||||
|
|
||||||
$sfd = New-Object System.Windows.Forms.SaveFileDialog
|
$sfd = New-Object System.Windows.Forms.SaveFileDialog
|
||||||
$sfd.Filter = "JSON files (*.json)|*.json"
|
$sfd.Filter = "JSON files (*.json)|*.json"
|
||||||
$sfd.Title = "Save App List"
|
$sfd.Title = "Save Winget App List"
|
||||||
# Correctly get the path from the UI control via the State object
|
$sfd.InitialDirectory = $initialDirectory
|
||||||
$sfd.InitialDirectory = $State.Controls.txtApplicationPath.Text
|
$sfd.FileName = $fileName
|
||||||
$sfd.FileName = "AppList.json"
|
|
||||||
|
|
||||||
if ($sfd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
|
if ($sfd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
|
||||||
$appList | ConvertTo-Json -Depth 10 | Set-Content $sfd.FileName -Encoding UTF8
|
$appList | ConvertTo-Json -Depth 10 | Set-Content $sfd.FileName -Encoding UTF8
|
||||||
[System.Windows.MessageBox]::Show("App list saved successfully.", "Success", "OK", "Information")
|
$State.Controls.txtAppListJsonPath.Text = $sfd.FileName
|
||||||
|
[System.Windows.MessageBox]::Show("Winget app list saved successfully.", "Success", "OK", "Information")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
[System.Windows.MessageBox]::Show("Error saving app list: $_", "Error", "OK", "Error")
|
[System.Windows.MessageBox]::Show("Error saving Winget app list: $_", "Error", "OK", "Error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,11 +141,17 @@ function Import-WingetList {
|
|||||||
[psobject]$State
|
[psobject]$State
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
|
# Default the import dialog to the configured Winget app list path.
|
||||||
|
$currentPath = $State.Controls.txtAppListJsonPath.Text
|
||||||
|
$initialDirectory = if (-not [string]::IsNullOrWhiteSpace($currentPath)) { Split-Path -Path $currentPath -Parent } else { $State.Controls.txtApplicationPath.Text }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($initialDirectory) -or -not (Test-Path -Path $initialDirectory -PathType Container)) {
|
||||||
|
$initialDirectory = $State.Controls.txtApplicationPath.Text
|
||||||
|
}
|
||||||
|
|
||||||
$ofd = New-Object System.Windows.Forms.OpenFileDialog
|
$ofd = New-Object System.Windows.Forms.OpenFileDialog
|
||||||
$ofd.Filter = "JSON files (*.json)|*.json"
|
$ofd.Filter = "JSON files (*.json)|*.json"
|
||||||
$ofd.Title = "Import App List"
|
$ofd.Title = "Import Winget App List"
|
||||||
# Correctly get the path from the UI control via the State object
|
$ofd.InitialDirectory = $initialDirectory
|
||||||
$ofd.InitialDirectory = $State.Controls.txtApplicationPath.Text
|
|
||||||
|
|
||||||
if ($ofd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
|
if ($ofd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
|
||||||
$importedAppsData = Get-Content $ofd.FileName -Raw | ConvertFrom-Json
|
$importedAppsData = Get-Content $ofd.FileName -Raw | ConvertFrom-Json
|
||||||
@@ -144,16 +159,16 @@ function Import-WingetList {
|
|||||||
$newAppListForItemsSource = [System.Collections.Generic.List[object]]::new()
|
$newAppListForItemsSource = [System.Collections.Generic.List[object]]::new()
|
||||||
|
|
||||||
if ($null -ne $importedAppsData.apps) {
|
if ($null -ne $importedAppsData.apps) {
|
||||||
# Get default architecture from the UI for fallback
|
# Get default architecture from the UI for fallback.
|
||||||
$defaultArch = $State.Controls.cmbWindowsArch.SelectedItem
|
$defaultArch = $State.Controls.cmbWindowsArch.SelectedItem
|
||||||
|
|
||||||
foreach ($appInfo in $importedAppsData.apps) {
|
foreach ($appInfo in $importedAppsData.apps) {
|
||||||
$arch = if ($appInfo.source -eq 'msstore') { 'NA' } else { if ($appInfo.PSObject.Properties['architecture']) { $appInfo.architecture } else { $defaultArch } }
|
$arch = if ($appInfo.source -eq 'msstore') { 'NA' } else { if ($appInfo.PSObject.Properties['architecture']) { $appInfo.architecture } else { $defaultArch } }
|
||||||
$newAppListForItemsSource.Add([PSCustomObject]@{
|
$newAppListForItemsSource.Add([PSCustomObject]@{
|
||||||
IsSelected = $true # Imported apps are marked as selected
|
IsSelected = $true
|
||||||
Name = $appInfo.name
|
Name = $appInfo.name
|
||||||
Id = $appInfo.id
|
Id = $appInfo.id
|
||||||
Version = "" # Will be populated when searching or if data exists
|
Version = ""
|
||||||
Source = $appInfo.source
|
Source = $appInfo.source
|
||||||
Architecture = $arch
|
Architecture = $arch
|
||||||
AdditionalExitCodes = if ($appInfo.PSObject.Properties['AdditionalExitCodes']) { $appInfo.AdditionalExitCodes } else { "" }
|
AdditionalExitCodes = if ($appInfo.PSObject.Properties['AdditionalExitCodes']) { $appInfo.AdditionalExitCodes } else { "" }
|
||||||
@@ -164,12 +179,14 @@ function Import-WingetList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$State.Controls.lstWingetResults.ItemsSource = $newAppListForItemsSource.ToArray()
|
$State.Controls.lstWingetResults.ItemsSource = $newAppListForItemsSource.ToArray()
|
||||||
|
Request-ListViewColumnAutoResize -ListView $State.Controls.lstWingetResults
|
||||||
|
$State.Controls.txtAppListJsonPath.Text = $ofd.FileName
|
||||||
|
|
||||||
[System.Windows.MessageBox]::Show("App list imported successfully.", "Success", "OK", "Information")
|
[System.Windows.MessageBox]::Show("Winget app list imported successfully.", "Success", "OK", "Information")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
[System.Windows.MessageBox]::Show("Error importing app list: $_", "Error", "OK", "Error")
|
[System.Windows.MessageBox]::Show("Error importing Winget app list: $_", "Error", "OK", "Error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,16 +106,20 @@ function Get-GeneralDefaults {
|
|||||||
$peDriversPath = Join-Path -Path $FFUDevelopmentPath -ChildPath "PEDrivers"
|
$peDriversPath = Join-Path -Path $FFUDevelopmentPath -ChildPath "PEDrivers"
|
||||||
$vmLocationPath = Join-Path -Path $FFUDevelopmentPath -ChildPath "VM"
|
$vmLocationPath = Join-Path -Path $FFUDevelopmentPath -ChildPath "VM"
|
||||||
$ffuCapturePath = Join-Path -Path $FFUDevelopmentPath -ChildPath "FFU"
|
$ffuCapturePath = Join-Path -Path $FFUDevelopmentPath -ChildPath "FFU"
|
||||||
|
$unattendPath = Join-Path -Path $FFUDevelopmentPath -ChildPath "unattend"
|
||||||
$officePath = Join-Path -Path $appsPath -ChildPath "Office"
|
$officePath = Join-Path -Path $appsPath -ChildPath "Office"
|
||||||
$appListJsonPath = Join-Path -Path $appsPath -ChildPath "AppList.json"
|
$appListJsonPath = Join-Path -Path $appsPath -ChildPath "AppList.json"
|
||||||
|
$userAppListPath = Join-Path -Path $appsPath -ChildPath "UserAppList.json"
|
||||||
$driversJsonPath = Join-Path -Path $driversPath -ChildPath "Drivers.json"
|
$driversJsonPath = Join-Path -Path $driversPath -ChildPath "Drivers.json"
|
||||||
|
$deviceNamePrefixesPath = Join-Path -Path $unattendPath -ChildPath "prefixes.txt"
|
||||||
|
$deviceNameSerialComputerNamesPath = Join-Path -Path $unattendPath -ChildPath "SerialComputerNames.csv"
|
||||||
|
$unattendX64FilePath = Join-Path -Path $unattendPath -ChildPath "unattend_x64.xml"
|
||||||
|
$unattendArm64FilePath = Join-Path -Path $unattendPath -ChildPath "unattend_arm64.xml"
|
||||||
|
|
||||||
return [PSCustomObject]@{
|
return [PSCustomObject]@{
|
||||||
# Build Tab Defaults
|
# Build Tab Defaults
|
||||||
CustomFFUNameTemplate = "{WindowsRelease}_{WindowsVersion}_{SKU}_{yyyy}-{MM}-{dd}_{HH}{mm}"
|
CustomFFUNameTemplate = "{WindowsRelease}_{WindowsVersion}_{SKU}_{yyyy}-{MM}-{dd}_{HH}{mm}"
|
||||||
FFUCaptureLocation = $ffuCapturePath
|
FFUCaptureLocation = $ffuCapturePath
|
||||||
ShareName = "FFUCaptureShare"
|
|
||||||
Username = "ffu_user"
|
|
||||||
Threads = 5
|
Threads = 5
|
||||||
BitsPriority = 'Normal'
|
BitsPriority = 'Normal'
|
||||||
MaxUSBDrives = 5
|
MaxUSBDrives = 5
|
||||||
@@ -123,7 +127,6 @@ function Get-GeneralDefaults {
|
|||||||
CompactOS = $true
|
CompactOS = $true
|
||||||
Optimize = $true
|
Optimize = $true
|
||||||
AllowVHDXCaching = $false
|
AllowVHDXCaching = $false
|
||||||
CreateCaptureMedia = $true
|
|
||||||
CreateDeploymentMedia = $true
|
CreateDeploymentMedia = $true
|
||||||
Verbose = $false
|
Verbose = $false
|
||||||
AllowExternalHardDiskMedia = $false
|
AllowExternalHardDiskMedia = $false
|
||||||
@@ -134,8 +137,15 @@ function Get-GeneralDefaults {
|
|||||||
CopyUnattend = $false
|
CopyUnattend = $false
|
||||||
CopyPPKG = $false
|
CopyPPKG = $false
|
||||||
InjectUnattend = $false
|
InjectUnattend = $false
|
||||||
|
UnattendX64FilePath = $unattendX64FilePath
|
||||||
|
UnattendArm64FilePath = $unattendArm64FilePath
|
||||||
|
DeviceNamingMode = 'None'
|
||||||
|
DeviceNameTemplate = ''
|
||||||
|
DeviceNamePrefixesPath = $deviceNamePrefixesPath
|
||||||
|
DeviceNamePrefixes = @()
|
||||||
|
DeviceNameSerialComputerNamesPath = $deviceNameSerialComputerNamesPath
|
||||||
|
DeviceNameSerialComputerNames = @()
|
||||||
CleanupAppsISO = $true
|
CleanupAppsISO = $true
|
||||||
CleanupCaptureISO = $true
|
|
||||||
CleanupDeployISO = $true
|
CleanupDeployISO = $true
|
||||||
CleanupDrivers = $false
|
CleanupDrivers = $false
|
||||||
RemoveFFU = $false
|
RemoveFFU = $false
|
||||||
@@ -143,7 +153,7 @@ function Get-GeneralDefaults {
|
|||||||
RemoveUpdates = $false
|
RemoveUpdates = $false
|
||||||
RemoveDownloadedESD = $true
|
RemoveDownloadedESD = $true
|
||||||
# Hyper-V Settings Defaults
|
# Hyper-V Settings Defaults
|
||||||
VMHostIPAddress = ""
|
EnableVMNetworking = $false
|
||||||
DiskSizeGB = 50
|
DiskSizeGB = 50
|
||||||
MemoryGB = 4
|
MemoryGB = 4
|
||||||
Processors = 4
|
Processors = 4
|
||||||
@@ -163,6 +173,7 @@ function Get-GeneralDefaults {
|
|||||||
InstallApps = $false
|
InstallApps = $false
|
||||||
ApplicationPath = $appsPath
|
ApplicationPath = $appsPath
|
||||||
AppListJsonPath = $appListJsonPath
|
AppListJsonPath = $appListJsonPath
|
||||||
|
UserAppListPath = $userAppListPath
|
||||||
InstallWingetApps = $false
|
InstallWingetApps = $false
|
||||||
BringYourOwnApps = $false
|
BringYourOwnApps = $false
|
||||||
# M365 Apps/Office Tab Defaults
|
# M365 Apps/Office Tab Defaults
|
||||||
@@ -266,6 +277,7 @@ function Update-AdditionalFFUList {
|
|||||||
foreach ($it in $items) { $listView.Items.Add($it) | Out-Null }
|
foreach ($it in $items) { $listView.Items.Add($it) | Out-Null }
|
||||||
WriteLog "Additional FFUs: Found $($listView.Items.Count) FFU files in $ffuFolder."
|
WriteLog "Additional FFUs: Found $($listView.Items.Count) FFU files in $ffuFolder."
|
||||||
}
|
}
|
||||||
|
Request-ListViewColumnAutoResize -ListView $listView
|
||||||
$headerChk = $State.Controls.chkSelectAllAdditionalFFUs
|
$headerChk = $State.Controls.chkSelectAllAdditionalFFUs
|
||||||
if ($null -ne $headerChk) {
|
if ($null -ne $headerChk) {
|
||||||
Update-SelectAllHeaderCheckBoxState -ListView $listView -HeaderCheckBox $headerChk
|
Update-SelectAllHeaderCheckBoxState -ListView $listView -HeaderCheckBox $headerChk
|
||||||
@@ -305,6 +317,7 @@ function Update-ApplicationPanelVisibility {
|
|||||||
$subOptionVisibility = if ($installAppsChecked) { 'Visible' } else { 'Collapsed' }
|
$subOptionVisibility = if ($installAppsChecked) { 'Visible' } else { 'Collapsed' }
|
||||||
$State.Controls.applicationPathPanel.Visibility = $subOptionVisibility
|
$State.Controls.applicationPathPanel.Visibility = $subOptionVisibility
|
||||||
$State.Controls.appListJsonPathPanel.Visibility = $subOptionVisibility
|
$State.Controls.appListJsonPathPanel.Visibility = $subOptionVisibility
|
||||||
|
$State.Controls.userAppListPathPanel.Visibility = $subOptionVisibility
|
||||||
$State.Controls.chkInstallWingetApps.Visibility = $subOptionVisibility
|
$State.Controls.chkInstallWingetApps.Visibility = $subOptionVisibility
|
||||||
$State.Controls.chkBringYourOwnApps.Visibility = $subOptionVisibility
|
$State.Controls.chkBringYourOwnApps.Visibility = $subOptionVisibility
|
||||||
$State.Controls.chkDefineAppsScriptVariables.Visibility = $subOptionVisibility
|
$State.Controls.chkDefineAppsScriptVariables.Visibility = $subOptionVisibility
|
||||||
@@ -468,6 +481,777 @@ function Update-DriverDownloadPanelVisibility {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# SECTION: Home Page Build Status
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Function to normalize release strings so local builds and GitHub tags compare consistently
|
||||||
|
function ConvertTo-NormalizedReleaseVersion {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[AllowNull()]
|
||||||
|
[string]$Version
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Version)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedVersion = $Version.Trim().ToLowerInvariant()
|
||||||
|
$normalizedVersion = $normalizedVersion -replace '^[v]', ''
|
||||||
|
return $normalizedVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to read the current FFU Builder build from the main build script
|
||||||
|
function Get-FFUBuilderCurrentBuild {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$FFUDevelopmentPath
|
||||||
|
)
|
||||||
|
|
||||||
|
$buildScriptPath = Join-Path -Path $FFUDevelopmentPath -ChildPath 'BuildFFUVM.ps1'
|
||||||
|
if (-not (Test-Path -Path $buildScriptPath)) {
|
||||||
|
return 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$buildScriptContent = Get-Content -Path $buildScriptPath -Raw -ErrorAction Stop
|
||||||
|
$versionMatch = [regex]::Match($buildScriptContent, '(?m)^\$version\s*=\s*''([^'']+)''')
|
||||||
|
if ($versionMatch.Success) {
|
||||||
|
return $versionMatch.Groups[1].Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Unable to read the current FFU Builder build version: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to query GitHub for the latest published FFU Builder release
|
||||||
|
function Get-FFUBuilderLatestRelease {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param()
|
||||||
|
|
||||||
|
$releaseApiUri = 'https://api.github.com/repos/rbalsleyMSFT/FFU/releases/latest'
|
||||||
|
$releaseHeaders = @{
|
||||||
|
'User-Agent' = 'FFUBuilderUI'
|
||||||
|
'Accept' = 'application/vnd.github+json'
|
||||||
|
}
|
||||||
|
|
||||||
|
$releaseResponse = Invoke-RestMethod -Uri $releaseApiUri -Headers $releaseHeaders -TimeoutSec 5 -ErrorAction Stop
|
||||||
|
$releaseVersion = if (-not [string]::IsNullOrWhiteSpace([string]$releaseResponse.tag_name)) {
|
||||||
|
[string]$releaseResponse.tag_name
|
||||||
|
}
|
||||||
|
elseif (-not [string]::IsNullOrWhiteSpace([string]$releaseResponse.name)) {
|
||||||
|
[string]$releaseResponse.name
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$null
|
||||||
|
}
|
||||||
|
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Version = $releaseVersion
|
||||||
|
HtmlUrl = [string]$releaseResponse.html_url
|
||||||
|
Body = [string]$releaseResponse.body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to build a user-friendly release status message for the Home page
|
||||||
|
function Get-FFUBuilderReleaseStatusMessage {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$CurrentBuild,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$LatestRelease
|
||||||
|
)
|
||||||
|
|
||||||
|
# Format the release string for Home page display while keeping compare logic normalized
|
||||||
|
$displayLatestRelease = if ([string]::IsNullOrWhiteSpace($LatestRelease)) {
|
||||||
|
$LatestRelease
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$LatestRelease -replace '^[vV]', ''
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedCurrentBuild = ConvertTo-NormalizedReleaseVersion -Version $CurrentBuild
|
||||||
|
$normalizedLatestRelease = ConvertTo-NormalizedReleaseVersion -Version $LatestRelease
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($normalizedCurrentBuild)) {
|
||||||
|
return 'Installed build information is unavailable.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($normalizedLatestRelease)) {
|
||||||
|
return 'Unable to compare the installed build with the latest release.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($normalizedCurrentBuild -eq $normalizedLatestRelease) {
|
||||||
|
return 'You are running the latest published build.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentVersionMatch = [regex]::Match($normalizedCurrentBuild, '^\d+(?:\.\d+){0,3}')
|
||||||
|
$latestVersionMatch = [regex]::Match($normalizedLatestRelease, '^\d+(?:\.\d+){0,3}')
|
||||||
|
|
||||||
|
if ($currentVersionMatch.Success -and $latestVersionMatch.Success) {
|
||||||
|
try {
|
||||||
|
$currentVersion = [version]$currentVersionMatch.Value
|
||||||
|
$latestVersion = [version]$latestVersionMatch.Value
|
||||||
|
|
||||||
|
if ($currentVersion -lt $latestVersion) {
|
||||||
|
return "A newer release is available: $displayLatestRelease."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($currentVersion -gt $latestVersion) {
|
||||||
|
return "This build is newer than the latest published release: $displayLatestRelease."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Unable to compare FFU Builder release versions numerically: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Installed build $CurrentBuild differs from the latest published release $displayLatestRelease."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to normalize a markdown heading for release-notes display
|
||||||
|
function ConvertTo-ReleaseNotesHeadingText {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[AllowNull()]
|
||||||
|
[string]$Line
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Line)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
$cleanLine = $Line.Trim()
|
||||||
|
$cleanLine = $cleanLine -replace '^#+\s*', ''
|
||||||
|
$cleanLine = [regex]::Replace($cleanLine, '\[([^\]]+)\]\([^)]+\)', '$1')
|
||||||
|
$cleanLine = $cleanLine -replace '\*\*', ''
|
||||||
|
$cleanLine = $cleanLine -replace '`', ''
|
||||||
|
return $cleanLine.Trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to clean plain text segments before rendering markdown-aware inlines
|
||||||
|
function ConvertTo-ReleaseNotesPlainText {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[AllowNull()]
|
||||||
|
[string]$Text
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Text)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
$cleanText = $Text
|
||||||
|
$cleanText = $cleanText -replace '\*\*', ''
|
||||||
|
$cleanText = $cleanText -replace '`', ''
|
||||||
|
return $cleanText
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to add markdown-aware inline content to a TextBlock
|
||||||
|
function Add-ReleaseNotesInlinesToTextBlock {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[System.Windows.Controls.TextBlock]$TextBlock,
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[AllowNull()]
|
||||||
|
[string]$Text
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Text)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$matchPattern = '(?<MarkdownLink>\[(?<LinkText>[^\]]+)\]\((?<LinkUrl>https?://[^)\s]+)\))|(?<BareUrl>https?://[^\s)]+)|(?<Bold>\*\*(?<BoldText>.+?)\*\*)'
|
||||||
|
$currentIndex = 0
|
||||||
|
|
||||||
|
foreach ($match in [regex]::Matches($Text, $matchPattern)) {
|
||||||
|
if ($match.Index -gt $currentIndex) {
|
||||||
|
$plainText = ConvertTo-ReleaseNotesPlainText -Text $Text.Substring($currentIndex, $match.Index - $currentIndex)
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($plainText)) {
|
||||||
|
$TextBlock.Inlines.Add([System.Windows.Documents.Run]::new($plainText)) | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($match.Groups['MarkdownLink'].Success) {
|
||||||
|
$hyperlink = [System.Windows.Documents.Hyperlink]::new()
|
||||||
|
$hyperlink.NavigateUri = [System.Uri]$match.Groups['LinkUrl'].Value
|
||||||
|
$hyperlink.ToolTip = $match.Groups['LinkUrl'].Value
|
||||||
|
$hyperlink.Inlines.Add([System.Windows.Documents.Run]::new($match.Groups['LinkText'].Value)) | Out-Null
|
||||||
|
$hyperlink.Add_RequestNavigate({
|
||||||
|
param($eventSource, $requestNavigateEventArgs)
|
||||||
|
Start-Process $requestNavigateEventArgs.Uri.AbsoluteUri
|
||||||
|
$requestNavigateEventArgs.Handled = $true
|
||||||
|
})
|
||||||
|
$TextBlock.Inlines.Add($hyperlink) | Out-Null
|
||||||
|
}
|
||||||
|
elseif ($match.Groups['BareUrl'].Success) {
|
||||||
|
$bareUrl = $match.Groups['BareUrl'].Value.TrimEnd('.', ',', ';', ':')
|
||||||
|
$hyperlink = [System.Windows.Documents.Hyperlink]::new()
|
||||||
|
$hyperlink.NavigateUri = [System.Uri]$bareUrl
|
||||||
|
$hyperlink.ToolTip = $bareUrl
|
||||||
|
$hyperlink.Inlines.Add([System.Windows.Documents.Run]::new($bareUrl)) | Out-Null
|
||||||
|
$hyperlink.Add_RequestNavigate({
|
||||||
|
param($eventSource, $requestNavigateEventArgs)
|
||||||
|
Start-Process $requestNavigateEventArgs.Uri.AbsoluteUri
|
||||||
|
$requestNavigateEventArgs.Handled = $true
|
||||||
|
})
|
||||||
|
$TextBlock.Inlines.Add($hyperlink) | Out-Null
|
||||||
|
|
||||||
|
$trailingCharactersLength = $match.Groups['BareUrl'].Value.Length - $bareUrl.Length
|
||||||
|
if ($trailingCharactersLength -gt 0) {
|
||||||
|
$trailingCharacters = $match.Groups['BareUrl'].Value.Substring($bareUrl.Length, $trailingCharactersLength)
|
||||||
|
$TextBlock.Inlines.Add([System.Windows.Documents.Run]::new($trailingCharacters)) | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($match.Groups['Bold'].Success) {
|
||||||
|
$boldRun = [System.Windows.Documents.Run]::new((ConvertTo-ReleaseNotesPlainText -Text $match.Groups['BoldText'].Value))
|
||||||
|
$boldRun.FontWeight = 'SemiBold'
|
||||||
|
$TextBlock.Inlines.Add($boldRun) | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentIndex = $match.Index + $match.Length
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($currentIndex -lt $Text.Length) {
|
||||||
|
$remainingText = ConvertTo-ReleaseNotesPlainText -Text $Text.Substring($currentIndex)
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($remainingText)) {
|
||||||
|
$TextBlock.Inlines.Add([System.Windows.Documents.Run]::new($remainingText)) | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to build a formatted UI element for a release-notes section body
|
||||||
|
function New-ReleaseNotesSectionContent {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[AllowNull()]
|
||||||
|
[string]$Content
|
||||||
|
)
|
||||||
|
|
||||||
|
$contentPanel = New-Object System.Windows.Controls.StackPanel
|
||||||
|
$contentPanel.Margin = '0,2,0,2'
|
||||||
|
|
||||||
|
foreach ($contentLine in ($Content -split "`r?`n")) {
|
||||||
|
$trimmedLine = $contentLine.Trim()
|
||||||
|
if ([string]::IsNullOrWhiteSpace($trimmedLine)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$isFirstRenderedLine = ($contentPanel.Children.Count -eq 0)
|
||||||
|
|
||||||
|
$textBlock = New-Object System.Windows.Controls.TextBlock
|
||||||
|
$textBlock.TextWrapping = 'Wrap'
|
||||||
|
$textBlock.Margin = if ($isFirstRenderedLine) { '0,2,0,0' } else { '0,12,0,0' }
|
||||||
|
|
||||||
|
$lineContent = $trimmedLine
|
||||||
|
$listItemMatch = [regex]::Match($trimmedLine, '^(?:[-*]|\d+\.)\s+(.+)$')
|
||||||
|
if ($listItemMatch.Success) {
|
||||||
|
$textBlock.Margin = if ($isFirstRenderedLine) { '0,2,0,0' } else { '0,10,0,0' }
|
||||||
|
$textBlock.Inlines.Add([System.Windows.Documents.Run]::new([string][char]0x2022 + ' ')) | Out-Null
|
||||||
|
$lineContent = $listItemMatch.Groups[1].Value
|
||||||
|
}
|
||||||
|
|
||||||
|
Add-ReleaseNotesInlinesToTextBlock -TextBlock $textBlock -Text $lineContent
|
||||||
|
$contentPanel.Children.Add($textBlock) | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($contentPanel.Children.Count -eq 0) {
|
||||||
|
$fallbackTextBlock = New-Object System.Windows.Controls.TextBlock
|
||||||
|
$fallbackTextBlock.Text = 'No additional details were published for this section.'
|
||||||
|
$fallbackTextBlock.TextWrapping = 'Wrap'
|
||||||
|
$fallbackTextBlock.Margin = '0,2,0,0'
|
||||||
|
$contentPanel.Children.Add($fallbackTextBlock) | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
return $contentPanel
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to parse the full GitHub release notes into UI sections
|
||||||
|
function Get-FFUBuilderReleaseNotesSections {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[AllowNull()]
|
||||||
|
[string]$ReleaseNotesBody
|
||||||
|
)
|
||||||
|
|
||||||
|
$releaseNoteSections = [System.Collections.Generic.List[object]]::new()
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ReleaseNotesBody)) {
|
||||||
|
$releaseNoteSections.Add([PSCustomObject]@{
|
||||||
|
Title = 'Release Notes'
|
||||||
|
Content = 'No release notes were published for this release.'
|
||||||
|
UseExpander = $false
|
||||||
|
IsExpanded = $true
|
||||||
|
})
|
||||||
|
return $releaseNoteSections
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentTitle = 'Release Overview'
|
||||||
|
$currentLines = [System.Collections.Generic.List[string]]::new()
|
||||||
|
|
||||||
|
foreach ($releaseNotesLine in ($ReleaseNotesBody -split "`r?`n")) {
|
||||||
|
$trimmedLine = $releaseNotesLine.Trim()
|
||||||
|
|
||||||
|
if ($trimmedLine -match '^#+\s*(.+)$') {
|
||||||
|
$sectionContent = ($currentLines -join [Environment]::NewLine).Trim()
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($sectionContent)) {
|
||||||
|
$useExpander = (($sectionContent -split "`r?`n").Count -gt 2 -or $sectionContent.Length -gt 220)
|
||||||
|
$releaseNoteSections.Add([PSCustomObject]@{
|
||||||
|
Title = $currentTitle
|
||||||
|
Content = $sectionContent
|
||||||
|
UseExpander = $useExpander
|
||||||
|
IsExpanded = ($releaseNoteSections.Count -eq 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentTitle = ConvertTo-ReleaseNotesHeadingText -Line $matches[1]
|
||||||
|
$currentLines = [System.Collections.Generic.List[string]]::new()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($trimmedLine)) {
|
||||||
|
if ($currentLines.Count -gt 0 -and $currentLines[$currentLines.Count - 1] -ne '') {
|
||||||
|
$currentLines.Add('')
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentLines.Add($trimmedLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
$finalSectionContent = ($currentLines -join [Environment]::NewLine).Trim()
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($finalSectionContent)) {
|
||||||
|
$useExpander = (($finalSectionContent -split "`r?`n").Count -gt 2 -or $finalSectionContent.Length -gt 220)
|
||||||
|
$releaseNoteSections.Add([PSCustomObject]@{
|
||||||
|
Title = $currentTitle
|
||||||
|
Content = $finalSectionContent
|
||||||
|
UseExpander = $useExpander
|
||||||
|
IsExpanded = ($releaseNoteSections.Count -eq 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($releaseNoteSections.Count -eq 0) {
|
||||||
|
$releaseNoteSections.Add([PSCustomObject]@{
|
||||||
|
Title = 'Release Notes'
|
||||||
|
Content = 'No release notes were published for this release.'
|
||||||
|
UseExpander = $false
|
||||||
|
IsExpanded = $true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return $releaseNoteSections
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to render formatted release notes into the Home page
|
||||||
|
function Set-HomeReleaseNotesContent {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[PSCustomObject]$State,
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[AllowNull()]
|
||||||
|
[string]$ReleaseNotesBody
|
||||||
|
)
|
||||||
|
|
||||||
|
$releaseNotesPanel = $State.Controls.spHomeReleaseNotesSections
|
||||||
|
if ($null -eq $releaseNotesPanel) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$releaseNotesPanel.Children.Clear()
|
||||||
|
$releaseNoteSections = @(Get-FFUBuilderReleaseNotesSections -ReleaseNotesBody $ReleaseNotesBody)
|
||||||
|
|
||||||
|
foreach ($releaseNoteSection in $releaseNoteSections) {
|
||||||
|
$sectionContent = New-ReleaseNotesSectionContent -Content $releaseNoteSection.Content
|
||||||
|
|
||||||
|
if ($releaseNoteSection.UseExpander) {
|
||||||
|
$headerTextBlock = New-Object System.Windows.Controls.TextBlock
|
||||||
|
$headerTextBlock.Text = $releaseNoteSection.Title
|
||||||
|
$headerTextBlock.TextWrapping = 'Wrap'
|
||||||
|
$headerTextBlock.FontWeight = 'SemiBold'
|
||||||
|
|
||||||
|
$releaseNotesExpander = New-Object System.Windows.Controls.Expander
|
||||||
|
$releaseNotesExpander.Header = $headerTextBlock
|
||||||
|
$releaseNotesExpander.IsExpanded = [bool]$releaseNoteSection.IsExpanded
|
||||||
|
$releaseNotesExpander.Margin = '0,0,0,8'
|
||||||
|
$releaseNotesExpander.Content = $sectionContent
|
||||||
|
|
||||||
|
$releaseNotesPanel.Children.Add($releaseNotesExpander) | Out-Null
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$releaseNotesSectionPanel = New-Object System.Windows.Controls.StackPanel
|
||||||
|
$releaseNotesSectionPanel.Margin = '0,0,0,8'
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($releaseNoteSection.Title)) {
|
||||||
|
$titleTextBlock = New-Object System.Windows.Controls.TextBlock
|
||||||
|
$titleTextBlock.Text = $releaseNoteSection.Title
|
||||||
|
$titleTextBlock.FontWeight = 'SemiBold'
|
||||||
|
$titleTextBlock.TextWrapping = 'Wrap'
|
||||||
|
$releaseNotesSectionPanel.Children.Add($titleTextBlock) | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$releaseNotesSectionPanel.Children.Add($sectionContent) | Out-Null
|
||||||
|
$releaseNotesPanel.Children.Add($releaseNotesSectionPanel) | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to return a Home page status light brush for environment checks
|
||||||
|
function Get-HomeStatusBrush {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidateSet('Green', 'Yellow', 'Red')]
|
||||||
|
[string]$Level
|
||||||
|
)
|
||||||
|
|
||||||
|
switch ($Level) {
|
||||||
|
'Green' { return [System.Windows.Media.Brushes]::LimeGreen }
|
||||||
|
'Yellow' { return [System.Windows.Media.Brushes]::Gold }
|
||||||
|
'Red' { return [System.Windows.Media.Brushes]::IndianRed }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to evaluate free disk space on the drive hosting the FFU development path
|
||||||
|
function Get-FFUBuilderDiskSpaceStatus {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$FFUDevelopmentPath
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
$resolvedPath = if (Test-Path -Path $FFUDevelopmentPath) {
|
||||||
|
(Resolve-Path -Path $FFUDevelopmentPath -ErrorAction Stop).Path
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$FFUDevelopmentPath
|
||||||
|
}
|
||||||
|
|
||||||
|
$driveRoot = [System.IO.Path]::GetPathRoot($resolvedPath)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($driveRoot)) {
|
||||||
|
throw "Unable to determine a drive root for path $FFUDevelopmentPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$driveInfo = [System.IO.DriveInfo]::new($driveRoot)
|
||||||
|
$freeSpaceGb = [math]::Round($driveInfo.AvailableFreeSpace / 1GB, 2)
|
||||||
|
|
||||||
|
if ($freeSpaceGb -lt 50) {
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Level = 'Red'
|
||||||
|
Message = "$freeSpaceGb GB free on $driveRoot. FFU Builder is likely to run out of disk space and should have at least 100 GB free."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($freeSpaceGb -lt 100) {
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Level = 'Yellow'
|
||||||
|
Message = "$freeSpaceGb GB free on $driveRoot. FFU Builder recommends at least 100 GB free space."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Level = 'Green'
|
||||||
|
Message = "$freeSpaceGb GB free on $driveRoot. Free space is within the recommended range."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Unable to determine free disk space for FFUDevelopmentPath: $($_.Exception.Message)"
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Level = 'Red'
|
||||||
|
Message = 'Unable to determine free disk space for the FFUDevelopmentPath drive.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to evaluate the local Hyper-V installation state
|
||||||
|
function Get-FFUBuilderHyperVStatus {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param()
|
||||||
|
|
||||||
|
try {
|
||||||
|
$hyperVFeature = Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-All -ErrorAction Stop
|
||||||
|
switch ([string]$hyperVFeature.State) {
|
||||||
|
'Enabled' {
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Level = 'Green'
|
||||||
|
Message = 'Hyper-V is installed and ready.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'EnablePending' {
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Level = 'Yellow'
|
||||||
|
Message = 'Hyper-V is installed, but a reboot is required before it is ready.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Level = 'Red'
|
||||||
|
Message = "Hyper-V is not installed. Current feature state: $($hyperVFeature.State)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Unable to determine Hyper-V installation state: $($_.Exception.Message)"
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Level = 'Red'
|
||||||
|
Message = 'Unable to determine the Hyper-V installation state.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to update the Home page release status fields
|
||||||
|
function Update-HomeReleaseStatus {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[PSCustomObject]$State,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$CurrentBuild,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$LatestRelease,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$StatusMessage,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ReleaseNotesBody
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -ne $State.Controls.txtHomeCurrentBuildValue) {
|
||||||
|
$State.Controls.txtHomeCurrentBuildValue.Text = $CurrentBuild
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -ne $State.Controls.txtHomeLatestReleaseValue) {
|
||||||
|
$State.Controls.txtHomeLatestReleaseValue.Text = $LatestRelease
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -ne $State.Controls.txtHomeReleaseStatusValue) {
|
||||||
|
$State.Controls.txtHomeReleaseStatusValue.Text = $StatusMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
# Render the full release notes into structured sections on the Home page
|
||||||
|
Set-HomeReleaseNotesContent -State $State -ReleaseNotesBody $ReleaseNotesBody
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to update the Home page environment check fields
|
||||||
|
function Update-HomeEnvironmentStatus {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[PSCustomObject]$State,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[PSCustomObject]$DiskSpaceStatus,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[PSCustomObject]$HyperVStatus
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -ne $State.Controls.ellipseHomeDiskSpaceStatus) {
|
||||||
|
$State.Controls.ellipseHomeDiskSpaceStatus.Fill = Get-HomeStatusBrush -Level $DiskSpaceStatus.Level
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -ne $State.Controls.txtHomeDiskSpaceStatusValue) {
|
||||||
|
$State.Controls.txtHomeDiskSpaceStatusValue.Text = $DiskSpaceStatus.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -ne $State.Controls.ellipseHomeHyperVStatus) {
|
||||||
|
$State.Controls.ellipseHomeHyperVStatus.Fill = Get-HomeStatusBrush -Level $HyperVStatus.Level
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -ne $State.Controls.txtHomeHyperVStatusValue) {
|
||||||
|
$State.Controls.txtHomeHyperVStatusValue.Text = $HyperVStatus.Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to retrieve latest public GitHub discussions for Home page display
|
||||||
|
function Get-FFUBuilderLatestDiscussions {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param()
|
||||||
|
|
||||||
|
$discussionUri = 'https://github.com/rbalsleyMSFT/FFU/discussions'
|
||||||
|
$discussionHeaders = @{
|
||||||
|
'User-Agent' = 'FFUBuilderUI'
|
||||||
|
'Accept' = 'text/html,application/xhtml+xml'
|
||||||
|
}
|
||||||
|
|
||||||
|
$discussionResponse = Invoke-WebRequest -Uri $discussionUri -Headers $discussionHeaders -TimeoutSec 5 -ErrorAction Stop
|
||||||
|
$discussionContent = [string]$discussionResponse.Content
|
||||||
|
$latestDiscussions = New-Object System.Collections.Generic.List[PSCustomObject]
|
||||||
|
$seenDiscussionUrls = @{}
|
||||||
|
|
||||||
|
# Parse the raw HTML instead of Invoke-WebRequest Links because GitHub's page structure
|
||||||
|
# does not reliably surface the discussion topic anchors through the Links collection.
|
||||||
|
$discussionMatches = [regex]::Matches(
|
||||||
|
$discussionContent,
|
||||||
|
'<a[^>]+href="(?<Href>/rbalsleyMSFT/FFU/discussions/(?<Id>\d+))"[^>]*>(?<InnerHtml>.*?)</a>',
|
||||||
|
[System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::Singleline
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($discussionMatch in $discussionMatches) {
|
||||||
|
$discussionHref = [string]$discussionMatch.Groups['Href'].Value
|
||||||
|
$discussionUrl = "https://github.com$discussionHref"
|
||||||
|
|
||||||
|
if ($seenDiscussionUrls.ContainsKey($discussionUrl)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$discussionInnerHtml = [string]$discussionMatch.Groups['InnerHtml'].Value
|
||||||
|
$discussionTitle = [regex]::Replace($discussionInnerHtml, '<[^>]+>', ' ')
|
||||||
|
$discussionTitle = [System.Net.WebUtility]::HtmlDecode($discussionTitle)
|
||||||
|
$discussionTitle = [regex]::Replace($discussionTitle, '\s+', ' ').Trim()
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($discussionTitle)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Skip links that resolve to comment counts or other numeric-only link text.
|
||||||
|
if ($discussionTitle -match '^\d+$') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$seenDiscussionUrls[$discussionUrl] = $true
|
||||||
|
$latestDiscussions.Add([PSCustomObject]@{
|
||||||
|
Title = $discussionTitle
|
||||||
|
Url = $discussionUrl
|
||||||
|
})
|
||||||
|
|
||||||
|
if ($latestDiscussions.Count -ge 5) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $latestDiscussions
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to update the Home page discussions card
|
||||||
|
function Update-HomeDiscussions {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[PSCustomObject]$State,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$StatusMessage,
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[AllowNull()]
|
||||||
|
[System.Collections.IEnumerable]$Discussions
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -ne $State.Controls.txtHomeDiscussionsStatusValue) {
|
||||||
|
$State.Controls.txtHomeDiscussionsStatusValue.Text = $StatusMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
$discussionItems = @($Discussions)
|
||||||
|
for ($index = 1; $index -le 5; $index++) {
|
||||||
|
$container = $State.Controls["tbDiscussion$index"]
|
||||||
|
$link = $State.Controls["linkDiscussion$index"]
|
||||||
|
$run = $State.Controls["runDiscussion$index"]
|
||||||
|
|
||||||
|
if ($null -eq $container -or $null -eq $link -or $null -eq $run) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($index -le $discussionItems.Count -and $null -ne $discussionItems[$index - 1]) {
|
||||||
|
$discussionItem = $discussionItems[$index - 1]
|
||||||
|
$run.Text = $discussionItem.Title
|
||||||
|
$link.NavigateUri = [System.Uri]$discussionItem.Url
|
||||||
|
$container.Visibility = 'Visible'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$run.Text = ''
|
||||||
|
$link.NavigateUri = [System.Uri]'https://github.com/rbalsleyMSFT/FFU/discussions'
|
||||||
|
$container.Visibility = 'Collapsed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -ne $State.Controls.tbDiscussionsLink) {
|
||||||
|
$State.Controls.tbDiscussionsLink.Visibility = 'Visible'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to populate the Home page build status after the window has rendered
|
||||||
|
function Start-HomeStatusRefresh {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[PSCustomObject]$State
|
||||||
|
)
|
||||||
|
|
||||||
|
# Populate local status checks immediately so Home is useful even before network requests complete
|
||||||
|
$currentBuild = Get-FFUBuilderCurrentBuild -FFUDevelopmentPath $State.FFUDevelopmentPath
|
||||||
|
$diskSpaceStatus = Get-FFUBuilderDiskSpaceStatus -FFUDevelopmentPath $State.FFUDevelopmentPath
|
||||||
|
$hyperVStatus = Get-FFUBuilderHyperVStatus
|
||||||
|
|
||||||
|
Update-HomeReleaseStatus -State $State -CurrentBuild $currentBuild -LatestRelease 'Checking GitHub...' -StatusMessage 'Checking whether this build is current...' -ReleaseNotesBody 'Checking latest release notes...'
|
||||||
|
Update-HomeEnvironmentStatus -State $State -DiskSpaceStatus $diskSpaceStatus -HyperVStatus $hyperVStatus
|
||||||
|
Update-HomeDiscussions -State $State -StatusMessage 'Checking latest discussions...' -Discussions @()
|
||||||
|
|
||||||
|
if ($null -eq $State.Window) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Capture the state values before dispatching to avoid losing them in the deferred callback
|
||||||
|
$refreshState = $State
|
||||||
|
$refreshCurrentBuild = $currentBuild
|
||||||
|
$refreshAction = {
|
||||||
|
$latestReleaseDisplay = 'Unable to check'
|
||||||
|
$statusMessage = 'Unable to check the latest release right now. Check GitHub Releases when you are back online.'
|
||||||
|
$releaseNotesBody = 'Unable to load the latest release notes right now.'
|
||||||
|
$discussionsStatusMessage = 'Unable to load the latest GitHub discussions right now.'
|
||||||
|
$latestDiscussions = @()
|
||||||
|
|
||||||
|
try {
|
||||||
|
$latestRelease = Get-FFUBuilderLatestRelease
|
||||||
|
if ($null -ne $latestRelease -and -not [string]::IsNullOrWhiteSpace($latestRelease.Version)) {
|
||||||
|
# Strip the GitHub tag prefix so Home shows the same style as the installed build
|
||||||
|
$latestReleaseDisplay = $latestRelease.Version -replace '^[vV]', ''
|
||||||
|
$statusMessage = Get-FFUBuilderReleaseStatusMessage -CurrentBuild $refreshCurrentBuild -LatestRelease $latestRelease.Version
|
||||||
|
$releaseNotesBody = if ([string]::IsNullOrWhiteSpace($latestRelease.Body)) {
|
||||||
|
'No release notes were published for this release.'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$latestRelease.Body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Unable to retrieve the latest FFU Builder release: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$latestDiscussions = @(Get-FFUBuilderLatestDiscussions)
|
||||||
|
if ($latestDiscussions.Count -gt 0) {
|
||||||
|
$discussionsStatusMessage = 'Latest public GitHub discussions.'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$discussionsStatusMessage = 'No recent public discussion topics were found.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Unable to retrieve the latest FFU Builder discussions: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
Update-HomeReleaseStatus -State $refreshState -CurrentBuild $refreshCurrentBuild -LatestRelease $latestReleaseDisplay -StatusMessage $statusMessage -ReleaseNotesBody $releaseNotesBody
|
||||||
|
Update-HomeDiscussions -State $refreshState -StatusMessage $discussionsStatusMessage -Discussions $latestDiscussions
|
||||||
|
}.GetNewClosure()
|
||||||
|
|
||||||
|
# Queue the network checks after the UI renders so startup remains responsive
|
||||||
|
$null = $State.Window.Dispatcher.BeginInvoke(
|
||||||
|
[System.Action]$refreshAction,
|
||||||
|
[System.Windows.Threading.DispatcherPriority]::Background
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# SECTION: Module Export
|
# SECTION: Module Export
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
select disk 0
|
|
||||||
select partition 3
|
|
||||||
Assign letter="M"
|
|
||||||
exit
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
$VMHostIPAddress = '192.168.1.158'
|
|
||||||
$ShareName = 'FFUCaptureShare'
|
|
||||||
$UserName = 'ffu_user'
|
|
||||||
$Password = '23202eb4-10c3-47e9-b389-f0c462663a23'
|
|
||||||
$CustomFFUNameTemplate = '{WindowsRelease}_{WindowsVersion}_{SKU}_{yyyy}-{MM}-{dd}_{HH}{mm}'
|
|
||||||
|
|
||||||
$netuseCommand = "net use W: \\$VMHostIPAddress\$ShareName /user:$UserName $Password 2>&1"
|
|
||||||
|
|
||||||
# Connect to network share
|
|
||||||
try {
|
|
||||||
Write-Host "Connecting to network share via $netuseCommand"
|
|
||||||
$netUseResult = net use W: "\\$VMHostIPAddress\$ShareName" "/user:$UserName" "$Password" 2>&1
|
|
||||||
|
|
||||||
# Check if the result contains an error
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
# Extract the error code from the Exception Message
|
|
||||||
# Example message format: "System error 53 has occurred."
|
|
||||||
$message = $netUseResult.Exception.Message
|
|
||||||
$regex = [regex]'System error (\d+)'
|
|
||||||
$match = $regex.Match($message)
|
|
||||||
if ($match.Success) {
|
|
||||||
$errorCode = [int]$match.Groups[1].Value
|
|
||||||
|
|
||||||
$errorMessage = switch ($errorCode) {
|
|
||||||
53 { "Network path not found. Verify the IP address is correct and the server is accessible." }
|
|
||||||
67 { "Network name cannot be found. Verify the share name exists on the server." }
|
|
||||||
86 { "Password is incorrect for the specified username." }
|
|
||||||
1219 { "Multiple connections to the share exist."}
|
|
||||||
1326 { "Logon failure: unknown username or bad password." }
|
|
||||||
1385 { "Logon failure: the user has not been granted the requested logon type at this computer.
|
|
||||||
This is likely due to changes to the User Rights Assignment: Access this computer from the network local security policy
|
|
||||||
See: https://github.com/rbalsleyMSFT/FFU/issues/122 for more info" }
|
|
||||||
1792 { "Unable to connect. Verify the server is running and accepting connections." }
|
|
||||||
2250 { "Network connection attempt timed out." }
|
|
||||||
default { "Network connection failed with error code: $errorCode. Details: $message" }
|
|
||||||
}
|
|
||||||
# Write-Error $errorMessage
|
|
||||||
throw $errorMessage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
Write-Error "Failed to connect to network share: Error code: $errorcode $_"
|
|
||||||
Write-Host "Some things to try:"
|
|
||||||
Write-Host '1. If not using an external switch, change to using an external switch'
|
|
||||||
Write-Host '2. Make sure the VMHostIPAddress is correct for the VMSwitch that is being used'
|
|
||||||
Write-Host '3. Try disabling the Windows Firewall on the host machine as a test only. If that helps, there is a Windows firewall rule that is blocking SMB 445 into the VM host'
|
|
||||||
Write-Host '4. If this is a machine that is managed by your organization, try using another machine that is not managed. There could be security policies in place that are blocking the connection to the share.'
|
|
||||||
Write-Host '5. You can also try disabling Hyper-V and re-enabling it. This has helped some users in the past.'
|
|
||||||
Write-Host '6. If all else fails, open an issue on the github repo and attach screenshots of this message, your FFUDevelopment.log, your command line that you used to build the FFU, and/or the config file you used (if you used one).'
|
|
||||||
pause
|
|
||||||
throw
|
|
||||||
}
|
|
||||||
|
|
||||||
$AssignDriveLetter = 'x:\AssignDriveLetter.txt'
|
|
||||||
try {
|
|
||||||
Write-Host 'Assigning M: as Windows drive letter'
|
|
||||||
Start-Process -FilePath diskpart.exe -ArgumentList "/S $AssignDriveLetter" -Wait -ErrorAction Stop | Out-Null
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Error "Failed to assign drive letter using diskpart: $_"
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#Load Registry Hive
|
|
||||||
$Software = 'M:\Windows\System32\config\software'
|
|
||||||
try {
|
|
||||||
Write-Host "Loading software registry hive to $Software"
|
|
||||||
if (-not (Test-Path -Path $Software)) {
|
|
||||||
throw "Software registry hive not found at $Software"
|
|
||||||
}
|
|
||||||
$regResult = reg load "HKLM\FFU" $Software 2>&1
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "Registry load failed with exit code $($LASTEXITCODE): $regResult"
|
|
||||||
}
|
|
||||||
Write-Host "Successfully loaded software registry hive."
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Error "Failed to load registry hive: $_"
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
#Find Windows version values
|
|
||||||
Write-Host "Retrieving Windows information from the registry..."
|
|
||||||
$SKU = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'EditionID'
|
|
||||||
Write-Host "SKU: $SKU"
|
|
||||||
[int]$CurrentBuild = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'CurrentBuild'
|
|
||||||
Write-Host "CurrentBuild: $CurrentBuild"
|
|
||||||
if ($CurrentBuild -notin 14393, 17763) {
|
|
||||||
Write-Host "CurrentBuild is not 14393 or 17763, retrieving WindowsVersion..."
|
|
||||||
$WindowsVersion = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'DisplayVersion'
|
|
||||||
Write-Host "WindowsVersion: $WindowsVersion"
|
|
||||||
}
|
|
||||||
$InstallationType = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'InstallationType'
|
|
||||||
Write-Host "InstallationType: $InstallationType"
|
|
||||||
$BuildDate = Get-Date -uformat %b%Y
|
|
||||||
Write-Host "BuildDate: $BuildDate"
|
|
||||||
|
|
||||||
$SKU = switch ($SKU) {
|
|
||||||
Core { 'Home' }
|
|
||||||
CoreN { 'Home_N' }
|
|
||||||
CoreSingleLanguage { 'Home_SL' }
|
|
||||||
Professional { 'Pro' }
|
|
||||||
ProfessionalN { 'Pro_N' }
|
|
||||||
ProfessionalEducation { 'Pro_Edu' }
|
|
||||||
ProfessionalEducationN { 'Pro_Edu_N' }
|
|
||||||
Enterprise { 'Ent' }
|
|
||||||
EnterpriseN { 'Ent_N' }
|
|
||||||
EnterpriseS { 'Ent_LTSC' }
|
|
||||||
EnterpriseSN { 'Ent_N_LTSC' }
|
|
||||||
IoTEnterpriseS { 'IoT_Ent_LTSC' }
|
|
||||||
Education { 'Edu' }
|
|
||||||
EducationN { 'Edu_N' }
|
|
||||||
ProfessionalWorkstation { 'Pro_Wks' }
|
|
||||||
ProfessionalWorkstationN { 'Pro_Wks_N' }
|
|
||||||
ServerStandard { 'Srv_Std' }
|
|
||||||
ServerDatacenter { 'Srv_Dtc' }
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($InstallationType -eq "Client") {
|
|
||||||
if ($CurrentBuild -ge 22000) {
|
|
||||||
$WindowsRelease = 'Win11'
|
|
||||||
Write-Host "WindowsRelease: $WindowsRelease"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$WindowsRelease = 'Win10'
|
|
||||||
Write-Host "WindowsRelease: $WindowsRelease"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$WindowsRelease = switch ($CurrentBuild) {
|
|
||||||
26100 { '2025' }
|
|
||||||
20348 { '2022' }
|
|
||||||
17763 { '2019' }
|
|
||||||
14393 { '2016' }
|
|
||||||
Default { $WindowsVersion }
|
|
||||||
}
|
|
||||||
Write-Host "WindowsRelease: $WindowsRelease"
|
|
||||||
if ($InstallationType -eq "Server Core") {
|
|
||||||
$SKU += "_Core"
|
|
||||||
Write-Host "InstallType is Server Core, changing SKU to: $SKU"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($CustomFFUNameTemplate) {
|
|
||||||
Write-Host 'Using custom FFU name template...'
|
|
||||||
$FFUFileName = $CustomFFUNameTemplate
|
|
||||||
$FFUFileName = $FFUFileName -replace '{WindowsRelease}', $WindowsRelease
|
|
||||||
$FFUFileName = $FFUFileName -replace '{WindowsVersion}', $WindowsVersion
|
|
||||||
$FFUFileName = $FFUFileName -replace '{SKU}', $SKU
|
|
||||||
$FFUFileName = $FFUFileName -replace '{BuildDate}', $BuildDate
|
|
||||||
$FFUFileName = $FFUFileName -replace '{yyyy}', (Get-Date -UFormat '%Y')
|
|
||||||
$FFUFileName = $FFUFileName -creplace '{MM}', (Get-Date -UFormat '%m')
|
|
||||||
$FFUFileName = $FFUFileName -replace '{dd}', (Get-Date -UFormat '%d')
|
|
||||||
$FFUFileName = $FFUFileName -creplace '{HH}', (Get-Date -UFormat '%H')
|
|
||||||
$FFUFileName = $FFUFileName -creplace '{hh}', (Get-Date -UFormat '%I')
|
|
||||||
$FFUFileName = $FFUFileName -creplace '{mm}', (Get-Date -UFormat '%M')
|
|
||||||
$FFUFileName = $FFUFileName -replace '{tt}', (Get-Date -UFormat '%p')
|
|
||||||
Write-Host "FFU File Name: $FFUFileName"
|
|
||||||
#If the custom FFU name template does not end with .ffu, append it
|
|
||||||
if ($FFUFileName -notlike '*.ffu') {
|
|
||||||
$FFUFileName += '.ffu'
|
|
||||||
Write-Host "Appended .ffu to FFU file name: $FFUFileName"
|
|
||||||
}
|
|
||||||
$dismArgs = "/capture-ffu /imagefile=W:\$FFUFileName /capturedrive=\\.\PhysicalDrive0 /name:$WindowsRelease$WindowsVersion$SKU /Compress:Default"
|
|
||||||
Write-Host "DISM arguments for capture: $dismArgs"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
#If Office is installed, modify the file name of the FFU
|
|
||||||
$Office = Get-ChildItem -Path 'M:\Program Files\Microsoft Office' -ErrorAction SilentlyContinue
|
|
||||||
if ($Office) {
|
|
||||||
$ffuFilePath = "W:\$WindowsRelease`_$WindowsVersion`_$SKU`_Office`_$BuildDate.ffu"
|
|
||||||
Write-Host "Office is installed, using modified FFU file name: $ffuFilePath"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$ffuFilePath = "W:\$WindowsRelease`_$WindowsVersion`_$SKU`_Apps`_$BuildDate.ffu"
|
|
||||||
Write-Host "Office is not installed, using modified FFU file name: $ffuFilePath"
|
|
||||||
}
|
|
||||||
$dismArgs = "/capture-ffu /imagefile=$ffuFilePath /capturedrive=\\.\PhysicalDrive0 /name:$WindowsRelease$WindowsVersion$SKU /Compress:Default"
|
|
||||||
Write-Host "DISM arguments for capture: $dismArgs"
|
|
||||||
}
|
|
||||||
|
|
||||||
#Unload Registry
|
|
||||||
Set-Location X:\
|
|
||||||
Remove-Variable SKU
|
|
||||||
Remove-Variable CurrentBuild
|
|
||||||
if ($CurrentBuild -notin 14393, 17763) {
|
|
||||||
Remove-Variable WindowsVersion
|
|
||||||
}
|
|
||||||
if ($Office) {
|
|
||||||
Remove-Variable Office
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Write-Host "Unloading registry hive HKLM\FFU..."
|
|
||||||
$regUnloadResult = reg unload "HKLM\FFU" 2>&1
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "Registry unload failed with exit code $($LASTEXITCODE): $regUnloadResult"
|
|
||||||
}
|
|
||||||
Write-Host "Successfully unloaded registry hive."
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Error "Failed to unload registry hive: $_"
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Sleeping for 60 seconds to allow registry to unload prior to capture"
|
|
||||||
Start-sleep 60
|
|
||||||
|
|
||||||
try {
|
|
||||||
Write-Host "Starting DISM FFU capture..."
|
|
||||||
$dismProcess = Start-Process -FilePath dism.exe -ArgumentList $dismArgs -Wait -PassThru -ErrorAction Stop
|
|
||||||
if ($dismProcess.ExitCode -ne 0) {
|
|
||||||
throw "DISM capture failed with exit code $($dismProcess.ExitCode)"
|
|
||||||
}
|
|
||||||
Write-Host "DISM FFU capture completed successfully."
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Error "FFU capture failed: $_"
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Write-Host "Copying DISM log to network share..."
|
|
||||||
xcopy X:\Windows\logs\dism\dism.log W:\ /Y | Out-Null
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Warning "Failed to copy DISM log: $_"
|
|
||||||
}
|
|
||||||
Write-Host "DISM log copied to network share, shutting down..."
|
|
||||||
wpeutil Shutdown
|
|
||||||
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Error "An unexpected error occurred: $_"
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
wpeinit
|
|
||||||
powercfg /s 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
|
|
||||||
powershell -Noprofile -ExecutionPolicy Bypass -File x:\CaptureFFU.ps1
|
|
||||||
exit
|
|
||||||
|
|
||||||
@@ -41,6 +41,59 @@ function WriteLog($LogText) {
|
|||||||
Add-Content -path $LogFile -value "$((Get-Date).ToString()) $LogText"
|
Add-Content -path $LogFile -value "$((Get-Date).ToString()) $LogText"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Read-MenuSelection {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Prompt,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$InvalidInputMessage,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[int[]]$ValidSelections,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[int]$Minimum = [int]::MinValue,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[int]$Maximum = [int]::MaxValue,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[switch]$AllowSkip
|
||||||
|
)
|
||||||
|
|
||||||
|
do {
|
||||||
|
$userInput = Read-Host $Prompt
|
||||||
|
if ([string]::IsNullOrWhiteSpace($userInput)) {
|
||||||
|
Write-Host $InvalidInputMessage
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$selection = 0
|
||||||
|
if (-not [int]::TryParse($userInput, [ref]$selection)) {
|
||||||
|
Write-Host $InvalidInputMessage
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($AllowSkip -and $selection -eq 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($PSBoundParameters.ContainsKey('ValidSelections')) {
|
||||||
|
if ($ValidSelections -notcontains $selection) {
|
||||||
|
Write-Host $InvalidInputMessage
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($selection -lt $Minimum -or $selection -gt $Maximum) {
|
||||||
|
Write-Host $InvalidInputMessage
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return $selection
|
||||||
|
} until ($false)
|
||||||
|
}
|
||||||
|
|
||||||
function Set-DiskpartAnswerFiles($DiskpartFile, $DiskID) {
|
function Set-DiskpartAnswerFiles($DiskpartFile, $DiskID) {
|
||||||
(Get-Content $DiskpartFile).Replace('disk 0', "disk $DiskID") | Set-Content -Path $DiskpartFile
|
(Get-Content $DiskpartFile).Replace('disk 0', "disk $DiskID") | Set-Content -Path $DiskpartFile
|
||||||
}
|
}
|
||||||
@@ -64,6 +117,68 @@ function Set-Computername($computername) {
|
|||||||
return $computername
|
return $computername
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-UnattendComputerNameValue {
|
||||||
|
if ($null -eq $UnattendFile) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
[xml]$xml = Get-Content $UnattendFile
|
||||||
|
foreach ($component in $xml.unattend.settings.component) {
|
||||||
|
if ($component.ComputerName) {
|
||||||
|
return [string]$component.ComputerName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-LegacyPromptComputerName($computername) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($computername)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedName = $computername.Trim().ToLowerInvariant()
|
||||||
|
return $normalizedName -in @('mycomputer', 'default')
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-NormalizedComputerName($computername) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($computername)) {
|
||||||
|
throw 'Computer name cannot be empty.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedName = ($computername -replace "\s", '').Trim()
|
||||||
|
if ([string]::IsNullOrWhiteSpace($normalizedName)) {
|
||||||
|
throw 'Computer name cannot be empty after removing spaces.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($normalizedName.Length -gt 15) {
|
||||||
|
$normalizedName = $normalizedName.Substring(0, 15)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalizedName
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ComputerNameTemplate($computerNameTemplate, $serialNumber) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($computerNameTemplate)) {
|
||||||
|
throw 'Computer name template cannot be empty.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedName = $computerNameTemplate -replace '(?i)%serial%', $serialNumber
|
||||||
|
if ($resolvedName -match '%') {
|
||||||
|
throw 'Unsupported device name variable found. Only %serial% is supported.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return Get-NormalizedComputerName($resolvedName)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-ConfiguredComputerName($computername) {
|
||||||
|
$normalizedName = Get-NormalizedComputerName($computername)
|
||||||
|
$normalizedName = Set-Computername($normalizedName)
|
||||||
|
Writelog "Computer name will be set to $normalizedName"
|
||||||
|
Write-Host "Computer name will be set to $normalizedName"
|
||||||
|
return $normalizedName
|
||||||
|
}
|
||||||
|
|
||||||
function Invoke-Process {
|
function Invoke-Process {
|
||||||
[CmdletBinding(SupportsShouldProcess)]
|
[CmdletBinding(SupportsShouldProcess)]
|
||||||
param
|
param
|
||||||
@@ -835,7 +950,7 @@ $LogFileName = 'ScriptLog.txt'
|
|||||||
$USBDrive = Get-USBDrive
|
$USBDrive = Get-USBDrive
|
||||||
New-item -Path $USBDrive -Name $LogFileName -ItemType "file" -Force | Out-Null
|
New-item -Path $USBDrive -Name $LogFileName -ItemType "file" -Force | Out-Null
|
||||||
$LogFile = $USBDrive + $LogFilename
|
$LogFile = $USBDrive + $LogFilename
|
||||||
$version = '2603.2'
|
$version = '2604.1'
|
||||||
WriteLog 'Begin Logging'
|
WriteLog 'Begin Logging'
|
||||||
WriteLog "Script version: $version"
|
WriteLog "Script version: $version"
|
||||||
|
|
||||||
@@ -891,21 +1006,7 @@ else {
|
|||||||
}
|
}
|
||||||
$displayList | Format-Table -AutoSize -Property Disk, 'Size (GB)', Sector, 'Bus Type', Model
|
$displayList | Format-Table -AutoSize -Property Disk, 'Size (GB)', Sector, 'Bus Type', Model
|
||||||
|
|
||||||
do {
|
$diskSelection = Read-MenuSelection -Prompt 'Enter the disk number to apply the FFU to' -InvalidInputMessage 'Invalid disk number. Please select from the available disks.' -ValidSelections $validDiskIndexes
|
||||||
try {
|
|
||||||
$var = $true
|
|
||||||
[int]$diskSelection = Read-Host 'Enter the disk number to apply the FFU to'
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Host 'Input was not in correct format. Please enter a valid disk number'
|
|
||||||
$var = $false
|
|
||||||
}
|
|
||||||
# Validate selected disk is in the list of available disks
|
|
||||||
if ($var -and $validDiskIndexes -notcontains $diskSelection) {
|
|
||||||
Write-Host "Invalid disk number. Please select from the available disks."
|
|
||||||
$var = $false
|
|
||||||
}
|
|
||||||
} until ($var)
|
|
||||||
|
|
||||||
$selectedDisk = $diskDriveCandidates | Where-Object { $_.Index -eq $diskSelection }
|
$selectedDisk = $diskDriveCandidates | Where-Object { $_.Index -eq $diskSelection }
|
||||||
WriteLog "Disk selection: DiskNumber=$($selectedDisk.Index), Model=$($selectedDisk.Model), SizeGB=$([math]::Round(($selectedDisk.Size / 1GB), 2)), BusType=$($selectedDisk.InterfaceType)"
|
WriteLog "Disk selection: DiskNumber=$($selectedDisk.Index), Model=$($selectedDisk.Model), SizeGB=$([math]::Round(($selectedDisk.Size / 1GB), 2)), BusType=$($selectedDisk.InterfaceType)"
|
||||||
@@ -949,18 +1050,8 @@ If ($FFUCount -gt 1) {
|
|||||||
$array += New-Object PSObject -Property $Properties
|
$array += New-Object PSObject -Property $Properties
|
||||||
}
|
}
|
||||||
$array | Format-Table -AutoSize -Property Number, FFUFile | Out-Host
|
$array | Format-Table -AutoSize -Property Number, FFUFile | Out-Host
|
||||||
do {
|
$FFUSelected = Read-MenuSelection -Prompt 'Enter the FFU number to install' -InvalidInputMessage 'Input was not in correct format. Please enter a valid FFU number.' -Minimum 1 -Maximum $FFUCount
|
||||||
try {
|
$FFUSelected = $FFUSelected - 1
|
||||||
$var = $true
|
|
||||||
[int]$FFUSelected = Read-Host 'Enter the FFU number to install'
|
|
||||||
$FFUSelected = $FFUSelected - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
catch {
|
|
||||||
Write-Host 'Input was not in correct format. Please enter a valid FFU number'
|
|
||||||
$var = $false
|
|
||||||
}
|
|
||||||
} until (($FFUSelected -le $FFUCount - 1) -and $var)
|
|
||||||
|
|
||||||
$FFUFileToInstall = $array[$FFUSelected].FFUFile
|
$FFUFileToInstall = $array[$FFUSelected].FFUFile
|
||||||
WriteLog "$FFUFileToInstall was selected"
|
WriteLog "$FFUFileToInstall was selected"
|
||||||
@@ -1023,13 +1114,26 @@ If (Test-Path -Path $UnattendComputerNamePath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Ask for device name if unattend exists
|
$UnattendConfiguredComputerName = $null
|
||||||
If ($Unattend -or $UnattendPrefix -or $UnattendComputerName) {
|
$RequiresLegacyDeviceNamePrompt = $false
|
||||||
|
$RequiresTemplateDeviceName = $false
|
||||||
|
if ($Unattend) {
|
||||||
|
$UnattendConfiguredComputerName = Get-UnattendComputerNameValue
|
||||||
|
$RequiresLegacyDeviceNamePrompt = Test-LegacyPromptComputerName($UnattendConfiguredComputerName)
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($UnattendConfiguredComputerName) -and $UnattendConfiguredComputerName -match '(?i)%serial%') {
|
||||||
|
$RequiresTemplateDeviceName = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Ask for device name if naming is explicitly required
|
||||||
|
If ($UnattendPrefix -or $UnattendComputerName -or $RequiresTemplateDeviceName -or $RequiresLegacyDeviceNamePrompt) {
|
||||||
Write-SectionHeader 'Device Name Selection'
|
Write-SectionHeader 'Device Name Selection'
|
||||||
if ($Unattend -and $UnattendPrefix) {
|
if ($Unattend -and $UnattendPrefix) {
|
||||||
Writelog 'Unattend file found with prefixes.txt. Getting prefixes.'
|
Writelog 'Unattend file found with prefixes.txt. Getting prefixes.'
|
||||||
$UnattendPrefixes = @(Get-content $UnattendPrefixFile)
|
$UnattendPrefixes = @(Get-content $UnattendPrefixFile)
|
||||||
$UnattendPrefixCount = $UnattendPrefixes.Count
|
$UnattendPrefixCount = $UnattendPrefixes.Count
|
||||||
|
$skipPrefixSelection = $false
|
||||||
|
$PrefixToUse = $null
|
||||||
If ($UnattendPrefixCount -gt 1) {
|
If ($UnattendPrefixCount -gt 1) {
|
||||||
WriteLog "Found $UnattendPrefixCount Prefixes"
|
WriteLog "Found $UnattendPrefixCount Prefixes"
|
||||||
$array = @()
|
$array = @()
|
||||||
@@ -1038,20 +1142,18 @@ If ($Unattend -or $UnattendPrefix -or $UnattendComputerName) {
|
|||||||
$array += New-Object PSObject -Property $Properties
|
$array += New-Object PSObject -Property $Properties
|
||||||
}
|
}
|
||||||
$array | Format-Table -AutoSize -Property Number, DeviceNamePrefix
|
$array | Format-Table -AutoSize -Property Number, DeviceNamePrefix
|
||||||
do {
|
$prefixSelection = Read-MenuSelection -Prompt 'Enter the prefix number to use for the device name (0 to skip)' -InvalidInputMessage 'Input was not in correct format. Please enter 0 to skip or a valid prefix number.' -Minimum 1 -Maximum $UnattendPrefixCount -AllowSkip
|
||||||
try {
|
if ($prefixSelection -eq 0) {
|
||||||
$var = $true
|
$skipPrefixSelection = $true
|
||||||
[int]$PrefixSelected = Read-Host 'Enter the prefix number to use for the device name'
|
WriteLog 'User chose to skip device name prefix selection. Existing unattend computer name will remain unchanged.'
|
||||||
$PrefixSelected = $PrefixSelected - 1
|
Write-Host "`nDevice name prefix selection was skipped. The existing unattend computer name will remain unchanged."
|
||||||
}
|
}
|
||||||
catch {
|
else {
|
||||||
Write-Host 'Input was not in correct format. Please enter a valid prefix number'
|
$PrefixSelected = $prefixSelection - 1
|
||||||
$var = $false
|
$PrefixToUse = $array[$PrefixSelected].DeviceNamePrefix
|
||||||
}
|
WriteLog "$PrefixToUse was selected"
|
||||||
} until (($PrefixSelected -le $UnattendPrefixCount - 1) -and $var)
|
Write-Host "`n$PrefixToUse was selected as device name prefix"
|
||||||
$PrefixToUse = $array[$PrefixSelected].DeviceNamePrefix
|
}
|
||||||
WriteLog "$PrefixToUse was selected"
|
|
||||||
Write-Host "`n$PrefixToUse was selected as device name prefix"
|
|
||||||
}
|
}
|
||||||
elseif ($UnattendPrefixCount -eq 1) {
|
elseif ($UnattendPrefixCount -eq 1) {
|
||||||
WriteLog "Found $UnattendPrefixCount Prefix"
|
WriteLog "Found $UnattendPrefixCount Prefix"
|
||||||
@@ -1060,17 +1162,10 @@ If ($Unattend -or $UnattendPrefix -or $UnattendComputerName) {
|
|||||||
WriteLog "Will use $PrefixToUse as device name prefix"
|
WriteLog "Will use $PrefixToUse as device name prefix"
|
||||||
Write-Host "Will use $PrefixToUse as device name prefix"
|
Write-Host "Will use $PrefixToUse as device name prefix"
|
||||||
}
|
}
|
||||||
#Get serial number to append. This can make names longer than 15 characters. Trim any leading or trailing whitespace
|
if (-not $skipPrefixSelection) {
|
||||||
$serial = (Get-CimInstance -ClassName win32_bios).SerialNumber.Trim()
|
$serial = (Get-CimInstance -ClassName win32_bios).SerialNumber.Trim()
|
||||||
#Combine prefix with serial
|
$computername = Set-ConfiguredComputerName($PrefixToUse + $serial)
|
||||||
$computername = ($PrefixToUse + $serial) -replace "\s", "" # Remove spaces because windows does not support spaces in the computer names
|
|
||||||
#If computername is longer than 15 characters, reduce to 15. Sysprep/unattend doesn't like ComputerName being longer than 15 characters even though Windows accepts it
|
|
||||||
If ($computername.Length -gt 15) {
|
|
||||||
$computername = $computername.substring(0, 15)
|
|
||||||
}
|
}
|
||||||
$computername = Set-Computername($computername)
|
|
||||||
Writelog "Computer name will be set to $computername"
|
|
||||||
Write-Host "Computer name will be set to $computername"
|
|
||||||
}
|
}
|
||||||
elseif ($Unattend -and $UnattendComputerName) {
|
elseif ($Unattend -and $UnattendComputerName) {
|
||||||
Writelog 'Unattend file found with SerialComputerNames.csv. Getting name for current computer.'
|
Writelog 'Unattend file found with SerialComputerNames.csv. Getting name for current computer.'
|
||||||
@@ -1080,32 +1175,31 @@ If ($Unattend -or $UnattendPrefix -or $UnattendComputerName) {
|
|||||||
$SCName = $SerialComputerNames | Where-Object { $_.SerialNumber -eq $SerialNumber }
|
$SCName = $SerialComputerNames | Where-Object { $_.SerialNumber -eq $SerialNumber }
|
||||||
|
|
||||||
If ($SCName) {
|
If ($SCName) {
|
||||||
[string]$computername = $SCName.ComputerName
|
[string]$computername = Set-ConfiguredComputerName($SCName.ComputerName)
|
||||||
$computername = Set-Computername($computername)
|
|
||||||
Writelog "Computer name will be set to $computername"
|
|
||||||
Write-Host "Computer name will be set to $computername"
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Writelog 'No matching serial number found in SerialComputerNames.csv. Setting random computer name to complete setup.'
|
Writelog 'No matching serial number found in SerialComputerNames.csv. Setting random computer name to complete setup.'
|
||||||
Write-Host 'No matching serial number found in SerialComputerNames.csv. Setting random computer name to complete setup.'
|
Write-Host 'No matching serial number found in SerialComputerNames.csv. Setting random computer name to complete setup.'
|
||||||
[string]$computername = ("FFU-" + ( -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 11 | ForEach-Object { [char]$_ })))
|
[string]$computername = Set-ConfiguredComputerName(("FFU-" + ( -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 11 | ForEach-Object { [char]$_ }))))
|
||||||
$computername = Set-Computername($computername)
|
|
||||||
Writelog "Computer name will be set to $computername"
|
|
||||||
Write-Host "Computer name will be set to $computername"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elseif ($Unattend) {
|
elseif ($Unattend -and $RequiresTemplateDeviceName) {
|
||||||
|
Writelog 'Unattend file found with a %serial% computer name template. Resolving the template.'
|
||||||
|
$serialNumber = (Get-CimInstance -ClassName Win32_Bios).SerialNumber.Trim()
|
||||||
|
[string]$computername = Set-ConfiguredComputerName((Resolve-ComputerNameTemplate -computerNameTemplate $UnattendConfiguredComputerName -serialNumber $serialNumber))
|
||||||
|
}
|
||||||
|
elseif ($Unattend -and $RequiresLegacyDeviceNamePrompt) {
|
||||||
Writelog 'Unattend file found with no prefixes.txt, asking for name'
|
Writelog 'Unattend file found with no prefixes.txt, asking for name'
|
||||||
Write-Host 'Unattend file found but no prefixes.txt. Please enter a device name.'
|
Write-Host 'Unattend file found but no prefixes.txt. Please enter a device name.'
|
||||||
[string]$computername = Read-Host 'Enter device name'
|
[string]$computername = Set-ConfiguredComputerName((Read-Host 'Enter device name'))
|
||||||
$computername = Set-Computername($computername)
|
|
||||||
Writelog "Computer name will be set to $computername"
|
|
||||||
Write-Host "Computer name will be set to $computername"
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
WriteLog 'Device naming assets detected without unattend.xml. Skipping device naming prompts.'
|
WriteLog 'Device naming assets detected without unattend.xml. Skipping device naming prompts.'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
elseif ($Unattend) {
|
||||||
|
WriteLog 'Unattend file found. Device naming is not required, but unattend settings will still be applied.'
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
WriteLog 'No unattend folder found. Device name will be set via PPKG, AP JSON, or default OS name.'
|
WriteLog 'No unattend folder found. Device name will be set via PPKG, AP JSON, or default OS name.'
|
||||||
}
|
}
|
||||||
@@ -1114,17 +1208,7 @@ else {
|
|||||||
If ($autopilot -eq $true -and $PPKG -eq $true) {
|
If ($autopilot -eq $true -and $PPKG -eq $true) {
|
||||||
WriteLog 'Both PPKG and Autopilot json files found'
|
WriteLog 'Both PPKG and Autopilot json files found'
|
||||||
Write-Host 'Both Autopilot JSON files and Provisioning packages were found.'
|
Write-Host 'Both Autopilot JSON files and Provisioning packages were found.'
|
||||||
do {
|
$APorPPKG = Read-MenuSelection -Prompt 'Enter 1 for Autopilot or 2 for Provisioning Package' -InvalidInputMessage 'Incorrect value. Please enter 1 for Autopilot or 2 for Provisioning Package.' -Minimum 1 -Maximum 2
|
||||||
try {
|
|
||||||
$var = $true
|
|
||||||
[int]$APorPPKG = Read-Host 'Enter 1 for Autopilot or 2 for Provisioning Package'
|
|
||||||
}
|
|
||||||
|
|
||||||
catch {
|
|
||||||
Write-Host 'Incorrect value. Please enter 1 for Autopilot or 2 for Provisioning Package'
|
|
||||||
$var = $false
|
|
||||||
}
|
|
||||||
} until (($APorPPKG -gt 0 -and $APorPPKG -lt 3) -and $var)
|
|
||||||
If ($APorPPKG -eq 1) {
|
If ($APorPPKG -eq 1) {
|
||||||
$PPKG = $false
|
$PPKG = $false
|
||||||
}
|
}
|
||||||
@@ -1143,22 +1227,20 @@ If ($APFilesCount -gt 1 -and $autopilot -eq $true) {
|
|||||||
$array += New-Object PSObject -Property $Properties
|
$array += New-Object PSObject -Property $Properties
|
||||||
}
|
}
|
||||||
$array | Format-Table -AutoSize -Property Number, APFileName
|
$array | Format-Table -AutoSize -Property Number, APFileName
|
||||||
do {
|
$APFileSelection = Read-MenuSelection -Prompt 'Enter the AP json file number to install (0 to skip)' -InvalidInputMessage 'Input was not in correct format. Please enter 0 to skip or a valid AP json file number.' -Minimum 1 -Maximum $APFilesCount -AllowSkip
|
||||||
try {
|
|
||||||
$var = $true
|
|
||||||
[int]$APFileSelected = Read-Host 'Enter the AP json file number to install'
|
|
||||||
$APFileSelected = $APFileSelected - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
catch {
|
if ($APFileSelection -eq 0) {
|
||||||
Write-Host 'Input was not in correct format. Please enter a valid AP json file number'
|
$APFileToInstall = $null
|
||||||
$var = $false
|
$APFileName = $null
|
||||||
}
|
WriteLog 'User chose to skip Autopilot JSON selection.'
|
||||||
} until (($APFileSelected -le $APFilesCount - 1) -and $var)
|
Write-Host "`nAutopilot JSON selection was skipped."
|
||||||
|
}
|
||||||
$APFileToInstall = $array[$APFileSelected].APFile
|
else {
|
||||||
$APFileName = $array[$APFileSelected].APFileName
|
$APFileSelected = $APFileSelection - 1
|
||||||
WriteLog "$APFileToInstall was selected"
|
$APFileToInstall = $array[$APFileSelected].APFile
|
||||||
|
$APFileName = $array[$APFileSelected].APFileName
|
||||||
|
WriteLog "$APFileToInstall was selected"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
elseif ($APFilesCount -eq 1 -and $autopilot -eq $true) {
|
elseif ($APFilesCount -eq 1 -and $autopilot -eq $true) {
|
||||||
WriteLog "Found $APFilesCount AP File"
|
WriteLog "Found $APFilesCount AP File"
|
||||||
@@ -1181,22 +1263,19 @@ If ($PPKGFilesCount -gt 1 -and $PPKG -eq $true) {
|
|||||||
$array += New-Object PSObject -Property $Properties
|
$array += New-Object PSObject -Property $Properties
|
||||||
}
|
}
|
||||||
$array | Format-Table -AutoSize -Property Number, PPKGFileName
|
$array | Format-Table -AutoSize -Property Number, PPKGFileName
|
||||||
do {
|
$PPKGFileSelection = Read-MenuSelection -Prompt 'Enter the PPKG file number to install (0 to skip)' -InvalidInputMessage 'Input was not in correct format. Please enter 0 to skip or a valid PPKG file number.' -Minimum 1 -Maximum $PPKGFilesCount -AllowSkip
|
||||||
try {
|
|
||||||
$var = $true
|
|
||||||
[int]$PPKGFileSelected = Read-Host 'Enter the PPKG file number to install'
|
|
||||||
$PPKGFileSelected = $PPKGFileSelected - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
catch {
|
if ($PPKGFileSelection -eq 0) {
|
||||||
Write-Host 'Input was not in correct format. Please enter a valid PPKG file number'
|
$PPKGFileToInstall = $null
|
||||||
$var = $false
|
WriteLog 'User chose to skip Provisioning Package selection.'
|
||||||
}
|
Write-Host "`nProvisioning Package selection was skipped."
|
||||||
} until (($PPKGFileSelected -le $PPKGFilesCount - 1) -and $var)
|
}
|
||||||
|
else {
|
||||||
$PPKGFileToInstall = $array[$PPKGFileSelected].PPKGFile
|
$PPKGFileSelected = $PPKGFileSelection - 1
|
||||||
WriteLog "$PPKGFileToInstall was selected"
|
$PPKGFileToInstall = $array[$PPKGFileSelected].PPKGFile
|
||||||
Write-Host "`n$PPKGFileToInstall will be used"
|
WriteLog "$PPKGFileToInstall was selected"
|
||||||
|
Write-Host "`n$PPKGFileToInstall will be used"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
elseif ($PPKGFilesCount -eq 1 -and $PPKG -eq $true) {
|
elseif ($PPKGFilesCount -eq 1 -and $PPKG -eq $true) {
|
||||||
Write-SectionHeader -Title 'Provisioning Package Selection'
|
Write-SectionHeader -Title 'Provisioning Package Selection'
|
||||||
@@ -1312,7 +1391,7 @@ if ($null -eq $DriverSourcePath) {
|
|||||||
if ([string]::IsNullOrWhiteSpace($relativeSegment)) {
|
if ([string]::IsNullOrWhiteSpace($relativeSegment)) {
|
||||||
return Split-Path -Path $normalizedPath -Leaf
|
return Split-Path -Path $normalizedPath -Leaf
|
||||||
}
|
}
|
||||||
return $relativePath = $relativeSegment
|
return $relativeSegment
|
||||||
}
|
}
|
||||||
return $normalizedPath
|
return $normalizedPath
|
||||||
}
|
}
|
||||||
@@ -1391,21 +1470,13 @@ if ($null -eq $DriverSourcePath) {
|
|||||||
|
|
||||||
$DriverSelected = -1
|
$DriverSelected = -1
|
||||||
$skipDriverInstall = $false
|
$skipDriverInstall = $false
|
||||||
do {
|
$userSelection = Read-MenuSelection -Prompt 'Enter the number of the driver source to install (0 to skip)' -InvalidInputMessage 'Input was not in correct format. Please enter 0 to skip or a valid number.' -Minimum 1 -Maximum $DriverSourcesCount -AllowSkip
|
||||||
try {
|
if ($userSelection -eq 0) {
|
||||||
$var = $true
|
$skipDriverInstall = $true
|
||||||
[int]$userSelection = Read-Host 'Enter the number of the driver source to install (0 to skip)'
|
}
|
||||||
if ($userSelection -eq 0) {
|
else {
|
||||||
$skipDriverInstall = $true
|
$DriverSelected = $userSelection - 1
|
||||||
break
|
}
|
||||||
}
|
|
||||||
$DriverSelected = $userSelection - 1
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Host 'Input was not in correct format. Please enter a valid number.'
|
|
||||||
$var = $false
|
|
||||||
}
|
|
||||||
} until ((($DriverSelected -ge 0 -and $DriverSelected -lt $DriverSourcesCount) -or $skipDriverInstall) -and $var)
|
|
||||||
|
|
||||||
if ($skipDriverInstall) {
|
if ($skipDriverInstall) {
|
||||||
$DriverSourcePath = $null
|
$DriverSourcePath = $null
|
||||||
@@ -1568,8 +1639,9 @@ If ($PPKGFileToInstall) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#Set DeviceName
|
#Set DeviceName
|
||||||
If ($computername) {
|
If ($Unattend) {
|
||||||
Write-SectionHeader -Title 'Applying Computer Name and Unattend Configuration'
|
$unattendSectionTitle = if ($computername) { 'Applying Computer Name and Unattend Configuration' } else { 'Applying Unattend Configuration' }
|
||||||
|
Write-SectionHeader -Title $unattendSectionTitle
|
||||||
try {
|
try {
|
||||||
$PantherDir = 'w:\windows\panther'
|
$PantherDir = 'w:\windows\panther'
|
||||||
If (Test-Path -Path $PantherDir) {
|
If (Test-Path -Path $PantherDir) {
|
||||||
@@ -1590,8 +1662,8 @@ If ($computername) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
WriteLog "Copying Unattend.xml to name device failed"
|
WriteLog 'Copying Unattend.xml to Panther failed'
|
||||||
Stop-Script -Message "Copying Unattend.xml to name device failed with error: $_"
|
Stop-Script -Message "Copying Unattend.xml to Panther failed with error: $_"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
SerialNumber,ComputerName
|
||||||
|
ABC12345,CORP-001
|
||||||
|
DEF67890,KIOSK-010
|
||||||
|
XYZ24680,STORE-015
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
<settings pass="specialize">
|
<settings pass="specialize">
|
||||||
<!--<ComputerName> must be in the first Component Element "Microsoft-Windows-Shell-Setup" . Do not change the order or remove it -->
|
<!--<ComputerName> must be in the first Component Element "Microsoft-Windows-Shell-Setup" . Do not change the order or remove it -->
|
||||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
<ComputerName>MYCOMPUTER</ComputerName><!--Leave Default will be renamed-->
|
<ComputerName>*</ComputerName><!--Leave Default will be renamed-->
|
||||||
<TimeZone>Eastern Standard Time</TimeZone><!--Add Your Local TimeZone-->
|
<TimeZone>Eastern Standard Time</TimeZone><!--Add Your Local TimeZone-->
|
||||||
</component>
|
</component>
|
||||||
<!-- Place additional Components Elements and Settings below here: -->
|
<!-- Place additional Components Elements and Settings below here: -->
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<settings pass="specialize">
|
<settings pass="specialize">
|
||||||
<!--<ComputerName> must be in the first Component Element "Microsoft-Windows-Shell-Setup" . Do not change the order or remove it -->
|
<!--<ComputerName> must be in the first Component Element "Microsoft-Windows-Shell-Setup" . Do not change the order or remove it -->
|
||||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="arm64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="arm64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
<ComputerName>MyComputer</ComputerName>
|
<ComputerName>*</ComputerName>
|
||||||
</component>
|
</component>
|
||||||
<!--Place addtional Components Elements and settings below here. -->
|
<!--Place addtional Components Elements and settings below here. -->
|
||||||
</settings>
|
</settings>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<settings pass="specialize">
|
<settings pass="specialize">
|
||||||
<!--<ComputerName> must be in the first Component Element "Microsoft-Windows-Shell-Setup" . Do not change the order or remove it -->
|
<!--<ComputerName> must be in the first Component Element "Microsoft-Windows-Shell-Setup" . Do not change the order or remove it -->
|
||||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
<ComputerName>MyComputer</ComputerName>
|
<ComputerName>*</ComputerName>
|
||||||
</component>
|
</component>
|
||||||
<!--Place addtional Components Elements and settings below here. -->
|
<!--Place addtional Components Elements and settings below here. -->
|
||||||
</settings>
|
</settings>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ parent: UI Overview
|
|||||||
---
|
---
|
||||||
# M365 Apps/Office
|
# M365 Apps/Office
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
FFU Builder uses the Office Deployment Toolkit (ODT) to install Office. In the `.\FFUDevelopment\Apps\Office` folder you'll find two files:
|
FFU Builder uses the Office Deployment Toolkit (ODT) to install Office. In the `.\FFUDevelopment\Apps\Office` folder you'll find two files:
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ has_toc: false
|
|||||||
---
|
---
|
||||||
# Applications
|
# Applications
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Applications can be installed in three different ways:
|
Applications can be installed in three different ways:
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ grand_parent: UI Overview
|
|||||||
---
|
---
|
||||||
# Apps Script Variables
|
# Apps Script Variables
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Apps Script Variables are key value pairs that are used to create a hashtable that is passed to the `BuildFFUVM.ps1` script (stored in the `$AppScriptVariables` parameter as a hashtable). At build time, `BuildFFUVM.ps1` will export the `$AppsScriptVariables` hashtable to an `AppsScriptVariables.json` file in the `$OrchestrationPath` folder (`$AppsPath\Orchestration`). You can also manually create your own `AppsScriptVariables.json `file and place it in the `$AppsPath\Orchestration` folder.
|
Apps Script Variables are key value pairs that are used to create a hashtable that is passed to the `BuildFFUVM.ps1` script (stored in the `$AppScriptVariables` parameter as a hashtable). At build time, `BuildFFUVM.ps1` will export the `$AppsScriptVariables` hashtable to an `AppsScriptVariables.json` file in the `$OrchestrationPath` folder (`$AppsPath\Orchestration`). You can also manually create your own `AppsScriptVariables.json `file and place it in the `$AppsPath\Orchestration` folder.
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ In the VM, the `Orchestrator.ps1` file will call `Invoke-AppsScript.ps1` if `App
|
|||||||
This allows for you to create a dynamic task sequence via a PowerShell script with simple if statements to run apps, commands, etc. This is all driven by your `FFUConfig.json` file. For example, the following `FFUConfig.json` file contains an AppsScriptVariables hashtable of foo and vmwaretools like the screenshot above. If you build servers that require vmware tools, you may set the value to true. However there may be situations where you don't need vmwaretools installed. If that's the case, you set vmwaretools to false. This allows for your `Invoke-AppsScript.ps1` file to stay the same and all you have to do is adjust the variables.
|
This allows for you to create a dynamic task sequence via a PowerShell script with simple if statements to run apps, commands, etc. This is all driven by your `FFUConfig.json` file. For example, the following `FFUConfig.json` file contains an AppsScriptVariables hashtable of foo and vmwaretools like the screenshot above. If you build servers that require vmware tools, you may set the value to true. However there may be situations where you don't need vmwaretools installed. If that's the case, you set vmwaretools to false. This allows for your `Invoke-AppsScript.ps1` file to stay the same and all you have to do is adjust the variables.
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
s{
|
||||||
"AdditionalFFUFiles": [],
|
"AdditionalFFUFiles": [],
|
||||||
"AllowExternalHardDiskMedia": false,
|
"AllowExternalHardDiskMedia": false,
|
||||||
"AllowVHDXCaching": false,
|
"AllowVHDXCaching": false,
|
||||||
@@ -57,7 +57,6 @@ This allows for you to create a dynamic task sequence via a PowerShell script wi
|
|||||||
},
|
},
|
||||||
"BuildUSBDrive": false,
|
"BuildUSBDrive": false,
|
||||||
"CleanupAppsISO": true,
|
"CleanupAppsISO": true,
|
||||||
"CleanupCaptureISO": true,
|
|
||||||
"CleanupDeployISO": true,
|
"CleanupDeployISO": true,
|
||||||
"CleanupDrivers": false,
|
"CleanupDrivers": false,
|
||||||
"CompactOS": true,
|
"CompactOS": true,
|
||||||
@@ -69,7 +68,6 @@ This allows for you to create a dynamic task sequence via a PowerShell script wi
|
|||||||
"CopyPEDrivers": false,
|
"CopyPEDrivers": false,
|
||||||
"CopyPPKG": false,
|
"CopyPPKG": false,
|
||||||
"CopyUnattend": false,
|
"CopyUnattend": false,
|
||||||
"CreateCaptureMedia": true,
|
|
||||||
"CreateDeploymentMedia": true,
|
"CreateDeploymentMedia": true,
|
||||||
"CustomFFUNameTemplate": "{WindowsRelease}_{WindowsVersion}_{SKU}_{yyyy}-{MM}-{dd}_{HH}{mm}",
|
"CustomFFUNameTemplate": "{WindowsRelease}_{WindowsVersion}_{SKU}_{yyyy}-{MM}-{dd}_{HH}{mm}",
|
||||||
"Disksize": 53687091200,
|
"Disksize": 53687091200,
|
||||||
@@ -101,7 +99,6 @@ This allows for you to create a dynamic task sequence via a PowerShell script wi
|
|||||||
"RemoveApps": false,
|
"RemoveApps": false,
|
||||||
"RemoveFFU": false,
|
"RemoveFFU": false,
|
||||||
"RemoveUpdates": false,
|
"RemoveUpdates": false,
|
||||||
"ShareName": "FFUCaptureShare",
|
|
||||||
"Threads": 5,
|
"Threads": 5,
|
||||||
"UpdateADK": true,
|
"UpdateADK": true,
|
||||||
"UpdateEdge": true,
|
"UpdateEdge": true,
|
||||||
@@ -115,9 +112,7 @@ This allows for you to create a dynamic task sequence via a PowerShell script wi
|
|||||||
"USBDriveList": {},
|
"USBDriveList": {},
|
||||||
"UseDriversAsPEDrivers": false,
|
"UseDriversAsPEDrivers": false,
|
||||||
"UserAppListPath": "C:\\FFUDevelopment\\Apps\\UserAppList.json",
|
"UserAppListPath": "C:\\FFUDevelopment\\Apps\\UserAppList.json",
|
||||||
"Username": "ffu_user",
|
|
||||||
"Verbose": false,
|
"Verbose": false,
|
||||||
"VMHostIPAddress": "192.168.1.169",
|
|
||||||
"VMLocation": "C:\\FFUDevelopment\\VM",
|
"VMLocation": "C:\\FFUDevelopment\\VM",
|
||||||
"VMSwitchName": "External",
|
"VMSwitchName": "External",
|
||||||
"WindowsArch": "x64",
|
"WindowsArch": "x64",
|
||||||
@@ -133,6 +128,4 @@ Example command line to run with vmwaretools set to false and foo set to foo. Th
|
|||||||
|
|
||||||
`.\BuildFFUVM.ps1 -configFile 'C:\FFUDevelopment\config\FFUConfig.json' -appsScriptVariables @{foo='foo'; vmwaretools='false'}`
|
`.\BuildFFUVM.ps1 -configFile 'C:\FFUDevelopment\config\FFUConfig.json' -appsScriptVariables @{foo='foo'; vmwaretools='false'}`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% include page_nav.html %}
|
{% include page_nav.html %}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ grand_parent: UI Overview
|
|||||||
---
|
---
|
||||||
# Bring Your Own Applications
|
# Bring Your Own Applications
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Bring Your Own Applications allows you to run any command line you want in the virtual machine to include in your FFU to install an application, run a script, etc. As the name implies, you'll provide the content, command line, arguments, and additional exit codes.
|
Bring Your Own Applications allows you to run any command line you want in the virtual machine to include in your FFU to install an application, run a script, etc. As the name implies, you'll provide the content, command line, arguments, and additional exit codes.
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ parent: Helper Scripts
|
|||||||
---
|
---
|
||||||
# Create PE Media
|
# Create PE Media
|
||||||
|
|
||||||
`Create-PEMedia.ps1` is a standalone helper script that creates WinPE capture or deployment ISO files outside the main build flow.
|
`Create-PEMedia.ps1` is a standalone helper script that creates WinPE deployment ISO files outside the main build flow.
|
||||||
|
|
||||||
This is useful when admins need to quickly generate a deploy ISO for a share (or local staging folder) that technicians will use with `USBImagingToolCreator.ps1`.
|
This is useful when admins need to quickly generate a deploy ISO for a share (or local staging folder) that technicians will use with `USBImagingToolCreator.ps1`.
|
||||||
|
|
||||||
@@ -40,25 +40,19 @@ Default output file:
|
|||||||
Create deploy ISO for x64:
|
Create deploy ISO for x64:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\Create-PEMedia.ps1 -Deploy $true -WindowsArch 'x64'
|
.\Create-PEMedia.ps1 -WindowsArch 'x64'
|
||||||
```
|
```
|
||||||
|
|
||||||
Create deploy ISO for ARM64:
|
Create deploy ISO for ARM64:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\Create-PEMedia.ps1 -Deploy $true -WindowsArch 'arm64' -DeployISO "$PSScriptRoot\WinPE_FFU_Deploy_arm64.iso"
|
.\Create-PEMedia.ps1 -WindowsArch 'arm64' -DeployISO "$PSScriptRoot\WinPE_FFU_Deploy_arm64.iso"
|
||||||
```
|
|
||||||
|
|
||||||
Create capture ISO only:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
.\Create-PEMedia.ps1 -Capture $true -Deploy $false
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Create deploy ISO and include PE drivers from `.\PEDrivers`:
|
Create deploy ISO and include PE drivers from `.\PEDrivers`:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\Create-PEMedia.ps1 -Deploy $true -CopyPEDrivers $true
|
.\Create-PEMedia.ps1 -CopyPEDrivers $true
|
||||||
```
|
```
|
||||||
|
|
||||||
## Stage output for USB imaging
|
## Stage output for USB imaging
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ parent: UI Overview
|
|||||||
---
|
---
|
||||||
# Drivers
|
# Drivers
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
FFU Builder supports adding drivers directly to the FFU file at build time, or adding them as folders on your USB drive which can be serviced offline after the FFU has been applied to your device.
|
FFU Builder supports adding drivers directly to the FFU file at build time, or adding them as folders on your USB drive which can be serviced offline after the FFU has been applied to your device.
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ To find the Machine Type for Lenovo devices, check the bottom/back of the device
|
|||||||
|
|
||||||
You can multi-select different models within the same make, or mix and match different makes. The screenshot below shows different Dell, HP, Lenovo, and Microsoft models selected
|
You can multi-select different models within the same make, or mix and match different makes. The screenshot below shows different Dell, HP, Lenovo, and Microsoft models selected
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Save Drivers.json
|
## Save Drivers.json
|
||||||
|
|
||||||
|
|||||||
@@ -9,17 +9,19 @@ parent: UI Overview
|
|||||||
---
|
---
|
||||||
# Hyper-V Settings
|
# Hyper-V Settings
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Enable VM Networking (Experimental)
|
||||||
|
|
||||||
|
Controls whether the build VM is connected to a Hyper-V switch during provisioning.
|
||||||
|
|
||||||
|
Leave this off for the default offline build path. Turn it on only if you want to test internet-connected builds and understand there may be Sysprep or capture issues.
|
||||||
|
|
||||||
## VM Switch Name
|
## VM Switch Name
|
||||||
|
|
||||||
Drop down of detected VM Switches. There's also an **Other** option which allows you to specify a VM Switch Name. The other option is useful in scenarios where the machine you're running the UI from isn't going to be the machine where you plan to build the FFU from.
|
Drop down of detected VM Switches. There's also an **Other** option which allows you to specify a VM Switch Name. The other option is useful in scenarios where the machine you're running the UI from isn't going to be the machine where you plan to build the FFU from.
|
||||||
|
|
||||||
## VM Host IP Address
|
This setting is only used when **Enable VM Networking (Experimental)** is turned on. VM-based builds still capture from the host-side VHDX after the VM shuts down, so you only need a switch when the VM requires network connectivity during provisioning.
|
||||||
|
|
||||||
IP address of the selected Hyper-V switch that will be used for FFU capture. The UI will auto-detected this based on the VM Switch that was selected.
|
|
||||||
|
|
||||||
If `$InstallApps` is set to `$true`, this parameter must be configured.
|
|
||||||
|
|
||||||
## Disk Size (GB)
|
## Disk Size (GB)
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 281 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 247 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 374 KiB |
|
After Width: | Height: | Size: 263 KiB |
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 211 KiB |
|
After Width: | Height: | Size: 293 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 330 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -7,7 +7,7 @@ parent: UI Overview
|
|||||||
---
|
---
|
||||||
# Monitor
|
# Monitor
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
The monitor tab parses the `.\FFUDevelopment\FFUDevelopment.log` file. This makes it easy to track what's happening during the FFU build process.
|
The monitor tab parses the `.\FFUDevelopment\FFUDevelopment.log` file. This makes it easy to track what's happening during the FFU build process.
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ This table lists all top-level parameters in BuildFFUVM.ps1.
|
|||||||
| -BuildUSBDrive | bool | Build USB Drive | When set to $true, will partition and format a USB drive and copy the captured FFU to the drive. |
|
| -BuildUSBDrive | bool | Build USB Drive | When set to $true, will partition and format a USB drive and copy the captured FFU to the drive. |
|
||||||
| -Cleanup | switch | Monitor cancel build action (no direct control) | Switch to run cleanup-only mode. When specified, the script performs cleanup and exits without starting a new build. |
|
| -Cleanup | switch | Monitor cancel build action (no direct control) | Switch to run cleanup-only mode. When specified, the script performs cleanup and exits without starting a new build. |
|
||||||
| -CleanupAppsISO | bool | Cleanup Apps ISO | When set to $true, will remove the Apps ISO after the FFU has been captured. Default is $true. |
|
| -CleanupAppsISO | bool | Cleanup Apps ISO | When set to $true, will remove the Apps ISO after the FFU has been captured. Default is $true. |
|
||||||
| -CleanupCaptureISO | bool | Cleanup Capture ISO | When set to $true, will remove the WinPE capture ISO after the FFU has been captured. Default is $true. |
|
|
||||||
| -CleanupCurrentRunDownloads | bool | Monitor cancel prompt option (no direct control) | When set to $true, cleanup mode will remove downloads created during the current run and restore backed up run JSON files. Default is $false. |
|
| -CleanupCurrentRunDownloads | bool | Monitor cancel prompt option (no direct control) | When set to $true, cleanup mode will remove downloads created during the current run and restore backed up run JSON files. Default is $false. |
|
||||||
| -CleanupDeployISO | bool | Cleanup Deploy ISO | When set to $true, will remove the WinPE deployment ISO after the FFU has been captured. Default is $true. |
|
| -CleanupDeployISO | bool | Cleanup Deploy ISO | When set to $true, will remove the WinPE deployment ISO after the FFU has been captured. Default is $true. |
|
||||||
| -CleanupDrivers | bool | Cleanup Drivers | When set to $true, will remove the drivers folder after the FFU has been captured. Default is $true. |
|
| -CleanupDrivers | bool | Cleanup Drivers | When set to $true, will remove the drivers folder after the FFU has been captured. Default is $true. |
|
||||||
@@ -39,19 +38,25 @@ This table lists all top-level parameters in BuildFFUVM.ps1.
|
|||||||
| -CopyDrivers | bool | Copy Drivers to USB drive | When set to $true, will copy the drivers from the $FFUDevelopmentPath\Drivers folder to the Drivers folder on the deploy partition of the USB drive. Default is $false. |
|
| -CopyDrivers | bool | Copy Drivers to USB drive | When set to $true, will copy the drivers from the $FFUDevelopmentPath\Drivers folder to the Drivers folder on the deploy partition of the USB drive. Default is $false. |
|
||||||
| -CopyPEDrivers | bool | Copy PE Drivers | When set to $true, enables adding WinPE drivers. By default copies drivers from $FFUDevelopmentPath\PEDrivers to the WinPE deployment media unless -UseDriversAsPEDrivers is also $true. |
|
| -CopyPEDrivers | bool | Copy PE Drivers | When set to $true, enables adding WinPE drivers. By default copies drivers from $FFUDevelopmentPath\PEDrivers to the WinPE deployment media unless -UseDriversAsPEDrivers is also $true. |
|
||||||
| -CopyPPKG | bool | Copy Provisioning Package | When set to $true, will copy the provisioning package from the $FFUDevelopmentPath\PPKG folder to the Deployment partition of the USB drive. Default is $false. |
|
| -CopyPPKG | bool | Copy Provisioning Package | When set to $true, will copy the provisioning package from the $FFUDevelopmentPath\PPKG folder to the Deployment partition of the USB drive. Default is $false. |
|
||||||
| -CopyUnattend | bool | Copy Unattend.xml | When set to $true, will copy the $FFUDevelopmentPath\Unattend folder to the Deployment partition of the USB drive. Default is $false. |
|
| -CopyUnattend | bool | Copy Unattend.xml | When set to $true, stages the selected architecture-specific unattend XML file as Unattend.xml on the Deployment partition of the USB drive. Cannot be used together with -InjectUnattend. Default is $false. |
|
||||||
| -CreateCaptureMedia | bool | Create Capture Media | When set to $true, this will create WinPE capture media for use when $InstallApps is set to $true. This capture media will be automatically attached to the VM, and the boot order will be changed to automate the capture of the FFU. |
|
|
||||||
| -CreateDeploymentMedia | bool | Create Deployment Media | When set to $true, this will create WinPE deployment media for use when deploying to a physical device. |
|
| -CreateDeploymentMedia | bool | Create Deployment Media | When set to $true, this will create WinPE deployment media for use when deploying to a physical device. |
|
||||||
| -CustomFFUNameTemplate | string | Custom FFU Name Template | Sets a custom FFU output name with placeholders. Allowed placeholders are: {WindowsRelease}, {WindowsVersion}, {SKU}, {BuildDate}, {yyyy}, {MM}, {dd}, {H}, {hh}, {mm}, {tt}. |
|
| -CustomFFUNameTemplate | string | Custom FFU Name Template | Sets a custom FFU output name with placeholders. Allowed placeholders are: {WindowsRelease}, {WindowsVersion}, {SKU}, {BuildDate}, {yyyy}, {MM}, {dd}, {H}, {hh}, {mm}, {tt}. |
|
||||||
|
| -DeviceNamePrefixes | string[] | Specify a list of Prefixes | Sets the prefixes used when DeviceNamingMode is Prefixes. Each entry becomes a line in prefixes.txt on the deployment media. |
|
||||||
|
| -DeviceNamePrefixesPath | string | Prefixes File Path | Path to the source prefixes file used for legacy copy or when -DeviceNamePrefixes is not supplied. Default is $FFUDevelopmentPath\Unattend\prefixes.txt. |
|
||||||
|
| -DeviceNameSerialComputerNames | string[] | Specify Serial to Device Name Mapping | Sets the CSV content used when DeviceNamingMode is SerialComputerNames. The content must include SerialNumber and ComputerName headers, and the staged file is written as SerialComputerNames.csv on the deployment media. |
|
||||||
|
| -DeviceNameSerialComputerNamesPath | string | Serial Computer Names CSV Mapping File Path | Path to the source CSV file used when DeviceNamingMode is SerialComputerNames and -DeviceNameSerialComputerNames is not supplied. Default is $FFUDevelopmentPath\Unattend\SerialComputerNames.csv. |
|
||||||
|
| -DeviceNameTemplate | string | Specify Device Name | Sets the device name used when DeviceNamingMode is Template. Supports a static name or the %serial% token when -CopyUnattend is used. |
|
||||||
|
| -DeviceNamingMode | string | Device Naming expander | Controls how device naming is handled when unattend content is copied to USB media or injected into the FFU. Accepted values are Legacy, None, Prompt, Template, Prefixes, and SerialComputerNames. The UI shows None, Prompt, Template, Prefixes, and SerialComputerNames. When device naming is left untouched in the UI, the generated config does not write DeviceNamingMode, which preserves the script default of Legacy. Prompt rewrites the staged deployment unattend to the existing manual prompt placeholder and requires -CopyUnattend. Prefixes writes prefixes.txt and requires -CopyUnattend. SerialComputerNames writes SerialComputerNames.csv and requires -CopyUnattend. |
|
||||||
| -Disksize | uint64 | Disk Size (GB) | Size of the virtual hard disk for the virtual machine. Default is a 50GB dynamic disk. |
|
| -Disksize | uint64 | Disk Size (GB) | Size of the virtual hard disk for the virtual machine. Default is a 50GB dynamic disk. |
|
||||||
| -DriversFolder | string | Drivers Folder | Path to the drivers folder. Default is $FFUDevelopmentPath\Drivers. |
|
| -DriversFolder | string | Drivers Folder | Path to the drivers folder. Default is $FFUDevelopmentPath\Drivers. |
|
||||||
| -DriversJsonPath | string | Drivers.json Path | Path to a JSON file that specifies which drivers to download. |
|
| -DriversJsonPath | string | Drivers.json Path | Path to a JSON file that specifies which drivers to download. |
|
||||||
|
| -EnableVMNetworking | bool | Enable VM Networking (Experimental) | When set to $true, connects the build VM to the selected Hyper-V switch during provisioning. Default is $false because internet-connected Sysprep is experimental. |
|
||||||
| -ExportConfigFile | string | Save Config File | Path to a JSON file to export the parameters used for the script. |
|
| -ExportConfigFile | string | Save Config File | Path to a JSON file to export the parameters used for the script. |
|
||||||
| -FFUCaptureLocation | string | FFU Capture Location | Path to the folder where the captured FFU will be stored. Default is $FFUDevelopmentPath\FFU. |
|
| -FFUCaptureLocation | string | FFU Capture Location | Path to the folder where the captured FFU will be stored. Default is $FFUDevelopmentPath\FFU. |
|
||||||
| -FFUDevelopmentPath | string | FFU Development Path | Path to the FFU development folder. Default is $PSScriptRoot. |
|
| -FFUDevelopmentPath | string | FFU Development Path | Path to the FFU development folder. Default is $PSScriptRoot. |
|
||||||
| -FFUPrefix | string | VM Name Prefix | Prefix for the generated FFU file. Default is _FFU. |
|
| -FFUPrefix | string | VM Name Prefix | Prefix for the generated FFU file. Default is _FFU. |
|
||||||
| -Headers | hashtable | CLI only (no UI control) | Headers to use when downloading files. Not recommended to modify. |
|
| -Headers | hashtable | CLI only (no UI control) | Headers to use when downloading files. Not recommended to modify. |
|
||||||
| -InjectUnattend | bool | Inject Unattend.xml | When set to $true and InstallApps is also $true, copies unattend_[arch].xml from $FFUDevelopmentPath\unattend to $FFUDevelopmentPath\Apps\Unattend\Unattend.xml so sysprep can use it inside the VM. Default is $false. |
|
| -InjectUnattend | bool | Inject Unattend.xml | When set to $true and InstallApps is also $true, stages the selected architecture-specific unattend XML file to $FFUDevelopmentPath\Apps\Unattend\Unattend.xml so sysprep can use it inside the VM. Cannot be used together with -CopyUnattend. Default is $false. |
|
||||||
| -InstallApps | bool | Install Applications | When set to $true, the script will create an Apps.iso file from the $FFUDevelopmentPath\Apps folder. It will also create a VM, mount the Apps.iso, install the apps, sysprep, and capture the VM. When set to $false, the FFU is created from a VHDX file, and no VM is created. |
|
| -InstallApps | bool | Install Applications | When set to $true, the script will create an Apps.iso file from the $FFUDevelopmentPath\Apps folder. It will also create a VM, mount the Apps.iso, install the apps, sysprep, and capture the VM. When set to $false, the FFU is created from a VHDX file, and no VM is created. |
|
||||||
| -InstallDrivers | bool | Install Drivers to FFU | Install device drivers from the specified $FFUDevelopmentPath\Drivers folder if set to $true. Download the drivers and put them in the Drivers folder. The script will recurse the drivers folder and add the drivers to the FFU. |
|
| -InstallDrivers | bool | Install Drivers to FFU | Install device drivers from the specified $FFUDevelopmentPath\Drivers folder if set to $true. Download the drivers and put them in the Drivers folder. The script will recurse the drivers folder and add the drivers to the FFU. |
|
||||||
| -InstallOffice | bool | Install Office | Install Microsoft Office if set to $true. The script will download the latest ODT and Office files in the $FFUDevelopmentPath\Apps\Office folder and install Office in the FFU via VM. |
|
| -InstallOffice | bool | Install Office | Install Microsoft Office if set to $true. The script will download the latest ODT and Office files in the $FFUDevelopmentPath\Apps\Office folder and install Office in the FFU via VM. |
|
||||||
@@ -71,10 +76,12 @@ This table lists all top-level parameters in BuildFFUVM.ps1.
|
|||||||
| -ProductKey | string | Product Key | Product key for the Windows edition specified in WindowsSKU. This will overwrite whatever SKU is entered for WindowsSKU. Recommended to use if you want to use a MAK or KMS key to activate Enterprise or Education. If using VL media instead of consumer media, you'll want to enter a MAK or KMS key here. |
|
| -ProductKey | string | Product Key | Product key for the Windows edition specified in WindowsSKU. This will overwrite whatever SKU is entered for WindowsSKU. Recommended to use if you want to use a MAK or KMS key to activate Enterprise or Education. If using VL media instead of consumer media, you'll want to enter a MAK or KMS key here. |
|
||||||
| -PromptExternalHardDiskMedia | bool | Prompt for External Hard Disk Media | When set to $true, will prompt the user to confirm the use of media identified as External Hard Disk media via WMI class Win32_DiskDrive. Default is $true. |
|
| -PromptExternalHardDiskMedia | bool | Prompt for External Hard Disk Media | When set to $true, will prompt the user to confirm the use of media identified as External Hard Disk media via WMI class Win32_DiskDrive. Default is $true. |
|
||||||
| -RemoveApps | bool | Remove Apps Folder Content | When set to $true, will remove the application content in the Apps folder after the FFU has been captured. Default is $true. |
|
| -RemoveApps | bool | Remove Apps Folder Content | When set to $true, will remove the application content in the Apps folder after the FFU has been captured. Default is $true. |
|
||||||
|
| -RemoveDownloadedESD | bool | Remove Downloaded ESD file(s) | When set to $true, downloaded Windows ESD files are automatically deleted after they have been applied. Default is $true. |
|
||||||
| -RemoveFFU | bool | Remove FFU | When set to $true, will remove the FFU file from the $FFUDevelopmentPath\FFU folder after it has been copied to the USB drive. Default is $false. |
|
| -RemoveFFU | bool | Remove FFU | When set to $true, will remove the FFU file from the $FFUDevelopmentPath\FFU folder after it has been copied to the USB drive. Default is $false. |
|
||||||
| -RemoveUpdates | bool | Remove Downloaded Update Files | When set to $true, will remove the downloaded CU, MSRT, Defender, Edge, OneDrive, and .NET files downloaded. Default is $true. |
|
| -RemoveUpdates | bool | Remove Downloaded Update Files | When set to $true, will remove the downloaded CU, MSRT, Defender, Edge, OneDrive, and .NET files downloaded. Default is $true. |
|
||||||
| -ShareName | string | Share Name | Name of the shared folder for FFU capture. The default is FFUCaptureShare. This share will be created with rights for the user account. When finished, the share will be removed. |
|
|
||||||
| -Threads | int | Threads | Controls the throttle applied to parallel tasks inside the script. Default is 5, matching the UI Threads field, and applies to driver downloads invoked through Invoke-ParallelProcessing. |
|
| -Threads | int | Threads | Controls the throttle applied to parallel tasks inside the script. Default is 5, matching the UI Threads field, and applies to driver downloads invoked through Invoke-ParallelProcessing. |
|
||||||
|
| -UnattendArm64FilePath | string | arm64 Unattend File Path | Path to the arm64 unattend XML source file used by Copy Unattend.xml and Inject Unattend.xml. Default is $FFUDevelopmentPath\Unattend\unattend_arm64.xml. |
|
||||||
|
| -UnattendX64FilePath | string | x64 Unattend File Path | Path to the x64 unattend XML source file used by Copy Unattend.xml and Inject Unattend.xml. Default is $FFUDevelopmentPath\Unattend\unattend_x64.xml. |
|
||||||
| -UpdateADK | bool | Update ADK | When set to $true, the script will check for and install the latest Windows ADK and WinPE add-on if they are not already installed or up-to-date. Default is $true. |
|
| -UpdateADK | bool | Update ADK | When set to $true, the script will check for and install the latest Windows ADK and WinPE add-on if they are not already installed or up-to-date. Default is $true. |
|
||||||
| -UpdateEdge | bool | Update Edge | When set to $true, will download and install the latest Microsoft Edge. Default is $false. |
|
| -UpdateEdge | bool | Update Edge | When set to $true, will download and install the latest Microsoft Edge. Default is $false. |
|
||||||
| -UpdateLatestCU | bool | Update Latest Cumulative Update | When set to $true, will download and install the latest cumulative update. Default is $false. |
|
| -UpdateLatestCU | bool | Update Latest Cumulative Update | When set to $true, will download and install the latest cumulative update. Default is $false. |
|
||||||
@@ -88,10 +95,8 @@ This table lists all top-level parameters in BuildFFUVM.ps1.
|
|||||||
| -UseDriversAsPEDrivers | bool | Use Drivers Folder as PE Drivers Source | When set to $true (and -CopyPEDrivers is also $true), bypasses the contents of $FFUDevelopmentPath\PEDrivers and instead builds the WinPE driver set dynamically from the $DriversFolder path, copying only the required WinPE drivers. Has no effect if -CopyPEDrivers is not specified. Default is $false. |
|
| -UseDriversAsPEDrivers | bool | Use Drivers Folder as PE Drivers Source | When set to $true (and -CopyPEDrivers is also $true), bypasses the contents of $FFUDevelopmentPath\PEDrivers and instead builds the WinPE driver set dynamically from the $DriversFolder path, copying only the required WinPE drivers. Has no effect if -CopyPEDrivers is not specified. Default is $false. |
|
||||||
| -UserAgent | string | CLI only (no UI control) | User agent string to use when downloading files. |
|
| -UserAgent | string | CLI only (no UI control) | User agent string to use when downloading files. |
|
||||||
| -UserAppListPath | string | Application Path (derived UserAppList.json) | Path to a JSON file containing a list of user-defined applications to install. Default is $FFUDevelopmentPath\Apps\UserAppList.json. |
|
| -UserAppListPath | string | Application Path (derived UserAppList.json) | Path to a JSON file containing a list of user-defined applications to install. Default is $FFUDevelopmentPath\Apps\UserAppList.json. |
|
||||||
| -Username | string | Username | Username for accessing the shared folder. The default is ffu_user. The script will auto-create the account and password. When finished, it will remove the account. |
|
|
||||||
| -VMHostIPAddress | string | VM Host IP Address | IP address of the Hyper-V host for FFU capture. If $InstallApps is set to $true, this parameter must be configured. You must manually configure this, or use the UI to auto-detect. |
|
|
||||||
| -VMLocation | string | VM Location | Default is $FFUDevelopmentPath\VM. This is the location of the VHDX that gets created where Windows will be installed to. |
|
| -VMLocation | string | VM Location | Default is $FFUDevelopmentPath\VM. This is the location of the VHDX that gets created where Windows will be installed to. |
|
||||||
| -VMSwitchName | string | VM Switch Name + Custom VM Switch Name (when Other selected) | Name of the Hyper-V virtual switch. If $InstallApps is set to $true, this must be set to capture the FFU from the VM. |
|
| -VMSwitchName | string | VM Switch Name + Custom VM Switch Name (when Other selected) | Name of the Hyper-V virtual switch used when -EnableVMNetworking is set to $true. |
|
||||||
| -WindowsArch | string | Windows Architecture | String value of 'x86', 'x64', or 'arm64'. This is used to identify which architecture of Windows to download. Default is 'x64'. |
|
| -WindowsArch | string | Windows Architecture | String value of 'x86', 'x64', or 'arm64'. This is used to identify which architecture of Windows to download. Default is 'x64'. |
|
||||||
| -WindowsLang | string | Windows Language | String value in language-region format (e.g., 'en-us'). This is used to identify which language of media to download. Default is 'en-us'. |
|
| -WindowsLang | string | Windows Language | String value in language-region format (e.g., 'en-us'). This is used to identify which language of media to download. Default is 'en-us'. |
|
||||||
| -WindowsRelease | int | Windows Release | Integer value of 10, 11, 2016, 2019, 2021, 2022, 2024, or 2025. This is used to identify which Windows client/LTSC/server release to use. Default is 11. |
|
| -WindowsRelease | int | Windows Release | Integer value of 10, 11, 2016, 2019, 2021, 2022, 2024, or 2025. This is used to identify which Windows client/LTSC/server release to use. Default is 11. |
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Follow the guide linked below to install Hyper-V on Windows client or Server
|
|||||||
|
|
||||||
## Install PowerShell 7
|
## Install PowerShell 7
|
||||||
|
|
||||||
PowerShell 7 is required as of releases 2507+ onward.
|
PowerShell 7.6+ is required as of releases 2507+ onward.
|
||||||
|
|
||||||
[Installing PowerShell on Windows - PowerShell \| Microsoft Learn
|
[Installing PowerShell on Windows - PowerShell \| Microsoft Learn
|
||||||
](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows)
|
](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows)
|
||||||
@@ -57,10 +57,10 @@ Replace `C:\FFUDevelopment` with the path you extracted the files to.
|
|||||||
|
|
||||||
## Running BuildFFUVM_UI.ps1
|
## Running BuildFFUVM_UI.ps1
|
||||||
|
|
||||||
Either run Terminal as Admin, making sure to select PowerShell, not Windows PowerShell, or PowerShell 7.5+ as Admin and run `C:\FFUDevelopment\BuildFFUVM_UI.ps1`
|
Either run Terminal as Admin, making sure to select PowerShell, not Windows PowerShell, or PowerShell 7.6+ as Admin and run `C:\FFUDevelopment\BuildFFUVM_UI.ps1`
|
||||||
|
|
||||||
If all went well, you should see the FFU Builder UI
|
If all went well, you should see the FFU Builder UI
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
{% include page_nav.html %}
|
{% include page_nav.html %}
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ After following this guide, you will have a USB drive with an FFU that contains
|
|||||||
|
|
||||||
## Video Walkthrough
|
## Video Walkthrough
|
||||||
|
|
||||||
|
{: .note-title}
|
||||||
|
|
||||||
|
> Note
|
||||||
|
>
|
||||||
|
> The below video was recorded prior to the Fluent UI refresh. Some things will look a bit different until a new quick start video is recorded.
|
||||||
|
|
||||||
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;">
|
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;">
|
||||||
<iframe
|
<iframe
|
||||||
src="https://www.youtube-nocookie.com/embed/kOIK5OmDugc"
|
src="https://www.youtube-nocookie.com/embed/kOIK5OmDugc"
|
||||||
@@ -45,7 +51,9 @@ Follow the [prerequisites](/FFU/prerequisites.html) documentation before getting
|
|||||||
|
|
||||||
Click the Hyper-V Settings tab
|
Click the Hyper-V Settings tab
|
||||||
|
|
||||||
You should be able to keep these settings at the defaults. For VM Switch Name and VM Host IP Address, you'll want to make sure the switch you created in the prerequisites section is listed. FFU Builder should automatically figure out the IP address of the swtich for you.
|
You should be able to keep these settings at the defaults. **Enable VM Networking (Experimental)** is off by default and should stay off unless you specifically want to test an internet-connected VM during provisioning.
|
||||||
|
|
||||||
|
If you turn on **Enable VM Networking (Experimental)**, make sure the switch you created in the prerequisites section is listed under **VM Switch Name**. If the build does not create a VM, this setting has no effect.
|
||||||
|
|
||||||
One setting you might need to set is the Logical Sector Size. 512 is the default and that's what most physical disks use today. However 4kn drives and UFS drives use 4k sector sizes, which would require you to select 4096. For now, select 512. If you have issues during deployment, there is error logging that will tell you if you need to set to 4096 on your device.
|
One setting you might need to set is the Logical Sector Size. 512 is the default and that's what most physical disks use today. However 4kn drives and UFS drives use 4k sector sizes, which would require you to select 4096. For now, select 512. If you have issues during deployment, there is error logging that will tell you if you need to set to 4096 on your device.
|
||||||
|
|
||||||
@@ -53,7 +61,7 @@ One setting you might need to set is the Logical Sector Size. 512 is the default
|
|||||||
|
|
||||||
Click the Windows Settings tab
|
Click the Windows Settings tab
|
||||||
|
|
||||||
If you keep ISO Path blank, FFU Builder will download the ESD file that the Windows Media Creation Tool uses. Most people should leave this blank since the Media Creation Tool media ESD file is now kept up to date as of Windows 11 25H2 (it's usually updated 2-3 days after patch Tuesday). This reduces the need to service the media and saves time and disk space.
|
Keep Download Windows ESD selected. FFU Builder will download the ESD file that the Windows Media Creation Tool uses. This is the recommended approach since the Media Creation Tool media ESD file is now kept up to date as of Windows 11 25H2 (it's usually updated 2-3 days after patch Tuesday). This reduces the need to service the media and saves time and disk space.
|
||||||
|
|
||||||
Change the Windows language to the one of your choosing.
|
Change the Windows language to the one of your choosing.
|
||||||
|
|
||||||
@@ -166,7 +174,7 @@ Check **Copy Drivers to USB drive** (even though we're doing a single model in t
|
|||||||
|
|
||||||
Your view should look like this:
|
Your view should look like this:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
At this point, you can either Save the Drivers.json file or click Download Selected. Clicking Save Drivers.json will save the Driver model information to the Drivers.json file which will be used at build time to download the drivers. Clicking Download Selected will download the drivers right now. This can be useful for testing without having to go through an entire build, or good for airgapped environments where you can download what you need from the internet on one network, and bring that over to the airgapped network.
|
At this point, you can either Save the Drivers.json file or click Download Selected. Clicking Save Drivers.json will save the Driver model information to the Drivers.json file which will be used at build time to download the drivers. Clicking Download Selected will download the drivers right now. This can be useful for testing without having to go through an entire build, or good for airgapped environments where you can download what you need from the internet on one network, and bring that over to the airgapped network.
|
||||||
|
|
||||||
@@ -184,18 +192,46 @@ Another safety measure is **Select Specific USB Drives**. When you check **Selec
|
|||||||
|
|
||||||
**Device Naming**
|
**Device Naming**
|
||||||
|
|
||||||
Device naming can be done from PE. The way this works is by leveraging an unattend.xml file to either take input from the user at imaging time or read a list of prefix values and append the serial number of the device. There are some major benefits to doing this:
|
Use the **Device Naming** expander on the Build page to decide whether `ComputerName` should be set during deployment. There are some major benefits to doing this:
|
||||||
|
|
||||||
1. Total deployment time is reduced if naming is set at FFU deployment time since there is no additional reboot done during OOBE.
|
1. Total deployment time is reduced if naming is set at FFU deployment time since there is no additional reboot done during OOBE.
|
||||||
2. Reduces the need for multiple provisioning packages or autopilot profiles. This means you can use a single PPKG or autopilot profile.
|
2. Reduces the need for multiple provisioning packages or autopilot profiles. This means you can use a single PPKG or autopilot profile.
|
||||||
|
|
||||||
**Prompt for Device Name**
|
**No Device Name**
|
||||||
|
|
||||||
If you want to be prompted for the device name, simply check **Copy Unattend.xml.** This tells the build script to copy the appropriate architecture unattend_arch.xml file from the `C:\FFUDevelopment\Unattend` folder to the `.\unattend` folder on the deploy partition of the USB drive.
|
This is the default option. The unattend file is still applied, but Windows generates a random computer name.
|
||||||
|
|
||||||
**Specifying Multiple Name Prefixes**
|
**Specify Device Name**
|
||||||
|
|
||||||
If you have multiple device name prefixes for different locations or device use cases, or even a single prefix, you can specify a prefixes.txt file in the `C:\FFUDevelopment\unattend` folder. If the prefixes.txt file is detected and a single prefix is listed, the device will just use that prefix and append the serial number of the device. If there are multiple prefixes listed in the prefixes.txt file, you will be prompted to select which prefix you want to name the device and the serial number will be appended to that prefix. If you want a dash in the name, include the dash in the prefix (e.g. if ABCD- is in the prefixes.txt file, the device name will be ABCD-SerialNumber).
|
Use this option when you want a static device name or a template such as `Comp-%serial%`.
|
||||||
|
|
||||||
|
- With **Copy Unattend.xml**, `%serial%` is resolved during deployment in PE.
|
||||||
|
- With **Inject Unattend.xml**, only static names are supported.
|
||||||
|
- **Copy Unattend.xml** and **Inject Unattend.xml** are mutually exclusive. Select only one.
|
||||||
|
|
||||||
|
**Specify a list of Prefixes**
|
||||||
|
|
||||||
|
This option writes `prefixes.txt` from the list in the UI. Enter one prefix per line or import an existing prefixes file. The source file can use any name because the UI tracks the prefixes path separately. If there is one prefix, deployment uses it automatically. If there are multiple prefixes, the technician is prompted to select one and the serial number is appended to that prefix.
|
||||||
|
|
||||||
|
{: .note-title}
|
||||||
|
|
||||||
|
> Note
|
||||||
|
>
|
||||||
|
> If the technician skips prefix selection when multiple prefixes are available, `ApplyFFU.ps1` leaves the existing unattend `ComputerName` value unchanged. With the current unattend samples set to `<ComputerName>*</ComputerName>`, Windows falls back to its default random computer-name behavior, typically resulting in a name such as `WIN-*`.
|
||||||
|
|
||||||
|
**Specify Serial to Device Name Mapping**
|
||||||
|
|
||||||
|
This option writes `SerialComputerNames.csv` from the CSV content in the UI. Use `SerialNumber,ComputerName` as the header row, then add one row per device. During deployment, `ApplyFFU.ps1` compares the current BIOS serial number to the CSV and applies the matching computer name.
|
||||||
|
|
||||||
|
- This option requires **Copy Unattend.xml**.
|
||||||
|
- **Inject Unattend.xml** is not supported with this option.
|
||||||
|
- If no matching serial number is found during deployment, `ApplyFFU.ps1` falls back to a random `FFU-*` computer name.
|
||||||
|
|
||||||
|
{: .note-title}
|
||||||
|
|
||||||
|
> Note
|
||||||
|
>
|
||||||
|
> If `prefixes.txt` and `SerialComputerNames.csv` are both present on the same deployment media, `ApplyFFU.ps1` checks `prefixes.txt` first. FFU Builder stages only the naming file for the selected device-naming mode.
|
||||||
|
|
||||||
{: .warning-title}
|
{: .warning-title}
|
||||||
|
|
||||||
@@ -203,13 +239,23 @@ If you have multiple device name prefixes for different locations or device use
|
|||||||
>
|
>
|
||||||
> If using a provisioning package or autopilot json file, DO NOT specify a name in either of these. They will overwrite the name you have specified in the unattend.xml.
|
> If using a provisioning package or autopilot json file, DO NOT specify a name in either of these. They will overwrite the name you have specified in the unattend.xml.
|
||||||
|
|
||||||
|
For the purposes of this quickstart, we'll use **Prompt for Device Name**
|
||||||
|
|
||||||
**Post Build Cleanup**
|
**Post Build Cleanup**
|
||||||
|
|
||||||
Leave the Post Build Cleanup section at the defaults
|
Leave the Post Build Cleanup section at the defaults
|
||||||
|
|
||||||
Your Build tab should look something like this:
|
Your Build tab should look something like this:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Click **Build FFU**
|
Click **Build FFU**
|
||||||
|
|
||||||
@@ -217,8 +263,6 @@ Depending on your internet speed, speed of your build machine, etc. this will ta
|
|||||||
|
|
||||||
## Monitor
|
## Monitor
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
The monitor tab parses the `C:\FFUDevelopment\FFUDevelopment.log` file. If you'd like to use CMTrace or another tool to monitor the log, feel free. The monitor tab has some similar functionality to CMTrace. If you click off the last line of the log in the monitor tab it will stay on that line, allowing you to read what you have selected instead of the log autoscrolling. You can also copy one or multiple lines by selecting a line and shift+clicking the last line you want to select and hitting ctrl+c to copy the lines.
|
The monitor tab parses the `C:\FFUDevelopment\FFUDevelopment.log` file. If you'd like to use CMTrace or another tool to monitor the log, feel free. The monitor tab has some similar functionality to CMTrace. If you click off the last line of the log in the monitor tab it will stay on that line, allowing you to read what you have selected instead of the log autoscrolling. You can also copy one or multiple lines by selecting a line and shift+clicking the last line you want to select and hitting ctrl+c to copy the lines.
|
||||||
|
|
||||||
Now sit back, relax, and watch FFU Builder do its magic. You should see a VM pop up after everything is downloaded and Windows has been installed to the VHDX file (it may not if Hyper-V manager wasn't previously open). If you don't see a VM pop up, you can open up Hyper-V manager and look for a VM with a name that starts with **_FFU-.**
|
Now sit back, relax, and watch FFU Builder do its magic. You should see a VM pop up after everything is downloaded and Windows has been installed to the VHDX file (it may not if Hyper-V manager wasn't previously open). If you don't see a VM pop up, you can open up Hyper-V manager and look for a VM with a name that starts with **_FFU-.**
|
||||||
@@ -270,18 +314,22 @@ And the Unattend folder should have an unattend.xml file with the following cont
|
|||||||
<settings pass="specialize">
|
<settings pass="specialize">
|
||||||
<!--<ComputerName> must be in the first Component Element "Microsoft-Windows-Shell-Setup" . Do not change the order or remove it -->
|
<!--<ComputerName> must be in the first Component Element "Microsoft-Windows-Shell-Setup" . Do not change the order or remove it -->
|
||||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
<ComputerName>MyComputer</ComputerName>
|
<ComputerName>*</ComputerName>
|
||||||
</component>
|
</component>
|
||||||
<!--Place addtional Components Elements and settings below here. -->
|
<!--Place addtional Components Elements and settings below here. -->
|
||||||
</settings>
|
</settings>
|
||||||
</unattend>
|
</unattend>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Keep `*` if you want Windows to generate a random device name by default.
|
||||||
|
|
||||||
|
If you want the technician to be prompted for the device name during deployment, select **Prompt for Device Name** in the Build tab and enable **Copy Unattend.xml**. FFU Builder will rewrite only the staged deployment copy of `Unattend.xml` for that workflow.
|
||||||
|
|
||||||
Now you're ready to deploy the FFU to your device.
|
Now you're ready to deploy the FFU to your device.
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
Deployment should be fairly straight forward: boot off the USB device, get prompted for a device name, and the deployment of the FFU and drivers should happen automatically.
|
Deployment should be fairly straight forward: boot off the USB device and the deployment of the FFU and drivers should happen automatically. If you selected **Prompt for Device Name** or another supported device naming option, that naming step will happen during deployment.
|
||||||
|
|
||||||
If you have any questions or run into any issues, [open a discussion in the Github repo](https://github.com/rbalsleyMSFT/FFU/discussions).
|
If you have any questions or run into any issues, [open a discussion in the Github repo](https://github.com/rbalsleyMSFT/FFU/discussions).
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ has_toc: false
|
|||||||
---
|
---
|
||||||
# UI Overview
|
# UI Overview
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
The user interface has 9 distinct tabs for easy navigation.
|
The user interface has 9 distinct pages for easy navigation.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ parent: UI Overview
|
|||||||
---
|
---
|
||||||
# Updates
|
# Updates
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Update Latest Cumulative Update
|
## Update Latest Cumulative Update
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,21 @@ parent: UI Overview
|
|||||||
---
|
---
|
||||||
# Windows Settings
|
# Windows Settings
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Windows ISO Path
|
## Windows Media Source
|
||||||
|
|
||||||
Path to Windows 10/11 ISO file. If left blank, FFU Builder will download the latest version of Windows 10 or 11 from the Media Creation Tool.
|
### Download Windows ESD
|
||||||
|
|
||||||
|
Download Windows ESD will download the ESD file provided from the Windows Media Creation Tool
|
||||||
|
|
||||||
|
### Provide Windows ISO
|
||||||
|
|
||||||
|
You can provide your own Windows ISO (Client or Server). Good for scenarios where you want to deploy Enterprise or Education SKUs and have a product key you want to use (MAK or KMS).
|
||||||
|
|
||||||
|
#### Windows ISO Path
|
||||||
|
|
||||||
|
Path to Windows ISO file.
|
||||||
|
|
||||||
{: .tip-title}
|
{: .tip-title}
|
||||||
|
|
||||||
@@ -21,11 +31,11 @@ Path to Windows 10/11 ISO file. If left blank, FFU Builder will download the lat
|
|||||||
>
|
>
|
||||||
> Should I provide my own ISO, or let FFU Builder download the media
|
> Should I provide my own ISO, or let FFU Builder download the media
|
||||||
>
|
>
|
||||||
> It's recommended to use the latest updated ISO from Visual Studio Downloads, or the Media Creation tool. See the Media Type section below for a better understanding of business and consumer media and how it impacts Subscription Activation.
|
> It's recommended to use the latest ESD in most scenarios. It gets updated a couple of days after Patch Tuesday (so roughly the 2nd Thursday or Friday). See the Media Type section below for a better understanding of business and consumer media and how it impacts Subscription Activation.
|
||||||
|
|
||||||
## Windows Release
|
## Windows Release
|
||||||
|
|
||||||
Integer value of 10 or 11. This is used to identify which release of Windows to download. Default is 11.
|
Can be 10, 11, different Server values, or LTSB/LTSC.
|
||||||
|
|
||||||
## Windows Version
|
## Windows Version
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ grand_parent: UI Overview
|
|||||||
---
|
---
|
||||||
# Install Winget Applications
|
# Install Winget Applications
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Check Winget Status
|
## Check Winget Status
|
||||||
|
|
||||||
@@ -18,19 +18,19 @@ Installing Winget applications requires that both the winget CLI and Microsoft.W
|
|||||||
|
|
||||||
Click **Check Winget Status** to validate the versions of both the CLI and PowerShell module. If older than the minimum required version, will be updated to the latest version.
|
Click **Check Winget Status** to validate the versions of both the CLI and PowerShell module. If older than the minimum required version, will be updated to the latest version.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
After validating Winget status, you'll be able to search winget for applications. The larger the result set, the longer it will take for the list view to be populated. For example, if searching for **win**, the UI might appear to hang while it searches for apps with a name or id of **win** due to 669 results being returned and processed. Instead, if you search for **windows app**, 13 results are returned within a few seconds.
|
After validating Winget status, you'll be able to search winget for applications. The larger the result set, the longer it will take for the list view to be populated. For example, if searching for **win**, the UI might appear to hang while it searches for apps with a name or id of **win** due to 669 results being returned and processed. Instead, if you search for **windows app**, 13 results are returned within a few seconds.
|
||||||
|
|
||||||
The UI allows for multi-selection of applications
|
The UI allows for multi-selection of applications
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
You can also change the architecture, add additional exit codes, or ignore exit codes completely.
|
You can also change the architecture, add additional exit codes, or ignore exit codes completely.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
FFU Builder supports x86, x64, arm64, and x86/x64 (both) for applications in the winget source repository. For apps in the msstore source repository, the architecture cannot be changed. In most cases, x64 will be what you want, however in some cases the combo of x86 and x64 will be necessary. This might be due to runtimes (.NET, Visual C++) where an application is expecting both x86 and x64 runtimes.
|
FFU Builder supports x86, x64, arm64, and x86/x64 (both) for applications in the winget source repository. For apps in the msstore source repository, the architecture cannot be changed. In most cases, x64 will be what you want, however in some cases the combo of x86 and x64 will be necessary. This might be due to runtimes (.NET, Visual C++) where an application is expecting both x86 and x64 runtimes.
|
||||||
|
|
||||||
|
|||||||