From 15a5b16b39887b71ae545c638d57183c97bdf629 Mon Sep 17 00:00:00 2001
From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com>
Date: Thu, 18 Sep 2025 18:17:58 -0700
Subject: [PATCH] Adds UI/CLI to copy additional FFUs to USB build
- Enables selecting multiple existing FFU images to include on the deployment USB for easier distribution and testing.
- Adds a UI option with selectable, sortable list from the capture folder, refresh support, and persisted selections.
- Validates that selections exist when the option is enabled to prevent empty runs.
- Supports unattended/CLI flows by prompting early or accepting a preselected list for USB creation; deduplicates and logs chosen files.
- Always includes the just-built (or latest available) FFU as a base.
- Improves no-FFU handling and streamlines multi-FFU selection workflow.
---
FFUDevelopment/BuildFFUVM.ps1 | 164 +++++++++++++-----
FFUDevelopment/BuildFFUVM_UI.ps1 | 9 +
FFUDevelopment/BuildFFUVM_UI.xaml | 23 +++
.../FFUUI.Core/FFUUI.Core.Config.psm1 | 56 +++++-
.../FFUUI.Core/FFUUI.Core.Handlers.psm1 | 44 +++++
.../FFUUI.Core/FFUUI.Core.Initialize.psm1 | 55 +++++-
FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1 | 60 +++++++
7 files changed, 367 insertions(+), 44 deletions(-)
diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1
index 9eb547f..21c94a0 100644
--- a/FFUDevelopment/BuildFFUVM.ps1
+++ b/FFUDevelopment/BuildFFUVM.ps1
@@ -383,6 +383,8 @@ param(
[bool]$CopyPEDrivers,
[bool]$UseDriversAsPEDrivers,
[bool]$RemoveFFU,
+ [bool]$CopyAdditionalFFUFiles,
+ [string[]]$AdditionalFFUFiles,
[bool]$UpdateLatestCU,
[bool]$UpdatePreviewCU,
[bool]$UpdateLatestMicrocode,
@@ -3462,7 +3464,8 @@ Function Get-USBDrive {
}
Function New-DeploymentUSB {
param(
- [switch]$CopyFFU
+ [switch]$CopyFFU,
+ [string[]]$FFUFilesToCopy
)
WriteLog "CopyFFU is set to $CopyFFU"
$BuildUSBPath = $PSScriptRoot
@@ -3472,46 +3475,58 @@ Function New-DeploymentUSB {
# 1. Get FFU File(s) - This happens once before parallel processing
if ($CopyFFU.IsPresent) {
- $FFUFiles = Get-ChildItem -Path "$BuildUSBPath\FFU" -Filter "*.ffu"
- $FFUCount = $FFUFiles.count
-
- if ($FFUCount -eq 1) {
- $SelectedFFUFile = $FFUFiles.FullName
- WriteLog "One FFU file found, will use: $SelectedFFUFile"
- }
- elseif ($FFUCount -gt 1) {
- WriteLog "Found $FFUCount FFU files"
- if ($VerbosePreference -ne 'Continue') {
- Write-Host "Found $FFUCount FFU files"
+ if ($null -ne $FFUFilesToCopy -and $FFUFilesToCopy.Count -gt 0) {
+ $SelectedFFUFile = $FFUFilesToCopy
+ WriteLog "Using preselected FFU file list. Count: $($FFUFilesToCopy.Count)"
+ WriteLog "FFU files to copy:"
+ foreach ($f in $FFUFilesToCopy) {
+ WriteLog ("- {0}" -f (Split-Path $f -Leaf))
}
- $output = @()
- for ($i = 0; $i -lt $FFUCount; $i++) {
- $output += [PSCustomObject]@{
- 'FFU Number' = $i + 1
- 'FFU Name' = $FFUFiles[$i].Name
- 'Last Modified' = $FFUFiles[$i].LastWriteTime
- }
- }
- $output | Format-Table -AutoSize | Out-String | Write-Host
-
- do {
- $inputChoice = Read-Host "Enter the number for the FFU to copy, or 'A' for all"
- if ($inputChoice -eq 'A') {
- $SelectedFFUFile = $FFUFiles.FullName
- WriteLog 'Will copy all FFU Files'
- }
- elseif ($inputChoice -match '^\d+$' -and [int]$inputChoice -ge 1 -and [int]$inputChoice -le $FFUCount) {
- $SelectedFFUFile = $FFUFiles[[int]$inputChoice - 1].FullName
- WriteLog "$SelectedFFUFile was selected"
- }
- else {
- Write-Host "Invalid selection. Please try again."
- }
- } while ($null -eq $SelectedFFUFile)
}
else {
- Write-Error "No FFU files found in $BuildUSBPath\FFU. Cannot copy FFU to USB drive."
- Return
+ $FFUFiles = Get-ChildItem -Path "$BuildUSBPath\FFU" -Filter "*.ffu"
+ $FFUCount = $FFUFiles.count
+
+ switch ($FFUCount) {
+ 0 {
+ Write-Error "No FFU files found in $BuildUSBPath\FFU. Cannot copy FFU to USB drive."
+ return
+ }
+ 1 {
+ $SelectedFFUFile = $FFUFiles.FullName
+ WriteLog "One FFU file found, will use: $SelectedFFUFile"
+ }
+ default {
+ WriteLog "Found $FFUCount FFU files"
+ if ($VerbosePreference -ne 'Continue') {
+ Write-Host "Found $FFUCount FFU files"
+ }
+ $output = @()
+ for ($i = 0; $i -lt $FFUCount; $i++) {
+ $output += [PSCustomObject]@{
+ 'FFU Number' = $i + 1
+ 'FFU Name' = $FFUFiles[$i].Name
+ 'Last Modified' = $FFUFiles[$i].LastWriteTime
+ }
+ }
+ $output | Format-Table -AutoSize | Out-String | Write-Host
+
+ do {
+ $inputChoice = Read-Host "Enter the number for the FFU to copy, or 'A' for all"
+ if ($inputChoice -eq 'A') {
+ $SelectedFFUFile = $FFUFiles.FullName
+ WriteLog 'Will copy all FFU Files'
+ }
+ elseif ($inputChoice -match '^\d+$' -and [int]$inputChoice -ge 1 -and [int]$inputChoice -le $FFUCount) {
+ $SelectedFFUFile = $FFUFiles[[int]$inputChoice - 1].FullName
+ WriteLog "$SelectedFFUFile was selected"
+ }
+ else {
+ Write-Host "Invalid selection. Please try again."
+ }
+ } while ($null -eq $SelectedFFUFile)
+ }
+ }
}
}
@@ -4817,6 +4832,49 @@ If (Test-Path -Path "$FFUDevelopmentPath\dirty.txt") {
WriteLog 'Creating dirty.txt file'
New-Item -Path .\ -Name "dirty.txt" -ItemType "file" | Out-Null
+# Early CLI prompt for additional FFUs (only if enabled and not provided)
+if ($BuildUSBDrive -and $CopyAdditionalFFUFiles -and ((-not $AdditionalFFUFiles) -or ($AdditionalFFUFiles.Count -eq 0))) {
+ try {
+ $ffuFolder = Join-Path $FFUDevelopmentPath 'FFU'
+ if (Test-Path -Path $ffuFolder) {
+ $cand = Get-ChildItem -Path $ffuFolder -Filter '*.ffu' -File | Sort-Object LastWriteTime -Descending
+ if ($cand.Count -gt 0) {
+ Write-Host ""
+ Write-Host "Additional FFU files available in $($ffuFolder):"
+ $i = 1
+ foreach ($c in $cand) {
+ Write-Host ("{0,3}. {1} [{2}]" -f $i, $c.Name, $c.LastWriteTime)
+ $i++
+ }
+ Write-Host ""
+ $resp = Read-Host "Select additional FFUs to copy (e.g. 1,3,5) or 'A' for all, or press Enter to skip"
+ if ($resp -match '^[Aa]$') {
+ $AdditionalFFUFiles = @($cand.FullName)
+ }
+ elseif ($resp -match '^\s*\d+(\s*,\s*\d+)*\s*$') {
+ $indices = $resp.Split(',') | ForEach-Object { [int]($_.Trim()) }
+ $sel = @()
+ foreach ($idx in $indices) {
+ if ($idx -ge 1 -and $idx -le $cand.Count) {
+ $sel += $cand[$idx - 1].FullName
+ }
+ }
+ $AdditionalFFUFiles = @($sel | Select-Object -Unique)
+ }
+ else {
+ # Skip if blank or invalid
+ if (-not [string]::IsNullOrWhiteSpace($resp)) {
+ WriteLog "Invalid additional FFU selection input. Skipping."
+ }
+ }
+ }
+ }
+ }
+ catch {
+ WriteLog "Early additional FFU selection prompt failed: $($_.Exception.Message)"
+ }
+}
+
#Get drivers first since user could be prompted for additional info
Set-Progress -Percentage 3 -Message "Processing drivers..."
if ($driversJsonPath -and (Test-Path $driversJsonPath) -and ($InstallDrivers -or $CopyDrivers)) {
@@ -6084,7 +6142,35 @@ If ($BuildUSBDrive) {
Set-Progress -Percentage 95 -Message "Building USB drive..."
try {
If (Test-Path -Path $DeployISO) {
- New-DeploymentUSB -CopyFFU
+ $ffuFilesToCopy = @()
+
+ # Always include the FFU that was just built (fallback to most recent .ffu in capture folder)
+ $currentFFU = $null
+ if ($null -ne $FFUFile -and -not [string]::IsNullOrWhiteSpace($FFUFile) -and (Test-Path -LiteralPath $FFUFile)) {
+ $currentFFU = $FFUFile
+ }
+ else {
+ try {
+ $ffuDir = if (-not [string]::IsNullOrWhiteSpace($FFUCaptureLocation)) { $FFUCaptureLocation } else { Join-Path $FFUDevelopmentPath 'FFU' }
+ if (Test-Path -LiteralPath $ffuDir) {
+ $latest = Get-ChildItem -Path $ffuDir -Filter '*.ffu' -File | Sort-Object LastWriteTime -Descending | Select-Object -First 1
+ if ($null -ne $latest) { $currentFFU = $latest.FullName }
+ }
+ }
+ catch {
+ WriteLog "Failed to resolve latest FFU file to copy: $($_.Exception.Message)"
+ }
+ }
+ if ($null -ne $currentFFU) {
+ $ffuFilesToCopy += $currentFFU
+ }
+
+ if ($CopyAdditionalFFUFiles -and ($null -ne $AdditionalFFUFiles) -and ($AdditionalFFUFiles.Count -gt 0)) {
+ $ffuFilesToCopy += $AdditionalFFUFiles
+ }
+
+ $ffuFilesToCopy = $ffuFilesToCopy | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
+ New-DeploymentUSB -CopyFFU -FFUFilesToCopy $ffuFilesToCopy
}
else {
WriteLog "$BuildUSBDrive set to true, however unable to find $DeployISO. USB drive not built."
diff --git a/FFUDevelopment/BuildFFUVM_UI.ps1 b/FFUDevelopment/BuildFFUVM_UI.ps1
index 70bee77..182884e 100644
--- a/FFUDevelopment/BuildFFUVM_UI.ps1
+++ b/FFUDevelopment/BuildFFUVM_UI.ps1
@@ -400,6 +400,15 @@ $script:uiState.Controls.btnRun.Add_Click({
# Gather config on the UI thread before starting the job
$config = Get-UIConfig -State $script:uiState
+
+ # Validate Additional FFU selection if enabled
+ if ($config.BuildUSBDrive -and $config.CopyAdditionalFFUFiles -and (($null -eq $config.AdditionalFFUFiles) -or ($config.AdditionalFFUFiles.Count -eq 0))) {
+ [System.Windows.MessageBox]::Show("Please select at least one additional FFU file to copy, or uncheck 'Copy Additional FFU Files'.", "Selection Required", "OK", "Warning") | Out-Null
+ $btnRun.IsEnabled = $true
+ $script:uiState.Controls.txtStatus.Text = "Build canceled: Additional FFU selection required."
+ return
+ }
+
$configFilePath = Join-Path $config.FFUDevelopmentPath "\config\FFUConfig.json"
$config | ConvertTo-Json -Depth 10 | Set-Content -Path $configFilePath -Encoding UTF8
$script:uiState.Data.lastConfigFilePath = $configFilePath
diff --git a/FFUDevelopment/BuildFFUVM_UI.xaml b/FFUDevelopment/BuildFFUVM_UI.xaml
index 36c70d6..cf20f8e 100644
--- a/FFUDevelopment/BuildFFUVM_UI.xaml
+++ b/FFUDevelopment/BuildFFUVM_UI.xaml
@@ -756,6 +756,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1
index 9d7ba3d..43a65f6 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1
@@ -37,6 +37,7 @@ function Get-UIConfig {
UseDriversAsPEDrivers = $State.Controls.chkUseDriversAsPEDrivers.IsChecked
CopyPPKG = $State.Controls.chkCopyPPKG.IsChecked
CopyUnattend = $State.Controls.chkCopyUnattend.IsChecked
+ CopyAdditionalFFUFiles = $State.Controls.chkCopyAdditionalFFUFiles.IsChecked
CreateCaptureMedia = $State.Controls.chkCreateCaptureMedia.IsChecked
CreateDeploymentMedia = $State.Controls.chkCreateDeploymentMedia.IsChecked
InjectUnattend = $State.Controls.chkInjectUnattend.IsChecked
@@ -115,6 +116,16 @@ function Get-UIConfig {
$State.Controls.lstUSBDrives.Items | Where-Object { $_.IsSelected } | ForEach-Object {
$config.USBDriveList[$_.Model] = $_.SerialNumber
}
+
+ # Additional FFU file selections
+ $config.AdditionalFFUFiles = @()
+ if ($State.Controls.chkCopyAdditionalFFUFiles.IsChecked) {
+ $config.AdditionalFFUFiles = @(
+ $State.Controls.lstAdditionalFFUs.Items |
+ Where-Object { $_.IsSelected } |
+ ForEach-Object { $_.FullName }
+ )
+ }
return $config
}
@@ -360,6 +371,7 @@ function Update-UIFromConfig {
Set-UIValue -ControlName 'chkAllowVHDXCaching' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'AllowVHDXCaching' -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 '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 'chkInjectUnattend' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'InjectUnattend' -State $State
@@ -369,7 +381,7 @@ function Update-UIFromConfig {
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 'chkCopyPPKG' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyPPKG' -State $State
-
+
# Post Build Cleanup group (Build Tab)
Set-UIValue -ControlName 'chkCleanupAppsISO' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CleanupAppsISO' -State $State
Set-UIValue -ControlName 'chkCleanupCaptureISO' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CleanupCaptureISO' -State $State
@@ -654,8 +666,46 @@ function Update-UIFromConfig {
else {
WriteLog "LoadConfig: Condition to auto-check 'Select Specific USB Drives' was NOT met."
}
- WriteLog "LoadConfig: Configuration loading process finished."
-}
+ # Populate additional FFU list and apply selections
+ try {
+ if ($State.Controls.chkCopyAdditionalFFUFiles.IsChecked) {
+ $State.Controls.additionalFFUPanel.Visibility = 'Visible'
+ if ($State.Controls.btnRefreshAdditionalFFUs) {
+ $State.Controls.btnRefreshAdditionalFFUs.RaiseEvent([System.Windows.RoutedEventArgs]::new([System.Windows.Controls.Button]::ClickEvent))
+ }
+ $selectedFiles = @()
+ $addFFUKeyExists = $false
+ if ($ConfigContent -is [System.Management.Automation.PSCustomObject] -and $null -ne $ConfigContent.PSObject.Properties) {
+ if (($ConfigContent.PSObject.Properties.Match('AdditionalFFUFiles')).Count -gt 0) {
+ $addFFUKeyExists = $true
+ }
+ }
+ if ($addFFUKeyExists -and $null -ne $ConfigContent.AdditionalFFUFiles) {
+ $selectedFiles = @($ConfigContent.AdditionalFFUFiles)
+ }
+ if ($selectedFiles.Count -gt 0) {
+ foreach ($item in $State.Controls.lstAdditionalFFUs.Items) {
+ if ($selectedFiles -contains $item.FullName) {
+ $item.IsSelected = $true
+ }
+ }
+ $State.Controls.lstAdditionalFFUs.Items.Refresh()
+ $headerChk = $State.Controls.chkSelectAllAdditionalFFUs
+ if ($null -ne $headerChk) {
+ Update-SelectAllHeaderCheckBoxState -ListView $State.Controls.lstAdditionalFFUs -HeaderCheckBox $headerChk
+ }
+ }
+ }
+ else {
+ $State.Controls.additionalFFUPanel.Visibility = 'Collapsed'
+ }
+ }
+ catch {
+ WriteLog "LoadConfig: Error applying Additional FFU selections: $($_.Exception.Message)"
+ }
+
+ WriteLog "LoadConfig: Configuration loading process finished."
+ }
function Invoke-SaveConfiguration {
param(
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1
index e9ed276..5043d43 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1
@@ -152,6 +152,50 @@ function Register-EventHandlers {
$localState.Controls.chkPromptExternalHardDiskMedia.IsChecked = $false
})
+ # Additional FFU Files events
+ $State.Controls.chkCopyAdditionalFFUFiles.Add_Checked({
+ param($eventSource, $routedEventArgs)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ $localState = $window.Tag
+ $localState.Controls.additionalFFUPanel.Visibility = 'Visible'
+ Update-AdditionalFFUList -State $localState
+ })
+ $State.Controls.chkCopyAdditionalFFUFiles.Add_Unchecked({
+ param($eventSource, $routedEventArgs)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ $localState = $window.Tag
+ $localState.Controls.additionalFFUPanel.Visibility = 'Collapsed'
+ $localState.Controls.lstAdditionalFFUs.Items.Clear()
+ $headerChk = $localState.Controls.chkSelectAllAdditionalFFUs
+ if ($null -ne $headerChk) {
+ Update-SelectAllHeaderCheckBoxState -ListView $localState.Controls.lstAdditionalFFUs -HeaderCheckBox $headerChk
+ }
+ })
+ $State.Controls.btnRefreshAdditionalFFUs.Add_Click({
+ param($eventSource, $routedEventArgs)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ $localState = $window.Tag
+ Update-AdditionalFFUList -State $localState
+ })
+ $State.Controls.lstAdditionalFFUs.Add_PreviewKeyDown({
+ param($eventSource, $keyEvent)
+ if ($keyEvent.Key -eq 'Space') {
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ $localState = $window.Tag
+ Invoke-ListViewItemToggle -ListView $eventSource -State $localState -HeaderCheckBoxKeyName 'chkSelectAllAdditionalFFUs'
+ $keyEvent.Handled = $true
+ }
+ })
+ $State.Controls.lstAdditionalFFUs.Add_SelectionChanged({
+ param($eventSource, $selChangeEvent)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ $localState = $window.Tag
+ $headerChk = $localState.Controls.chkSelectAllAdditionalFFUs
+ if ($null -ne $headerChk) {
+ Update-SelectAllHeaderCheckBoxState -ListView $localState.Controls.lstAdditionalFFUs -HeaderCheckBox $headerChk
+ }
+ })
+
$State.Controls.btnCheckUSBDrives.Add_Click({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
index d870a96..376700e 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
@@ -59,6 +59,10 @@ function Initialize-UIControls {
$State.Controls.usbSelectionPanel = $window.FindName('usbDriveSelectionPanel')
$State.Controls.chkAllowExternalHardDiskMedia = $window.FindName('chkAllowExternalHardDiskMedia')
$State.Controls.chkPromptExternalHardDiskMedia = $window.FindName('chkPromptExternalHardDiskMedia')
+ $State.Controls.chkCopyAdditionalFFUFiles = $window.FindName('chkCopyAdditionalFFUFiles')
+ $State.Controls.additionalFFUPanel = $window.FindName('additionalFFUPanel')
+ $State.Controls.lstAdditionalFFUs = $window.FindName('lstAdditionalFFUs')
+ $State.Controls.btnRefreshAdditionalFFUs = $window.FindName('btnRefreshAdditionalFFUs')
$State.Controls.chkInstallWingetApps = $window.FindName('chkInstallWingetApps')
$State.Controls.wingetPanel = $window.FindName('wingetPanel')
$State.Controls.btnCheckWingetModule = $window.FindName('btnCheckWingetModule')
@@ -257,6 +261,8 @@ function Initialize-UIDefaults {
$State.Controls.usbSelectionPanel.Visibility = if ($State.Controls.chkSelectSpecificUSBDrives.IsChecked) { 'Visible' } else { 'Collapsed' }
$State.Controls.chkSelectSpecificUSBDrives.IsEnabled = $State.Controls.chkBuildUSBDriveEnable.IsChecked
$State.Controls.chkPromptExternalHardDiskMedia.IsEnabled = $State.Controls.chkAllowExternalHardDiskMedia.IsChecked
+ $State.Controls.chkCopyAdditionalFFUFiles.IsChecked = $State.Defaults.generalDefaults.CopyAdditionalFFUFiles
+ $State.Controls.additionalFFUPanel.Visibility = if ($State.Controls.chkCopyAdditionalFFUFiles.IsChecked) { 'Visible' } else { 'Collapsed' }
# Hyper-V Settings defaults from General Defaults
Initialize-VMSwitchData -State $State
@@ -454,7 +460,7 @@ function Initialize-DynamicUIElements {
$exitHeaderTextFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBlock])
$exitHeaderTextFactory.SetValue([System.Windows.Controls.TextBlock]::TextProperty, "Additional Exit Codes")
- $exitHeaderTextFactory.SetValue([System.Windows.Controls.TextBlock]::PaddingProperty, (New-Object System.Windows.Thickness(5,2,5,2)))
+ $exitHeaderTextFactory.SetValue([System.Windows.Controls.TextBlock]::PaddingProperty, (New-Object System.Windows.Thickness(5, 2, 5, 2)))
$exitHeaderTextFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
$exitHeaderTemplate = New-Object System.Windows.DataTemplate
@@ -482,7 +488,7 @@ function Initialize-DynamicUIElements {
$ignoreHeaderTextFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBlock])
$ignoreHeaderTextFactory.SetValue([System.Windows.Controls.TextBlock]::TextProperty, "Ignore Exit Codes")
- $ignoreHeaderTextFactory.SetValue([System.Windows.Controls.TextBlock]::PaddingProperty, (New-Object System.Windows.Thickness(5,2,5,2)))
+ $ignoreHeaderTextFactory.SetValue([System.Windows.Controls.TextBlock]::PaddingProperty, (New-Object System.Windows.Thickness(5, 2, 5, 2)))
$ignoreHeaderTextFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
$ignoreHeaderTemplate = New-Object System.Windows.DataTemplate
@@ -682,6 +688,51 @@ function Initialize-DynamicUIElements {
else {
WriteLog "Warning: lstUSBDrives.View is not a GridView. Selectable column not added, and sorting cannot be enabled."
}
+
+ # Additional FFUs ListView setup
+ $itemStyleAdditionalFFUs = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem])
+ $itemStyleAdditionalFFUs.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)))
+ $State.Controls.lstAdditionalFFUs.ItemContainerStyle = $itemStyleAdditionalFFUs
+
+ if ($State.Controls.lstAdditionalFFUs.View -is [System.Windows.Controls.GridView]) {
+ Add-SelectableGridViewColumn -ListView $State.Controls.lstAdditionalFFUs -State $State -HeaderCheckBoxKeyName "chkSelectAllAdditionalFFUs" -ColumnWidth 70
+
+ $additionalFFUsGridView = $State.Controls.lstAdditionalFFUs.View
+
+ if ($additionalFFUsGridView.Columns.Count -gt 1) {
+ $nameColumn = $additionalFFUsGridView.Columns[1]
+ $nameHeader = New-Object System.Windows.Controls.GridViewColumnHeader
+ $nameHeader.Content = "FFU Name"
+ $nameHeader.Tag = "Name"
+ $nameHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left
+ $nameColumn.Header = $nameHeader
+ }
+ if ($additionalFFUsGridView.Columns.Count -gt 2) {
+ $lastModColumn = $additionalFFUsGridView.Columns[2]
+ $lastModHeader = New-Object System.Windows.Controls.GridViewColumnHeader
+ $lastModHeader.Content = "Last Modified"
+ $lastModHeader.Tag = "LastModified"
+ $lastModHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left
+ $lastModColumn.Header = $lastModHeader
+ }
+
+ $State.Controls.lstAdditionalFFUs.AddHandler(
+ [System.Windows.Controls.GridViewColumnHeader]::ClickEvent,
+ [System.Windows.RoutedEventHandler] {
+ param($eventSource, $e)
+ $header = $e.OriginalSource
+ if ($header -is [System.Windows.Controls.GridViewColumnHeader] -and $header.Tag) {
+ $listViewControl = $eventSource
+ $window = [System.Windows.Window]::GetWindow($listViewControl)
+ $uiStateFromWindowTag = $window.Tag
+ Invoke-ListViewSort -listView $eventSource -property $header.Tag -State $uiStateFromWindowTag
+ }
+ }
+ )
+ }
+ else {
+ WriteLog "Warning: lstAdditionalFFUs.View is not a GridView. Selectable column not added, and sorting cannot be enabled."
+ }
}
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1
index d825e3a..b4bc6d3 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1
@@ -128,6 +128,7 @@ function Get-GeneralDefaults {
AllowExternalHardDiskMedia = $false
PromptExternalHardDiskMedia = $true
SelectSpecificUSBDrives = $false
+ CopyAdditionalFFUFiles = $false
CopyAutopilot = $false
CopyUnattend = $false
CopyPPKG = $false
@@ -198,6 +199,65 @@ function Get-USBDrives {
}
}
+# Returns a list of FFU files from the provided folder with selection metadata
+function Get-FFUFiles {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$Path
+ )
+ if (-not (Test-Path -Path $Path)) {
+ return @()
+ }
+ Get-ChildItem -Path $Path -Filter '*.ffu' -File -ErrorAction SilentlyContinue | ForEach-Object {
+ [PSCustomObject]@{
+ IsSelected = $false
+ Name = $_.Name
+ LastModified = $_.LastWriteTime
+ FullName = $_.FullName
+ }
+ }
+}
+
+# Helper: Populate Additional FFU List from the capture folder
+function Update-AdditionalFFUList {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true)]
+ [PSCustomObject]$State
+ )
+ try {
+ $ffuFolder = $State.Controls.txtFFUCaptureLocation.Text
+ $listView = $State.Controls.lstAdditionalFFUs
+ if ($null -eq $listView) { return }
+ $listView.Items.Clear()
+ if ([string]::IsNullOrWhiteSpace($ffuFolder) -or -not (Test-Path -Path $ffuFolder)) {
+ WriteLog "Additional FFUs: Capture folder not set or not found: $ffuFolder"
+ }
+ else {
+ $items = Get-ChildItem -Path $ffuFolder -Filter '*.ffu' -File -ErrorAction SilentlyContinue |
+ Sort-Object LastWriteTime -Descending |
+ ForEach-Object {
+ [PSCustomObject]@{
+ IsSelected = $false
+ Name = $_.Name
+ LastModified = $_.LastWriteTime
+ FullName = $_.FullName
+ }
+ }
+ foreach ($it in $items) { $listView.Items.Add($it) | Out-Null }
+ WriteLog "Additional FFUs: Found $($listView.Items.Count) FFU files in $ffuFolder."
+ }
+ $headerChk = $State.Controls.chkSelectAllAdditionalFFUs
+ if ($null -ne $headerChk) {
+ Update-SelectAllHeaderCheckBoxState -ListView $listView -HeaderCheckBox $headerChk
+ }
+ }
+ catch {
+ WriteLog "Update-AdditionalFFUList error: $($_.Exception.Message)"
+ }
+}
+
# Function to manage the visibility of the application UI panels
function Update-ApplicationPanelVisibility {
param(