From eac8be3d3110fad9f421919844ff76b1c14457ac Mon Sep 17 00:00:00 2001
From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com>
Date: Wed, 18 Mar 2026 18:59:28 -0700
Subject: [PATCH] Allow custom BYO app list file paths in UI
Updates the FFU UI and orchestration scripts to allow users to specify custom file paths for their Bring Your Own (BYO) app lists, rather than forcing the use of `UserAppList.json` in a specific directory.
Also modifies the orchestration to sync this custom path via `AppInstallConfig.json` so that the runtime orchestration phase resolves the correct file name and path during installation. Refreshes the Apps ISO if the custom BYO app list is updated.
---
.../Apps/Orchestration/Install-Win32Apps.ps1 | 25 +++---
.../Apps/Orchestration/Orchestrator.ps1 | 27 +++++-
FFUDevelopment/BuildFFUVM.ps1 | 84 +++++++++++++++++--
FFUDevelopment/BuildFFUVM_UI.xaml | 27 ++++--
.../FFU.Common/FFU.Common.Parallel.psm1 | 1 +
.../FFU.Common/FFU.Common.Winget.psm1 | 11 ++-
.../FFUUI.Core/FFUUI.Core.Applications.psm1 | 36 ++++++--
.../FFUUI.Core/FFUUI.Core.Config.psm1 | 3 +-
.../FFUUI.Core/FFUUI.Core.Handlers.psm1 | 33 ++++++--
.../FFUUI.Core/FFUUI.Core.Initialize.psm1 | 4 +
.../FFUUI.Core/FFUUI.Core.Winget.psm1 | 43 ++++++----
FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1 | 3 +
12 files changed, 237 insertions(+), 60 deletions(-)
diff --git a/FFUDevelopment/Apps/Orchestration/Install-Win32Apps.ps1 b/FFUDevelopment/Apps/Orchestration/Install-Win32Apps.ps1
index 6d4ab63..0316bb0 100644
--- a/FFUDevelopment/Apps/Orchestration/Install-Win32Apps.ps1
+++ b/FFUDevelopment/Apps/Orchestration/Install-Win32Apps.ps1
@@ -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 {
[CmdletBinding(SupportsShouldProcess)]
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
$wingetApps = @()
$userApps = @()
@@ -286,9 +289,9 @@ if ($wingetApps.Count -gt 0) {
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) {
- Write-Host "Processing UserAppList.json..."
+ Write-Host "Processing $(Split-Path -Path $userAppsJsonFile -Leaf)..."
try {
$userContent = Get-Content -Path $userAppsJsonFile -Raw -ErrorAction Stop | ConvertFrom-Json
if ($userContent -is [array]) {
@@ -296,19 +299,19 @@ if (Test-Path -Path $userAppsJsonFile) {
Write-Host "Found $(($userApps | Measure-Object).Count) user-defined apps."
}
elseif ($userContent) {
- $userApps = @($userContent) # Ensure it's an array
+ $userApps = @($userContent)
Write-Host "Found 1 user-defined app."
}
else {
- Write-Host "UserAppList.json is empty or invalid."
+ Write-Host "$(Split-Path -Path $userAppsJsonFile -Leaf) is empty or invalid."
}
}
catch {
- Write-Error "Failed to read or parse UserAppList.json file: $_"
+ Write-Error "Failed to read or parse BYO app list file '$userAppsJsonFile': $_"
}
}
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
diff --git a/FFUDevelopment/Apps/Orchestration/Orchestrator.ps1 b/FFUDevelopment/Apps/Orchestration/Orchestrator.ps1
index 9f6bc68..88c23f1 100644
--- a/FFUDevelopment/Apps/Orchestration/Orchestrator.ps1
+++ b/FFUDevelopment/Apps/Orchestration/Orchestrator.ps1
@@ -28,6 +28,23 @@ Write-Host "---------------------------------------------------" -ForegroundColo
# Define the path to the scripts
$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
$scriptList = @(
"Install-LTSCUpdate.ps1",
@@ -51,7 +68,6 @@ foreach ($script in $scriptList) {
switch ($script) {
"Install-Win32Apps.ps1" {
$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)) {
$shouldRun = $false
}
@@ -69,8 +85,13 @@ foreach ($script in $scriptList) {
Write-Host "---------------------------------------------------" -ForegroundColor Yellow
Write-Host " Running script: $script " -ForegroundColor Yellow
Write-Host "---------------------------------------------------" -ForegroundColor Yellow
- # Run script and wait for it to finish
- & $scriptFile
+ # Run script and wait for it to finish.
+ if ($script -eq "Install-Win32Apps.ps1") {
+ & $scriptFile -UserAppsJsonFile $userAppsJsonFile
+ }
+ else {
+ & $scriptFile
+ }
}
}
diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1
index 6a197fb..1554d87 100644
--- a/FFUDevelopment/BuildFFUVM.ps1
+++ b/FFUDevelopment/BuildFFUVM.ps1
@@ -633,6 +633,7 @@ if (-not $AppsPath) { $AppsPath = "$FFUDevelopmentPath\Apps" }
if (-not $AppListPath) { $AppListPath = "$AppsPath\AppList.json" }
if (-not $UserAppListPath) { $UserAppListPath = "$AppsPath\UserAppList.json" }
if (-not $OrchestrationPath) { $OrchestrationPath = "$AppsPath\Orchestration" }
+if (-not $appInstallConfigPath) { $appInstallConfigPath = "$OrchestrationPath\AppInstallConfig.json" }
if (-not $wingetWin32jsonFile) { $wingetWin32jsonFile = "$OrchestrationPath\WinGetWin32Apps.json" }
if (-not $InstallOfficePath) { $InstallOfficePath = "$OrchestrationPath\Install-Office.ps1" }
if (-not $InstallDefenderPath) { $InstallDefenderPath = "$OrchestrationPath\Update-Defender.ps1" }
@@ -2437,6 +2438,54 @@ function Save-KB {
return $fileName
}
+function Sync-UserAppListForOrchestration {
+ param(
+ [Parameter(Mandatory)]
+ [string]$SourcePath,
+ [Parameter(Mandatory)]
+ [string]$AppsPath,
+ [Parameter(Mandatory)]
+ [string]$OrchestrationPath,
+ [Parameter(Mandatory)]
+ [string]$AppInstallConfigPath
+ )
+
+ # Ensure the orchestration folder exists before writing runtime configuration.
+ if (-not (Test-Path -Path $OrchestrationPath -PathType Container)) {
+ New-Item -Path $OrchestrationPath -ItemType Directory -Force | Out-Null
+ }
+
+ # Persist the runtime BYO app list path so Orchestrator can honor custom file names.
+ $appInstallConfig = [ordered]@{
+ UserAppListPath = $null
+ }
+
+ if (-not [string]::IsNullOrWhiteSpace($SourcePath) -and (Test-Path -Path $SourcePath -PathType Leaf)) {
+ $stagedUserAppListName = Split-Path -Path $SourcePath -Leaf
+ $stagedUserAppListPath = Join-Path -Path $AppsPath -ChildPath $stagedUserAppListName
+
+ if (-not [string]::Equals([System.IO.Path]::GetFullPath($SourcePath), [System.IO.Path]::GetFullPath($stagedUserAppListPath), [System.StringComparison]::OrdinalIgnoreCase)) {
+ Copy-Item -Path $SourcePath -Destination $stagedUserAppListPath -Force | Out-Null
+ WriteLog "Staged BYO app list for orchestration: $SourcePath -> $stagedUserAppListPath"
+ }
+ else {
+ WriteLog "Using BYO app list already staged at $stagedUserAppListPath"
+ }
+
+ $appInstallConfig.UserAppListPath = "D:\$stagedUserAppListName"
+ }
+ elseif (Test-Path -Path (Join-Path -Path $AppsPath -ChildPath 'UserAppList.json') -PathType Leaf) {
+ $appInstallConfig.UserAppListPath = "D:\UserAppList.json"
+ WriteLog "Using default BYO app list path for orchestration."
+ }
+ else {
+ WriteLog "No BYO app list found at configured path '$SourcePath'."
+ }
+
+ $appInstallConfig | ConvertTo-Json | Set-Content -Path $AppInstallConfigPath -Encoding UTF8
+ WriteLog "Wrote app install config to $AppInstallConfigPath"
+}
+
function New-AppsISO {
#Create Apps ISO file
$OSCDIMG = "$adkpath`Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg\oscdimg.exe"
@@ -6082,7 +6131,19 @@ if ($InstallApps) {
Set-Progress -Percentage 6 -Message "Downloading and preparing applications..."
if (Test-Path -Path $AppsISO) {
WriteLog "Apps ISO exists at: $AppsISO"
- WriteLog "Will use existing ISO"
+
+ # Refresh the Apps ISO when a BYO app list is present so the staged manifest
+ # and AppInstallConfig.json stay in sync with the current build inputs.
+ if (Test-Path -Path $UserAppListPath) {
+ WriteLog "Configured BYO app list detected. Refreshing Apps ISO to include the latest BYO app list data."
+ Sync-UserAppListForOrchestration -SourcePath $UserAppListPath -AppsPath $AppsPath -OrchestrationPath $OrchestrationPath -AppInstallConfigPath $appInstallConfigPath
+ Remove-Item -Path $AppsISO -Force -ErrorAction SilentlyContinue
+ New-AppsISO
+ WriteLog "Apps ISO refreshed to include the latest BYO app list data."
+ }
+ else {
+ WriteLog "Will use existing ISO"
+ }
}
else {
try {
@@ -6125,7 +6186,7 @@ if ($InstallApps) {
# If there are no existing apps, use the original AppList.json directly
if (-not $hasExistingApps) {
WriteLog "No existing applications found. Using original AppList.json for all apps."
- Get-Apps -AppList $AppListPath -AppsPath $AppsPath -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath -LogFilePath $LogFile -ThrottleLimit $Threads
+ Get-Apps -AppList $AppListPath -AppsPath $AppsPath -UserAppListPath $UserAppListPath -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath -LogFilePath $LogFile -ThrottleLimit $Threads
}
else {
# Compare apps in AppList.json with existing installations
@@ -6197,7 +6258,7 @@ if ($InstallApps) {
# Download missing apps
WriteLog "Downloading missing applications"
- Get-Apps -AppList $modifiedAppListPath -AppsPath $AppsPath -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath -LogFilePath $LogFile -ThrottleLimit $Threads
+ Get-Apps -AppList $modifiedAppListPath -AppsPath $AppsPath -UserAppListPath $UserAppListPath -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath -LogFilePath $LogFile -ThrottleLimit $Threads
# Cleanup modified app list
Remove-Item -Path $modifiedAppListPath -Force
@@ -6207,11 +6268,11 @@ if ($InstallApps) {
}
}
}
- # Check is UserAppList.json exists and output to the user which apps will be installed
- # It's expected that the user will have already copied the applications and created the UserAppList.json file
+ # Check if the configured BYO app list exists and output which apps will be installed.
+ # It is expected that the user will have already copied the applications and created the BYO app list file.
if (Test-Path -Path $UserAppListPath) {
$userAppList = Get-Content -Path $UserAppListPath -Raw | ConvertFrom-Json
- WriteLog "UserAppList.json found, the following apps will be installed:"
+ WriteLog "$(Split-Path -Path $UserAppListPath -Leaf) found, the following apps will be installed:"
foreach ($app in $userAppList) {
WriteLog "$($app.name)"
}
@@ -6525,6 +6586,9 @@ if ($InstallApps) {
WriteLog "InjectUnattend is true but source file missing: $unattendSource. Skipping unattend injection."
}
}
+ # Stage the configured BYO app list and runtime config before creating the Apps ISO.
+ Sync-UserAppListForOrchestration -SourcePath $UserAppListPath -AppsPath $AppsPath -OrchestrationPath $OrchestrationPath -AppInstallConfigPath $appInstallConfigPath
+
Set-Progress -Percentage 10 -Message "Creating Apps ISO..."
WriteLog "Creating $AppsISO file"
New-AppsISO
@@ -7434,6 +7498,7 @@ if ($InstallApps) {
Remove-Item -Path $AppsISO -Force -ErrorAction SilentlyContinue
WriteLog 'Removal complete'
}
+ Sync-UserAppListForOrchestration -SourcePath $UserAppListPath -AppsPath $AppsPath -OrchestrationPath $OrchestrationPath -AppInstallConfigPath $appInstallConfigPath
New-AppsISO
WriteLog "Apps ISO refreshed with LTSC CU assets"
}
@@ -7621,6 +7686,13 @@ if (Test-Path -Path $wingetWin32jsonFile -PathType Leaf) {
Remove-Item -Path $wingetWin32jsonFile -Force -ErrorAction SilentlyContinue
WriteLog "Removal complete"
}
+
+# Remove AppInstallConfig.json so it is always rebuilt next run
+if (Test-Path -Path $appInstallConfigPath -PathType Leaf) {
+ WriteLog "Removing $appInstallConfigPath"
+ Remove-Item -Path $appInstallConfigPath -Force -ErrorAction SilentlyContinue
+ WriteLog "Removal complete"
+}
#Set $LongPathsEnabled registry value back to original value. $LongPathsEnabled could be $null if the registry value was not found
if ($null -eq $LongPathsEnabled) {
Remove-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -ErrorAction SilentlyContinue
diff --git a/FFUDevelopment/BuildFFUVM_UI.xaml b/FFUDevelopment/BuildFFUVM_UI.xaml
index b12a5b6..ae13f82 100644
--- a/FFUDevelopment/BuildFFUVM_UI.xaml
+++ b/FFUDevelopment/BuildFFUVM_UI.xaml
@@ -270,19 +270,32 @@
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -336,8 +349,8 @@
-
-
+
+
@@ -405,8 +418,8 @@
-
-
+
+
diff --git a/FFUDevelopment/FFU.Common/FFU.Common.Parallel.psm1 b/FFUDevelopment/FFU.Common/FFU.Common.Parallel.psm1
index dd982a8..ce2f475 100644
--- a/FFUDevelopment/FFU.Common/FFU.Common.Parallel.psm1
+++ b/FFUDevelopment/FFU.Common/FFU.Common.Parallel.psm1
@@ -161,6 +161,7 @@ function Invoke-ParallelProcessing {
ApplicationItemData = $currentItem
AppListJsonPath = $localJobArgs['AppListJsonPath']
AppsPath = $localJobArgs['AppsPath']
+ UserAppListPath = $localJobArgs['UserAppListPath']
OrchestrationPath = $localJobArgs['OrchestrationPath']
ProgressQueue = $localProgressQueue
WindowsArch = $localJobArgs['WindowsArch']
diff --git a/FFUDevelopment/FFU.Common/FFU.Common.Winget.psm1 b/FFUDevelopment/FFU.Common/FFU.Common.Winget.psm1
index 32a0e42..1e9aecb 100644
--- a/FFUDevelopment/FFU.Common/FFU.Common.Winget.psm1
+++ b/FFUDevelopment/FFU.Common/FFU.Common.Winget.psm1
@@ -358,6 +358,8 @@ function Start-WingetAppDownloadTask {
[string]$AppListJsonPath,
[Parameter(Mandatory = $true)]
[string]$AppsPath,
+ [Parameter()]
+ [string]$UserAppListPath,
[Parameter(Mandatory = $true)]
[string]$OrchestrationPath,
[Parameter(Mandatory = $true)]
@@ -379,11 +381,11 @@ function Start-WingetAppDownloadTask {
WriteLog "Starting download task for $($appName) with ID $($appId) from source $($source)."
try {
- # Define paths
- $userAppListPath = Join-Path -Path $AppsPath -ChildPath "UserAppList.json"
+ # Resolve the BYO app list path so duplicate checks honor custom file names.
+ $userAppListPath = if (-not [string]::IsNullOrWhiteSpace($UserAppListPath)) { $UserAppListPath } else { Join-Path -Path $AppsPath -ChildPath "UserAppList.json" }
$appFound = $false
- # 1. Check UserAppList.json and content
+ # 1. Check the configured BYO app list and content
if (Test-Path -Path $userAppListPath) {
try {
$userAppListContent = Get-Content -Path $userAppListPath -Raw | ConvertFrom-Json
@@ -724,6 +726,8 @@ function Get-Apps {
[string]$AppList,
[Parameter(Mandatory = $true)]
[string]$AppsPath,
+ [Parameter()]
+ [string]$UserAppListPath,
[Parameter(Mandatory = $true)]
[string]$WindowsArch,
[Parameter(Mandatory = $true)]
@@ -787,6 +791,7 @@ function Get-Apps {
# CLI builds should create WinGetWin32Apps.json, so SkipWin32Json is false
$taskArguments = @{
AppsPath = $AppsPath
+ UserAppListPath = $UserAppListPath
AppListJsonPath = $AppList
OrchestrationPath = $OrchestrationPath
WindowsArch = $WindowsArch
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Applications.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Applications.psm1
index a85a320..15fbdaa 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Applications.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Applications.psm1
@@ -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 Remove-SelectedBYOApplications {
[CmdletBinding()]
@@ -76,10 +92,10 @@ function Remove-SelectedBYOApplications {
}
# 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') {
- $userAppListPath = Join-Path -Path $State.Controls.txtApplicationPath.Text -ChildPath 'UserAppList.json'
+ $userAppListPath = Get-BYOApplicationListPath -State $State
Save-BYOApplicationList -Path $userAppListPath -State $State
}
}
@@ -391,18 +407,24 @@ function Invoke-CopyBYOApps {
)
$localAppsPath = $State.Controls.txtApplicationPath.Text
- $userAppListPath = Join-Path -Path $localAppsPath -ChildPath 'UserAppList.json'
+ $userAppListPath = Get-BYOApplicationListPath -State $State
$listView = $State.Controls.lstApplications
try {
- # 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
+ # Ensure the configured BYO app list folder exists before writing the manifest.
+ $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 | 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 {
- $errorMessage = "Failed to update UserAppList.json: $_"
+ $errorMessage = "Failed to update BYO app list at $($userAppListPath): $_"
WriteLog $errorMessage
[System.Windows.MessageBox]::Show($errorMessage, "Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
return
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1
index befaed3..f3d7098 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1
@@ -93,7 +93,7 @@ function Get-UIConfig {
UpdateLatestNet = $State.Controls.chkUpdateLatestNet.IsChecked
UpdateOneDrive = $State.Controls.chkUpdateOneDrive.IsChecked
UpdatePreviewCU = $State.Controls.chkUpdatePreviewCU.IsChecked
- UserAppListPath = "$($State.Controls.txtApplicationPath.Text)\UserAppList.json"
+ UserAppListPath = $State.Controls.txtUserAppListPath.Text
USBDriveList = @{}
Username = $State.Controls.txtUsername.Text
Threads = [int]$State.Controls.txtThreads.Text
@@ -585,6 +585,7 @@ function Update-UIFromConfig {
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 'txtAppListJsonPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'AppListPath' -State $State
+ Set-UIValue -ControlName 'txtUserAppListPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'UserAppListPath' -State $State
# Handle AppsScriptVariables
$appsScriptVarsKeyExists = $false
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1
index 1881c40..5446e7a 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1
@@ -435,10 +435,18 @@ function Register-EventHandlers {
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
$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 }
})
+ $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({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
@@ -466,17 +474,23 @@ function Register-EventHandlers {
$window = [System.Windows.Window]::GetWindow($eventSource)
$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 }
+ $fileName = if (-not [string]::IsNullOrWhiteSpace($currentPath)) { Split-Path -Path $currentPath -Leaf } else { "UserAppList.json" }
$savePath = Invoke-BrowseAction -Type 'SaveFile' `
- -Title "Save Application List" `
+ -Title "Save BYO App List" `
-Filter "JSON files (*.json)|*.json|All files (*.*)|*.*" `
-InitialDirectory $initialDir `
- -FileName "UserAppList.json" `
+ -FileName $fileName `
-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({
@@ -484,15 +498,18 @@ function Register-EventHandlers {
$window = [System.Windows.Window]::GetWindow($eventSource)
$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 }
$loadPath = Invoke-BrowseAction -Type 'OpenFile' `
- -Title "Import Application List" `
+ -Title "Import BYO App List" `
-Filter "JSON files (*.json)|*.json|All files (*.*)|*.*" `
-InitialDirectory $initialDir
- if ($loadPath) {
+ if ($loadPath) {
+ $localState.Controls.txtUserAppListPath.Text = $loadPath
Import-BYOApplicationList -Path $loadPath -State $localState
Update-CopyButtonState -State $localState
}
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
index fc84a6c..5cd41fa 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
@@ -70,8 +70,10 @@ function Initialize-UIControls {
$State.Controls.txtWingetModuleVersion = $window.FindName('txtWingetModuleVersion')
$State.Controls.applicationPathPanel = $window.FindName('applicationPathPanel')
$State.Controls.appListJsonPathPanel = $window.FindName('appListJsonPathPanel')
+ $State.Controls.userAppListPathPanel = $window.FindName('userAppListPathPanel')
$State.Controls.btnBrowseApplicationPath = $window.FindName('btnBrowseApplicationPath')
$State.Controls.btnBrowseAppListJsonPath = $window.FindName('btnBrowseAppListJsonPath')
+ $State.Controls.btnBrowseUserAppListPath = $window.FindName('btnBrowseUserAppListPath')
$State.Controls.chkBringYourOwnApps = $window.FindName('chkBringYourOwnApps')
$State.Controls.byoApplicationPanel = $window.FindName('byoApplicationPanel')
$State.Controls.wingetSearchPanel = $window.FindName('wingetSearchPanel')
@@ -159,6 +161,7 @@ function Initialize-UIControls {
$State.Controls.chkUpdatePreviewCU = $window.FindName('chkUpdatePreviewCU')
$State.Controls.txtApplicationPath = $window.FindName('txtApplicationPath')
$State.Controls.txtAppListJsonPath = $window.FindName('txtAppListJsonPath')
+ $State.Controls.txtUserAppListPath = $window.FindName('txtUserAppListPath')
$State.Controls.chkInstallDrivers = $window.FindName('chkInstallDrivers')
$State.Controls.chkCopyDrivers = $window.FindName('chkCopyDrivers')
$State.Controls.chkCompressDriversToWIM = $window.FindName('chkCompressDriversToWIM')
@@ -305,6 +308,7 @@ function Initialize-UIDefaults {
$State.Controls.chkInstallApps.IsChecked = $State.Defaults.generalDefaults.InstallApps
$State.Controls.txtApplicationPath.Text = $State.Defaults.generalDefaults.ApplicationPath
$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.chkBringYourOwnApps.IsChecked = $State.Defaults.generalDefaults.BringYourOwnApps
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Winget.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Winget.psm1
index 7a8c8f9..3adc7af 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Winget.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Winget.psm1
@@ -108,20 +108,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.Filter = "JSON files (*.json)|*.json"
- $sfd.Title = "Save App List"
- # Correctly get the path from the UI control via the State object
- $sfd.InitialDirectory = $State.Controls.txtApplicationPath.Text
- $sfd.FileName = "AppList.json"
+ $sfd.Title = "Save Winget App List"
+ $sfd.InitialDirectory = $initialDirectory
+ $sfd.FileName = $fileName
if ($sfd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
$appList | ConvertTo-Json -Depth 10 | Set-Content $sfd.FileName -Encoding UTF8
- [System.Windows.MessageBox]::Show("App list saved successfully.", "Success", "OK", "Information")
+ $State.Controls.txtAppListJsonPath.Text = $sfd.FileName
+ [System.Windows.MessageBox]::Show("Winget app list saved successfully.", "Success", "OK", "Information")
}
}
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 +140,17 @@ function Import-WingetList {
[psobject]$State
)
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.Filter = "JSON files (*.json)|*.json"
- $ofd.Title = "Import App List"
- # Correctly get the path from the UI control via the State object
- $ofd.InitialDirectory = $State.Controls.txtApplicationPath.Text
+ $ofd.Title = "Import Winget App List"
+ $ofd.InitialDirectory = $initialDirectory
if ($ofd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
$importedAppsData = Get-Content $ofd.FileName -Raw | ConvertFrom-Json
@@ -144,16 +158,16 @@ function Import-WingetList {
$newAppListForItemsSource = [System.Collections.Generic.List[object]]::new()
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
foreach ($appInfo in $importedAppsData.apps) {
$arch = if ($appInfo.source -eq 'msstore') { 'NA' } else { if ($appInfo.PSObject.Properties['architecture']) { $appInfo.architecture } else { $defaultArch } }
$newAppListForItemsSource.Add([PSCustomObject]@{
- IsSelected = $true # Imported apps are marked as selected
+ IsSelected = $true
Name = $appInfo.name
Id = $appInfo.id
- Version = "" # Will be populated when searching or if data exists
+ Version = ""
Source = $appInfo.source
Architecture = $arch
AdditionalExitCodes = if ($appInfo.PSObject.Properties['AdditionalExitCodes']) { $appInfo.AdditionalExitCodes } else { "" }
@@ -164,12 +178,13 @@ function Import-WingetList {
}
$State.Controls.lstWingetResults.ItemsSource = $newAppListForItemsSource.ToArray()
+ $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 {
- [System.Windows.MessageBox]::Show("Error importing app list: $_", "Error", "OK", "Error")
+ [System.Windows.MessageBox]::Show("Error importing Winget app list: $_", "Error", "OK", "Error")
}
}
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1
index a01c312..c9c2e10 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1
@@ -108,6 +108,7 @@ function Get-GeneralDefaults {
$ffuCapturePath = Join-Path -Path $FFUDevelopmentPath -ChildPath "FFU"
$officePath = Join-Path -Path $appsPath -ChildPath "Office"
$appListJsonPath = Join-Path -Path $appsPath -ChildPath "AppList.json"
+ $userAppListPath = Join-Path -Path $appsPath -ChildPath "UserAppList.json"
$driversJsonPath = Join-Path -Path $driversPath -ChildPath "Drivers.json"
return [PSCustomObject]@{
@@ -163,6 +164,7 @@ function Get-GeneralDefaults {
InstallApps = $false
ApplicationPath = $appsPath
AppListJsonPath = $appListJsonPath
+ UserAppListPath = $userAppListPath
InstallWingetApps = $false
BringYourOwnApps = $false
# M365 Apps/Office Tab Defaults
@@ -305,6 +307,7 @@ function Update-ApplicationPanelVisibility {
$subOptionVisibility = if ($installAppsChecked) { 'Visible' } else { 'Collapsed' }
$State.Controls.applicationPathPanel.Visibility = $subOptionVisibility
$State.Controls.appListJsonPathPanel.Visibility = $subOptionVisibility
+ $State.Controls.userAppListPathPanel.Visibility = $subOptionVisibility
$State.Controls.chkInstallWingetApps.Visibility = $subOptionVisibility
$State.Controls.chkBringYourOwnApps.Visibility = $subOptionVisibility
$State.Controls.chkDefineAppsScriptVariables.Visibility = $subOptionVisibility