feat: Add per-app architecture selection and multi-arch downloads

Introduces the ability to specify and download multiple architectures for a single application.

- Adds an "Architecture" dropdown column to the Winget UI, allowing users to select the desired architecture(s) for each app (e.g., x86, x64, arm64, or 'x86 x64').
- Updates the download logic to process each specified architecture, creating separate subfolders for multi-architecture Win32 apps.
- Modifies the app list format to save and load the selected architecture for each application.
- Improves the download process by checking if an application has already been downloaded before attempting a new download.
This commit is contained in:
rbalsleyMSFT
2025-07-18 20:12:05 -07:00
parent 9df663dc9b
commit 721f93d82d
4 changed files with 243 additions and 149 deletions
@@ -384,8 +384,48 @@ function Initialize-DynamicUIElements {
Add-SortableColumn -gridView $wingetGridView -header "Id" -binding "Id" -width 200 -headerHorizontalAlignment Left
Add-SortableColumn -gridView $wingetGridView -header "Version" -binding "Version" -width 100 -headerHorizontalAlignment Left
Add-SortableColumn -gridView $wingetGridView -header "Source" -binding "Source" -width 100 -headerHorizontalAlignment Left
Add-SortableColumn -gridView $wingetGridView -header "Download Status" -binding "DownloadStatus" -width 150 -headerHorizontalAlignment Left
# --- START: Add Architecture Column ---
$archColumn = New-Object System.Windows.Controls.GridViewColumn
$archHeader = New-Object System.Windows.Controls.GridViewColumnHeader
$archHeader.Tag = "Architecture" # For sorting
$archHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left
# Create header content with correct padding to match other columns
$commonPaddingForHeader = New-Object System.Windows.Thickness(5, 2, 5, 2)
$headerTextElementFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBlock])
$headerTextElementFactory.SetValue([System.Windows.Controls.TextBlock]::TextProperty, "Architecture")
$headerTextBlockPadding = New-Object System.Windows.Thickness($commonPaddingForHeader.Left, $commonPaddingForHeader.Top, $commonPaddingForHeader.Right, $commonPaddingForHeader.Bottom)
$headerTextElementFactory.SetValue([System.Windows.Controls.TextBlock]::PaddingProperty, $headerTextBlockPadding)
$headerTextElementFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
$headerDataTemplate = New-Object System.Windows.DataTemplate
$headerDataTemplate.VisualTree = $headerTextElementFactory
$archHeader.ContentTemplate = $headerDataTemplate
$archColumn.Header = $archHeader
$archColumn.Width = 120
# Create the CellTemplate with a ComboBox
$archCellTemplate = New-Object System.Windows.DataTemplate
$comboBoxFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.ComboBox])
# The ItemsSource for the ComboBox
$availableArchitectures = @('x86', 'x64', 'arm64', 'x86 x64')
$comboBoxFactory.SetValue([System.Windows.Controls.ItemsControl]::ItemsSourceProperty, $availableArchitectures)
# Bind the text property to the 'Architecture' property of the data item.
# This ensures the initial value is displayed correctly.
$binding = New-Object System.Windows.Data.Binding("Architecture")
$binding.Mode = [System.Windows.Data.BindingMode]::TwoWay
$comboBoxFactory.SetBinding([System.Windows.Controls.ComboBox]::TextProperty, $binding)
$archCellTemplate.VisualTree = $comboBoxFactory
$archColumn.CellTemplate = $archCellTemplate
$wingetGridView.Columns.Add($archColumn)
# --- END: Add Architecture Column ---
Add-SortableColumn -gridView $wingetGridView -header "Download Status" -binding "DownloadStatus" -width 150 -headerHorizontalAlignment Left
$State.Controls.lstWingetResults.AddHandler(
[System.Windows.Controls.GridViewColumnHeader]::ClickEvent,
[System.Windows.RoutedEventHandler] {
@@ -27,9 +27,12 @@ function Search-WingetApps {
# Store selected apps from the current view
$selectedAppsFromView = @($currentItemsInListView | Where-Object { $_.IsSelected })
# Get default architecture from the UI
$defaultArch = $State.Controls.cmbWindowsArch.SelectedItem
# Search for new apps, which are streamed directly as PSCustomObjects
# with the required properties for performance.
$searchedAppResults = Search-WingetPackagesPublic -Query $searchQuery
$searchedAppResults = Search-WingetPackagesPublic -Query $searchQuery -DefaultArchitecture $defaultArch
WriteLog "Found $($searchedAppResults.Count) apps matching query '$searchQuery'."
$finalAppList = [System.Collections.Generic.List[object]]::new()
@@ -73,9 +76,10 @@ function Save-WingetList {
$appList = @{
apps = @($selectedApps | ForEach-Object {
[ordered]@{
name = $_.Name
id = $_.Id
source = $_.Source.ToLower()
name = $_.Name
id = $_.Id
source = $_.Source.ToLower()
architecture = $_.Architecture
}
})
}
@@ -116,6 +120,9 @@ function Import-WingetList {
$newAppListForItemsSource = [System.Collections.Generic.List[object]]::new()
if ($null -ne $importedAppsData.apps) {
# Get default architecture from the UI for fallback
$defaultArch = $State.Controls.cmbWindowsArch.SelectedItem
foreach ($appInfo in $importedAppsData.apps) {
$newAppListForItemsSource.Add([PSCustomObject]@{
IsSelected = $true # Imported apps are marked as selected
@@ -123,6 +130,7 @@ function Import-WingetList {
Id = $appInfo.id
Version = "" # Will be populated when searching or if data exists
Source = $appInfo.source
Architecture = if ($appInfo.PSObject.Properties['architecture']) { $appInfo.architecture } else { $defaultArch }
DownloadStatus = ""
})
}
@@ -145,7 +153,9 @@ function Search-WingetPackagesPublic {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Query
[string]$Query,
[Parameter(Mandatory = $true)]
[string]$DefaultArchitecture
)
WriteLog "Searching Winget packages with query: '$Query'"
@@ -160,6 +170,7 @@ function Search-WingetPackagesPublic {
Id,
Version,
Source,
@{Name = 'Architecture'; Expression = { $DefaultArchitecture } },
@{Name = 'DownloadStatus'; Expression = { '' } }
}
catch {
@@ -342,8 +353,6 @@ function Start-WingetAppDownloadTask {
[Parameter(Mandatory = $true)]
[string]$AppsPath, # Pass necessary paths
[Parameter(Mandatory = $true)]
[string]$WindowsArch,
[Parameter(Mandatory = $true)]
[string]$OrchestrationPath,
[Parameter(Mandatory = $true)]
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue # Add queue parameter
@@ -361,7 +370,7 @@ function Start-WingetAppDownloadTask {
WriteLog "Starting download task for $($appName) with ID $($appId) from source $($source)."
# WriteLog "Apps Path: $($AppsPath)"
# WriteLog "AppList JSON Path: $($AppListJsonPath)"
# WriteLog "Windows Architecture: $($WindowsArch)"
# WriteLog "Windows Architecture: $($ApplicationItemData.Architecture)"
# WriteLog "Orchestration Path: $($OrchestrationPath)"
try {
@@ -449,7 +458,7 @@ function Start-WingetAppDownloadTask {
}
# For now, assuming Get-Application uses $global variables set in the main script or $using: scope.
# $global:AppsPath = $AppsPath # Potentially redundant if set globally before parallel call
# $global:WindowsArch = $WindowsArch # Potentially redundant
# $global:WindowsArch = $ApplicationItemData.Architecture # Potentially redundant
# $global:orchestrationPath = $OrchestrationPath # Potentially redundant
$wingetWin32jsonFile = Join-Path -Path $OrchestrationPath -ChildPath "WinGetWin32Apps.json"
@@ -577,7 +586,7 @@ function Start-WingetAppDownloadTask {
# Ensure variables needed by Get-Application are accessible
# (Assuming they are available via $using: scope or global scope from main script)
# $global:AppsPath = $AppsPath # Potentially redundant
# $global:WindowsArch = $WindowsArch # Potentially redundant
# $global:WindowsArch = $ApplicationItemData.Architecture # Potentially redundant
# $global:orchestrationPath = $OrchestrationPath # Potentially redundant"
WriteLog "Orchestration Path: $($OrchestrationPath)"
if (-not (Test-Path -Path $OrchestrationPath -PathType Container)) {
@@ -594,7 +603,7 @@ function Start-WingetAppDownloadTask {
try {
# Call Get-Application
$resultCode = Get-Application -AppName $appName -AppId $appId -Source $source -AppsPath $AppsPath -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath -ErrorAction Stop
$resultCode = Get-Application -AppName $appName -AppId $appId -Source $source -AppsPath $AppsPath -WindowsArch $ApplicationItemData.Architecture -OrchestrationPath $OrchestrationPath -ErrorAction Stop
# Determine status based on result code
switch ($resultCode) {
@@ -713,13 +722,11 @@ function Invoke-WingetDownload {
$taskArguments = @{
AppsPath = $localAppsPath
AppListJsonPath = $localAppListJsonPath
WindowsArch = $localWindowsArch
OrchestrationPath = $localOrchestrationPath
}
# Select only necessary properties before passing to Invoke-ParallelProcessing
$itemsToProcess = $selectedApps | Select-Object Name, Id, Source, Version # Include Version if needed
$itemsToProcess = $selectedApps | Select-Object Name, Id, Source, Version, Architecture # Include Version and Architecture if needed
# Invoke the centralized parallel processing function
# Pass task type and task-specific arguments
Invoke-ParallelProcessing -ItemsToProcess $itemsToProcess `