From e05af92acc34489b08de205198d3886f7c5e0367 Mon Sep 17 00:00:00 2001 From: Jeffrey <9938813+Raphire@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:59:04 +0100 Subject: [PATCH 1/2] Add support for multiple AppIds for app removal (#526) --- Config/Apps.json | 2 +- Scripts/AppRemoval/RemoveApps.ps1 | 50 ++++++++++++++++------ Scripts/FileIO/LoadAppsDetailsFromJson.ps1 | 32 +++++++++----- Scripts/FileIO/LoadAppsFromFile.ps1 | 8 ++-- Scripts/FileIO/ValidateAppslist.ps1 | 2 +- Scripts/GUI/Show-AppSelectionWindow.ps1 | 6 ++- Scripts/GUI/Show-MainWindow.ps1 | 35 +++++++-------- 7 files changed, 85 insertions(+), 50 deletions(-) diff --git a/Config/Apps.json b/Config/Apps.json index 44262f5..bfd5f98 100644 --- a/Config/Apps.json +++ b/Config/Apps.json @@ -647,7 +647,7 @@ }, { "FriendlyName": "Microsoft Edge", - "AppId": "Microsoft.Edge", + "AppId": ["Microsoft.Edge", "XPFFTQ037JWMHS"], "Description": "Windows' default browser, WARNING: Removing this app also removes the only browser from Windows Sandbox and could affect other apps", "SelectedByDefault": false, "Recommendation": "unsafe" diff --git a/Scripts/AppRemoval/RemoveApps.ps1 b/Scripts/AppRemoval/RemoveApps.ps1 index 38962e0..b365fb4 100644 --- a/Scripts/AppRemoval/RemoveApps.ps1 +++ b/Scripts/AppRemoval/RemoveApps.ps1 @@ -9,6 +9,9 @@ function RemoveApps { $appIndex = 0 $appCount = @($appsList).Count + $edgeIds = @('Microsoft.Edge', 'XPFFTQ037JWMHS') + $edgeUninstallSucceeded = $false + $edgeScheduledTaskAdded = $false Foreach ($app in $appsList) { if ($script:CancelRequested) { @@ -25,20 +28,27 @@ function RemoveApps { Write-Host "Attempting to remove $app..." # Use WinGet only to remove OneDrive and Edge - if (($app -eq "Microsoft.OneDrive") -or ($app -eq "Microsoft.Edge")) { + if (($app -eq "Microsoft.OneDrive") -or ($edgeIds -contains $app)) { if ($script:WingetInstalled -eq $false) { Write-Host "WinGet is either not installed or is outdated, $app could not be removed" -ForegroundColor Red continue } - $appName = $app -replace '\.', '_' + $isEdgeId = $edgeIds -contains $app + $appName = if ($isEdgeId) { 'Microsoft_Edge' } else { $app -replace '\.', '_' } # Uninstall app via WinGet, or create a scheduled task to uninstall it later if ($script:Params.ContainsKey("User")) { - ImportRegistryFile "Adding scheduled task to uninstall $app for user $(GetUserName)..." "Uninstall_$($appName).reg" + if (-not ($isEdgeId -and $edgeScheduledTaskAdded)) { + ImportRegistryFile "Adding scheduled task to uninstall $app for user $(GetUserName)..." "Uninstall_$($appName).reg" + if ($isEdgeId) { $edgeScheduledTaskAdded = $true } + } } elseif ($script:Params.ContainsKey("Sysprep")) { - ImportRegistryFile "Adding scheduled task to uninstall $app after for new users..." "Uninstall_$($appName).reg" + if (-not ($isEdgeId -and $edgeScheduledTaskAdded)) { + ImportRegistryFile "Adding scheduled task to uninstall $app after for new users..." "Uninstall_$($appName).reg" + if ($isEdgeId) { $edgeScheduledTaskAdded = $true } + } } else { # Uninstall app via WinGet @@ -47,21 +57,35 @@ function RemoveApps { winget uninstall --accept-source-agreements --disable-interactivity --id $appId } -ArgumentList $app - If (($app -eq "Microsoft.Edge") -and (Select-String -InputObject $wingetOutput -Pattern "Uninstall failed with exit code")) { - Write-Host "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red + $wingetFailed = Select-String -InputObject $wingetOutput -Pattern "Uninstall failed with exit code|No installed package found matching input criteria|No package found matching input criteria" -SimpleMatch:$false + if ($isEdgeId) { + if (-not $wingetFailed) { + $edgeUninstallSucceeded = $true + } - if ($script:GuiWindow) { - $result = Show-MessageBox -Message 'Unable to uninstall Microsoft Edge via WinGet. Would you like to forcefully uninstall it? NOT RECOMMENDED!' -Title 'Force Uninstall Microsoft Edge?' -Button 'YesNo' -Icon 'Warning' + # Prompt immediately after the final selected Edge ID attempt (if all attempts failed) + $hasRemainingEdgeIds = $false + if ($appIndex -lt $appCount) { + $remainingApps = @($appsList)[($appIndex)..($appCount - 1)] + $hasRemainingEdgeIds = @($remainingApps | Where-Object { $edgeIds -contains $_ }).Count -gt 0 + } - if ($result -eq 'Yes') { + if (-not $hasRemainingEdgeIds -and -not $edgeUninstallSucceeded) { + Write-Host "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red + + if ($script:GuiWindow) { + $result = Show-MessageBox -Message 'Unable to uninstall Microsoft Edge via WinGet. Would you like to forcefully uninstall it? NOT RECOMMENDED!' -Title 'Force Uninstall Microsoft Edge?' -Button 'YesNo' -Icon 'Warning' + + if ($result -eq 'Yes') { + Write-Host "" + ForceRemoveEdge + } + } + elseif ($( Read-Host -Prompt "Would you like to forcefully uninstall Microsoft Edge? NOT RECOMMENDED! (y/n)" ) -eq 'y') { Write-Host "" ForceRemoveEdge } } - elseif ($( Read-Host -Prompt "Would you like to forcefully uninstall Microsoft Edge? NOT RECOMMENDED! (y/n)" ) -eq 'y') { - Write-Host "" - ForceRemoveEdge - } } } diff --git a/Scripts/FileIO/LoadAppsDetailsFromJson.ps1 b/Scripts/FileIO/LoadAppsDetailsFromJson.ps1 index 565d64b..e4229b6 100644 --- a/Scripts/FileIO/LoadAppsDetailsFromJson.ps1 +++ b/Scripts/FileIO/LoadAppsDetailsFromJson.ps1 @@ -16,24 +16,36 @@ function LoadAppsDetailsFromJson { } foreach ($appData in $jsonContent.Apps) { - $appId = $appData.AppId.Trim() - if ($appId.length -eq 0) { continue } + # Handle AppId as array (could be single or multiple IDs) + $appIdArray = if ($appData.AppId -is [array]) { $appData.AppId } else { @($appData.AppId) } + $appIdArray = $appIdArray | ForEach-Object { $_.Trim() } | Where-Object { $_.length -gt 0 } + if ($appIdArray.Count -eq 0) { continue } if ($OnlyInstalled) { - if (-not ($InstalledList -like ("*$appId*")) -and -not (Get-AppxPackage -Name $appId)) { - continue - } - if (($appId -eq "Microsoft.Edge") -and -not ($InstalledList -like "* Microsoft.Edge *")) { - continue + $isInstalled = $false + foreach ($appId in $appIdArray) { + if (($InstalledList -like ("*$appId*")) -or (Get-AppxPackage -Name $appId)) { + $isInstalled = $true + break + } + if (($appId -eq "Microsoft.Edge") -and ($InstalledList -like "* Microsoft.Edge *")) { + $isInstalled = $true + break + } } + if (-not $isInstalled) { continue } } - $friendlyName = if ($appData.FriendlyName) { $appData.FriendlyName } else { $appId } - $displayName = if ($appData.FriendlyName) { "$($appData.FriendlyName) ($appId)" } else { $appId } + # Use first AppId for fallback names, join all for display + $primaryAppId = $appIdArray[0] + $appIdDisplay = $appIdArray -join ', ' + $friendlyName = if ($appData.FriendlyName) { $appData.FriendlyName } else { $primaryAppId } + $displayName = if ($appData.FriendlyName) { "$($appData.FriendlyName) ($appIdDisplay)" } else { $appIdDisplay } $isChecked = if ($InitialCheckedFromJson) { $appData.SelectedByDefault } else { $false } $apps += [PSCustomObject]@{ - AppId = $appId + AppId = $appIdArray + AppIdDisplay = $appIdDisplay FriendlyName = $friendlyName DisplayName = $displayName IsChecked = $isChecked diff --git a/Scripts/FileIO/LoadAppsFromFile.ps1 b/Scripts/FileIO/LoadAppsFromFile.ps1 index a5a98f3..d81186a 100644 --- a/Scripts/FileIO/LoadAppsFromFile.ps1 +++ b/Scripts/FileIO/LoadAppsFromFile.ps1 @@ -16,10 +16,12 @@ function LoadAppsFromFile { # JSON file format $jsonContent = Get-Content -Path $appsFilePath -Raw | ConvertFrom-Json Foreach ($appData in $jsonContent.Apps) { - $appId = $appData.AppId.Trim() + # Handle AppId as array (could be single or multiple IDs) + $appIdArray = if ($appData.AppId -is [array]) { $appData.AppId } else { @($appData.AppId) } + $appIdArray = $appIdArray | ForEach-Object { $_.Trim() } | Where-Object { $_.length -gt 0 } $selectedByDefault = $appData.SelectedByDefault - if ($selectedByDefault -and $appId.length -gt 0) { - $appsList += $appId + if ($selectedByDefault -and $appIdArray.Count -gt 0) { + $appsList += $appIdArray } } } diff --git a/Scripts/FileIO/ValidateAppslist.ps1 b/Scripts/FileIO/ValidateAppslist.ps1 index 3985f62..0cb0905 100644 --- a/Scripts/FileIO/ValidateAppslist.ps1 +++ b/Scripts/FileIO/ValidateAppslist.ps1 @@ -4,7 +4,7 @@ function ValidateAppslist { $appsList ) - $supportedAppsList = (LoadAppsDetailsFromJson | ForEach-Object { $_.AppId }) + $supportedAppsList = @(LoadAppsDetailsFromJson | ForEach-Object { @($_.AppId) }) | ForEach-Object { $_.Trim() } | Where-Object { $_.Length -gt 0 } $validatedAppsList = @() # Validate provided appsList against supportedAppsList diff --git a/Scripts/GUI/Show-AppSelectionWindow.ps1 b/Scripts/GUI/Show-AppSelectionWindow.ps1 index 8f6bff2..43d22b0 100644 --- a/Scripts/GUI/Show-AppSelectionWindow.ps1 +++ b/Scripts/GUI/Show-AppSelectionWindow.ps1 @@ -75,7 +75,8 @@ function Show-AppSelectionWindow { $checkbox = New-Object System.Windows.Controls.CheckBox $checkbox.Content = $_.DisplayName $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $_.DisplayName) - $checkbox.Tag = $_.AppId + $checkbox.Tag = $_.AppIdDisplay + Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppIds' -Value @($_.AppId) $checkbox.IsChecked = $_.IsChecked $checkbox.ToolTip = $_.Description $checkbox.Style = $window.Resources["AppsPanelCheckBoxStyle"] @@ -118,9 +119,10 @@ function Show-AppSelectionWindow { $selectedApps = @() foreach ($child in $appsPanel.Children) { if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) { - $selectedApps += $child.Tag + $selectedApps += @($child.AppIds) } } + $selectedApps = @($selectedApps | Where-Object { $_ } | Select-Object -Unique) # Close form without saving if no apps were selected if ($selectedApps.Count -eq 0) { diff --git a/Scripts/GUI/Show-MainWindow.ps1 b/Scripts/GUI/Show-MainWindow.ps1 index d27bd15..141baa5 100644 --- a/Scripts/GUI/Show-MainWindow.ps1 +++ b/Scripts/GUI/Show-MainWindow.ps1 @@ -241,7 +241,7 @@ function Show-MainWindow { $check = ($this.IsChecked -eq $true) if ($this.IsChecked -eq $null) { $this.IsChecked = $false; $check = $false } $presetIds = $this.PresetAppIds - ApplyPresetToApps -MatchFilter { param($c) $presetIds -contains $c.Tag }.GetNewClosure() -Check $check + ApplyPresetToApps -MatchFilter { param($c) (@($c.AppIds) | Where-Object { $presetIds -contains $_ }).Count -gt 0 }.GetNewClosure() -Check $check }) } @@ -317,7 +317,7 @@ function Show-MainWindow { $key = switch ($script:SortColumn) { 'Name' { { $_.AppName } } 'Description' { { $_.AppDescription } } - 'AppId' { { $_.Tag } } + 'AppId' { { $_.AppIdDisplay } } } $sorted = $children | Sort-Object $key -Descending:(-not $script:SortAscending) $appsPanel.Children.Clear() @@ -379,14 +379,6 @@ function Show-MainWindow { function UpdatePresetStates { $script:UpdatingPresets = $true try { - # Build a set of currently checked app tags for fast lookup - $checkedTags = @{} - foreach ($child in $appsPanel.Children) { - if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) { - $checkedTags[$child.Tag] = $true - } - } - # Helper: count matching and checked apps, set checkbox state function SetPresetState($checkbox, [scriptblock]$MatchFilter) { $total = 0; $checked = 0 @@ -394,7 +386,7 @@ function Show-MainWindow { if ($child -is [System.Windows.Controls.CheckBox]) { if (& $MatchFilter $child) { $total++ - if ($checkedTags.ContainsKey($child.Tag)) { $checked++ } + if ($child.IsChecked) { $checked++ } } } } @@ -416,12 +408,12 @@ function Show-MainWindow { SetPresetState $presetDefaultApps { param($c) $c.SelectedByDefault -eq $true } foreach ($jsonCb in $script:JsonPresetCheckboxes) { $localIds = $jsonCb.PresetAppIds - SetPresetState $jsonCb { param($c) $localIds -contains $c.Tag }.GetNewClosure() + SetPresetState $jsonCb { param($c) (@($c.AppIds) | Where-Object { $localIds -contains $_ }).Count -gt 0 }.GetNewClosure() } # Last used preset: only update if it's visible (has saved apps) if ($presetLastUsed.Visibility -ne 'Collapsed' -and $script:SavedAppIds) { - SetPresetState $presetLastUsed { param($c) $script:SavedAppIds -contains $c.Tag } + SetPresetState $presetLastUsed { param($c) (@($c.AppIds) | Where-Object { $script:SavedAppIds -contains $_ }).Count -gt 0 } } } finally { @@ -760,9 +752,9 @@ function Show-MainWindow { $app = $appsToAdd[$i] $checkbox = New-Object System.Windows.Controls.CheckBox - $automationName = if ($app.FriendlyName) { $app.FriendlyName } elseif ($app.AppId) { $app.AppId } else { $null } + $automationName = if ($app.FriendlyName) { $app.FriendlyName } elseif ($app.AppIdDisplay) { $app.AppIdDisplay } else { $null } if ($automationName) { $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $automationName) } - $checkbox.Tag = $app.AppId + $checkbox.Tag = $app.AppIdDisplay $checkbox.IsChecked = $app.IsChecked $checkbox.Style = $window.Resources['AppsPanelCheckBoxStyle'] @@ -798,9 +790,9 @@ function Show-MainWindow { [System.Windows.Controls.Grid]::SetColumn($tbDesc, 2) $tbId = New-Object System.Windows.Controls.TextBlock - $tbId.Text = $app.AppId - $tbId.Style = $window.Resources['AppIdTextStyle'] - $tbId.ToolTip = $app.AppId + $tbId.Text = $app.AppIdDisplay + $tbId.Style = $window.Resources["AppIdTextStyle"] + $tbId.ToolTip = $app.AppIdDisplay [System.Windows.Controls.Grid]::SetColumn($tbId, 3) $row.Children.Add($dot) | Out-Null @@ -812,6 +804,8 @@ function Show-MainWindow { Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppName' -Value $app.FriendlyName Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppDescription' -Value $app.Description Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'SelectedByDefault' -Value $app.SelectedByDefault + Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppIds' -Value @($app.AppId) + Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppIdDisplay' -Value $app.AppIdDisplay $checkbox.Add_Checked({ UpdateAppSelectionStatus }) $checkbox.Add_Unchecked({ UpdateAppSelectionStatus }) @@ -1537,9 +1531,10 @@ function Show-MainWindow { $selectedApps = @() foreach ($child in $appsPanel.Children) { if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) { - $selectedApps += $child.Tag + $selectedApps += @($child.AppIds) } } + $selectedApps = @($selectedApps | Where-Object { $_ } | Select-Object -Unique) if ($selectedApps.Count -gt 0) { # Check if Microsoft Store is selected @@ -1760,7 +1755,7 @@ function Show-MainWindow { if ($script:UpdatingPresets) { return } $check = ($this.IsChecked -eq $true) if ($this.IsChecked -eq $null) { $this.IsChecked = $false; $check = $false } - ApplyPresetToApps -MatchFilter { param($c) $script:SavedAppIds -contains $c.Tag } -Check $check + ApplyPresetToApps -MatchFilter { param($c) (@($c.AppIds) | Where-Object { $script:SavedAppIds -contains $_ }).Count -gt 0 } -Check $check }) } else { From 774c8ecd92f560a170e23303abbde4b547e955c8 Mon Sep 17 00:00:00 2001 From: Jeffrey <9938813+Raphire@users.noreply.github.com> Date: Fri, 27 Mar 2026 20:33:24 +0100 Subject: [PATCH 2/2] Add ability to export/import settings configuration (#522) --- Schemas/AboutWindow.xaml | 2 +- Schemas/ApplyChangesWindow.xaml | 2 +- Schemas/ImportExportConfigWindow.xaml | 77 +++++ Schemas/MainWindow.xaml | 23 ++ Schemas/MessageBoxWindow.xaml | 2 +- Scripts/FileIO/SaveSettings.ps1 | 5 +- Scripts/FileIO/SaveToFile.ps1 | 19 ++ Scripts/GUI/Show-AboutDialog.ps1 | 19 +- Scripts/GUI/Show-ApplyModal.ps1 | 17 +- Scripts/GUI/Show-ConfigWindow.ps1 | 389 +++++++++++++++++++++++ Scripts/GUI/Show-MainWindow.ps1 | 33 ++ Scripts/GUI/Show-MessageBox.ps1 | 17 +- Scripts/Get.ps1 | 1 + Scripts/Helpers/ImportConfigToParams.ps1 | 127 ++++++++ Win11Debloat.ps1 | 24 +- 15 files changed, 726 insertions(+), 31 deletions(-) create mode 100644 Schemas/ImportExportConfigWindow.xaml create mode 100644 Scripts/FileIO/SaveToFile.ps1 create mode 100644 Scripts/GUI/Show-ConfigWindow.ps1 create mode 100644 Scripts/Helpers/ImportConfigToParams.ps1 diff --git a/Schemas/AboutWindow.xaml b/Schemas/AboutWindow.xaml index bcbe2a5..cdca135 100644 --- a/Schemas/AboutWindow.xaml +++ b/Schemas/AboutWindow.xaml @@ -8,7 +8,7 @@ WindowStyle="None" AllowsTransparency="True" Background="Transparent" - Topmost="True" + Topmost="False" ShowInTaskbar="False"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + +