mirror of
https://github.com/Raphire/Win11Debloat.git
synced 2026-04-03 14:06:27 +00:00
Improve app page with sorting, recommendations and more (#520)
This commit is contained in:
31
Scripts/AppRemoval/GetInstalledAppsViaWinget.ps1
Normal file
31
Scripts/AppRemoval/GetInstalledAppsViaWinget.ps1
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
# Run winget list and return installed apps.
|
||||
# Use -NonBlocking to keep the UI responsive (GUI mode) via Invoke-NonBlocking.
|
||||
function GetInstalledAppsViaWinget {
|
||||
param (
|
||||
[int]$TimeOut = 10,
|
||||
[switch]$NonBlocking
|
||||
)
|
||||
|
||||
if (-not $script:WingetInstalled) { return $null }
|
||||
|
||||
$fetchBlock = {
|
||||
param($timeOut)
|
||||
$job = Start-Job { return winget list --accept-source-agreements --disable-interactivity }
|
||||
$done = $job | Wait-Job -Timeout $timeOut
|
||||
if ($done) {
|
||||
$result = Receive-Job -Job $job
|
||||
Remove-Job -Job $job -ErrorAction SilentlyContinue
|
||||
return $result
|
||||
}
|
||||
Remove-Job -Job $job -Force -ErrorAction SilentlyContinue
|
||||
return $null
|
||||
}
|
||||
|
||||
if ($NonBlocking) {
|
||||
return Invoke-NonBlocking -ScriptBlock $fetchBlock -ArgumentList $TimeOut
|
||||
}
|
||||
else {
|
||||
return & $fetchBlock $TimeOut
|
||||
}
|
||||
}
|
||||
@@ -5,31 +5,25 @@ function CreateSystemRestorePoint {
|
||||
if ($SysRestore.RPSessionInterval -eq 0) {
|
||||
# In GUI mode, skip the prompt and just try to enable it
|
||||
if ($script:GuiWindow -or $Silent -or $( Read-Host -Prompt "System restore is disabled, would you like to enable it and create a restore point? (y/n)") -eq 'y') {
|
||||
$enableSystemRestoreJob = Start-Job {
|
||||
try {
|
||||
Enable-ComputerRestore -Drive "$env:SystemDrive"
|
||||
try {
|
||||
$enableResult = Invoke-NonBlocking -TimeoutSeconds 20 -ScriptBlock {
|
||||
try {
|
||||
Enable-ComputerRestore -Drive "$env:SystemDrive"
|
||||
return $null
|
||||
}
|
||||
catch {
|
||||
return "Error: Failed to enable System Restore: $_"
|
||||
}
|
||||
}
|
||||
catch {
|
||||
return "Error: Failed to enable System Restore: $_"
|
||||
}
|
||||
return $null
|
||||
}
|
||||
catch {
|
||||
$enableResult = "Error: Failed to enable System Restore: $_"
|
||||
}
|
||||
|
||||
$enableSystemRestoreJobDone = $enableSystemRestoreJob | Wait-Job -TimeOut 20
|
||||
|
||||
if (-not $enableSystemRestoreJobDone) {
|
||||
Remove-Job -Job $enableSystemRestoreJob -Force -ErrorAction SilentlyContinue
|
||||
Write-Host "Error: Failed to enable system restore and create restore point, operation timed out" -ForegroundColor Red
|
||||
if ($enableResult) {
|
||||
Write-Host $enableResult -ForegroundColor Red
|
||||
$failed = $true
|
||||
}
|
||||
else {
|
||||
$result = Receive-Job $enableSystemRestoreJob
|
||||
Remove-Job -Job $enableSystemRestoreJob -ErrorAction SilentlyContinue
|
||||
if ($result) {
|
||||
Write-Host $result -ForegroundColor Red
|
||||
$failed = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host ""
|
||||
@@ -38,46 +32,43 @@ function CreateSystemRestorePoint {
|
||||
}
|
||||
|
||||
if (-not $failed) {
|
||||
$createRestorePointJob = Start-Job {
|
||||
# Find existing restore points that are less than 24 hours old
|
||||
try {
|
||||
$recentRestorePoints = Get-ComputerRestorePoint | Where-Object { (Get-Date) - [System.Management.ManagementDateTimeConverter]::ToDateTime($_.CreationTime) -le (New-TimeSpan -Hours 24) }
|
||||
}
|
||||
catch {
|
||||
return @{ Success = $false; Message = "Error: Unable to retrieve existing restore points: $_" }
|
||||
}
|
||||
|
||||
if ($recentRestorePoints.Count -eq 0) {
|
||||
try {
|
||||
$result = Invoke-NonBlocking -TimeoutSeconds 20 -ScriptBlock {
|
||||
try {
|
||||
Checkpoint-Computer -Description "Restore point created by Win11Debloat" -RestorePointType "MODIFY_SETTINGS"
|
||||
return @{ Success = $true; Message = "System restore point created successfully" }
|
||||
$recentRestorePoints = Get-ComputerRestorePoint | Where-Object { (Get-Date) - [System.Management.ManagementDateTimeConverter]::ToDateTime($_.CreationTime) -le (New-TimeSpan -Hours 24) }
|
||||
}
|
||||
catch {
|
||||
return @{ Success = $false; Message = "Error: Unable to create restore point: $_" }
|
||||
return [PSCustomObject]@{ Success = $false; Message = "Error: Unable to retrieve existing restore points: $_" }
|
||||
}
|
||||
|
||||
if ($recentRestorePoints.Count -eq 0) {
|
||||
try {
|
||||
Checkpoint-Computer -Description "Restore point created by Win11Debloat" -RestorePointType "MODIFY_SETTINGS"
|
||||
return [PSCustomObject]@{ Success = $true; Message = "System restore point created successfully" }
|
||||
}
|
||||
catch {
|
||||
return [PSCustomObject]@{ Success = $false; Message = "Error: Unable to create restore point: $_" }
|
||||
}
|
||||
}
|
||||
else {
|
||||
return [PSCustomObject]@{ Success = $true; Message = "A recent restore point already exists, no new restore point was created" }
|
||||
}
|
||||
}
|
||||
else {
|
||||
return @{ Success = $true; Message = "A recent restore point already exists, no new restore point was created" }
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$result = [PSCustomObject]@{ Success = $false; Message = "Error: Failed to create system restore point: $_" }
|
||||
}
|
||||
|
||||
$createRestorePointJobDone = $createRestorePointJob | Wait-Job -TimeOut 20
|
||||
|
||||
if (-not $createRestorePointJobDone) {
|
||||
Remove-Job -Job $createRestorePointJob -Force -ErrorAction SilentlyContinue
|
||||
Write-Host "Error: Failed to create system restore point, operation timed out" -ForegroundColor Red
|
||||
if ($result -and $result.Success) {
|
||||
Write-Host $result.Message
|
||||
}
|
||||
elseif ($result) {
|
||||
Write-Host $result.Message -ForegroundColor Red
|
||||
$failed = $true
|
||||
}
|
||||
else {
|
||||
$result = Receive-Job $createRestorePointJob
|
||||
Remove-Job -Job $createRestorePointJob -ErrorAction SilentlyContinue
|
||||
if ($result.Success) {
|
||||
Write-Host $result.Message
|
||||
}
|
||||
else {
|
||||
Write-Host $result.Message -ForegroundColor Red
|
||||
$failed = $true
|
||||
}
|
||||
Write-Host "Error: Failed to create system restore point" -ForegroundColor Red
|
||||
$failed = $true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
196
Scripts/Features/ExecuteChanges.ps1
Normal file
196
Scripts/Features/ExecuteChanges.ps1
Normal file
@@ -0,0 +1,196 @@
|
||||
# Executes a single parameter/feature based on its key
|
||||
# Parameters:
|
||||
# $paramKey - The parameter name to execute
|
||||
function ExecuteParameter {
|
||||
param (
|
||||
[string]$paramKey
|
||||
)
|
||||
|
||||
# Check if this feature has metadata in Features.json
|
||||
$feature = $null
|
||||
if ($script:Features.ContainsKey($paramKey)) {
|
||||
$feature = $script:Features[$paramKey]
|
||||
}
|
||||
|
||||
# If feature has RegistryKey and ApplyText, use dynamic ImportRegistryFile
|
||||
if ($feature -and $feature.RegistryKey -and $feature.ApplyText) {
|
||||
ImportRegistryFile "> $($feature.ApplyText)" $feature.RegistryKey
|
||||
|
||||
# Handle special cases that have additional logic after ImportRegistryFile
|
||||
switch ($paramKey) {
|
||||
'DisableBing' {
|
||||
# Also remove the app package for Bing search
|
||||
RemoveApps 'Microsoft.BingSearch'
|
||||
}
|
||||
'DisableCopilot' {
|
||||
# Also remove the app package for Copilot
|
||||
RemoveApps 'Microsoft.Copilot'
|
||||
}
|
||||
'DisableWidgets' {
|
||||
# Also remove the app package for Widgets
|
||||
RemoveApps 'Microsoft.StartExperiencesApp'
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
# Handle features without RegistryKey or with special logic
|
||||
switch ($paramKey) {
|
||||
'RemoveApps' {
|
||||
Write-Host "> Removing selected apps for $(GetFriendlyTargetUserName)..."
|
||||
$appsList = GenerateAppsList
|
||||
|
||||
if ($appsList.Count -eq 0) {
|
||||
Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "$($appsList.Count) apps selected for removal"
|
||||
RemoveApps $appsList
|
||||
}
|
||||
'RemoveAppsCustom' {
|
||||
Write-Host "> Removing selected apps..."
|
||||
$appsList = LoadAppsFromFile $script:CustomAppsListFilePath
|
||||
|
||||
if ($appsList.Count -eq 0) {
|
||||
Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "$($appsList.Count) apps selected for removal"
|
||||
RemoveApps $appsList
|
||||
}
|
||||
'RemoveCommApps' {
|
||||
$appsList = 'Microsoft.windowscommunicationsapps', 'Microsoft.People'
|
||||
Write-Host "> Removing Mail, Calendar and People apps..."
|
||||
RemoveApps $appsList
|
||||
return
|
||||
}
|
||||
'RemoveW11Outlook' {
|
||||
$appsList = 'Microsoft.OutlookForWindows'
|
||||
Write-Host "> Removing new Outlook for Windows app..."
|
||||
RemoveApps $appsList
|
||||
return
|
||||
}
|
||||
'RemoveGamingApps' {
|
||||
$appsList = 'Microsoft.GamingApp', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay'
|
||||
Write-Host "> Removing gaming related apps..."
|
||||
RemoveApps $appsList
|
||||
return
|
||||
}
|
||||
'RemoveHPApps' {
|
||||
$appsList = 'AD2F1837.HPAIExperienceCenter', 'AD2F1837.HPJumpStarts', 'AD2F1837.HPPCHardwareDiagnosticsWindows', 'AD2F1837.HPPowerManager', 'AD2F1837.HPPrivacySettings', 'AD2F1837.HPSupportAssistant', 'AD2F1837.HPSureShieldAI', 'AD2F1837.HPSystemInformation', 'AD2F1837.HPQuickDrop', 'AD2F1837.HPWorkWell', 'AD2F1837.myHP', 'AD2F1837.HPDesktopSupportUtilities', 'AD2F1837.HPQuickTouch', 'AD2F1837.HPEasyClean', 'AD2F1837.HPConnectedMusic', 'AD2F1837.HPFileViewer', 'AD2F1837.HPRegistration', 'AD2F1837.HPWelcome', 'AD2F1837.HPConnectedPhotopoweredbySnapfish', 'AD2F1837.HPPrinterControl'
|
||||
Write-Host "> Removing HP apps..."
|
||||
RemoveApps $appsList
|
||||
return
|
||||
}
|
||||
"EnableWindowsSandbox" {
|
||||
Write-Host "> Enabling Windows Sandbox..."
|
||||
EnableWindowsFeature "Containers-DisposableClientVM"
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
"EnableWindowsSubsystemForLinux" {
|
||||
Write-Host "> Enabling Windows Subsystem for Linux..."
|
||||
EnableWindowsFeature "VirtualMachinePlatform"
|
||||
EnableWindowsFeature "Microsoft-Windows-Subsystem-Linux"
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
'ClearStart' {
|
||||
Write-Host "> Removing all pinned apps from the start menu for user $(GetUserName)..."
|
||||
ReplaceStartMenu
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
'ReplaceStart' {
|
||||
Write-Host "> Replacing the start menu for user $(GetUserName)..."
|
||||
ReplaceStartMenu $script:Params.Item("ReplaceStart")
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
'ClearStartAllUsers' {
|
||||
ReplaceStartMenuForAllUsers
|
||||
return
|
||||
}
|
||||
'ReplaceStartAllUsers' {
|
||||
ReplaceStartMenuForAllUsers $script:Params.Item("ReplaceStartAllUsers")
|
||||
return
|
||||
}
|
||||
'DisableStoreSearchSuggestions' {
|
||||
if ($script:Params.ContainsKey("Sysprep")) {
|
||||
Write-Host "> Disabling Microsoft Store search suggestions in the start menu for all users..."
|
||||
DisableStoreSearchSuggestionsForAllUsers
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "> Disabling Microsoft Store search suggestions for user $(GetUserName)..."
|
||||
DisableStoreSearchSuggestions
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Executes all selected parameters/features
|
||||
function ExecuteAllChanges {
|
||||
# Build list of actionable parameters (skip control params and data-only params)
|
||||
$actionableKeys = @()
|
||||
foreach ($paramKey in $script:Params.Keys) {
|
||||
if ($script:ControlParams -contains $paramKey) { continue }
|
||||
if ($paramKey -eq 'Apps') { continue }
|
||||
if ($paramKey -eq 'CreateRestorePoint') { continue }
|
||||
$actionableKeys += $paramKey
|
||||
}
|
||||
|
||||
$totalSteps = $actionableKeys.Count
|
||||
if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ }
|
||||
$currentStep = 0
|
||||
|
||||
# Create restore point if requested (CLI only - GUI handles this separately)
|
||||
if ($script:Params.ContainsKey("CreateRestorePoint")) {
|
||||
$currentStep++
|
||||
if ($script:ApplyProgressCallback) {
|
||||
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating system restore point"
|
||||
}
|
||||
Write-Host "> Attempting to create a system restore point..."
|
||||
CreateSystemRestorePoint
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Execute all parameters
|
||||
foreach ($paramKey in $actionableKeys) {
|
||||
if ($script:CancelRequested) {
|
||||
return
|
||||
}
|
||||
|
||||
$currentStep++
|
||||
|
||||
# Get friendly name for the step
|
||||
$stepName = $paramKey
|
||||
if ($script:Features.ContainsKey($paramKey)) {
|
||||
$feature = $script:Features[$paramKey]
|
||||
if ($feature.ApplyText) {
|
||||
# Prefer explicit ApplyText when provided
|
||||
$stepName = $feature.ApplyText
|
||||
} elseif ($feature.Label) {
|
||||
# Fallback: construct a name from Action and Label, or just Label
|
||||
if ($feature.Action) {
|
||||
$stepName = "$($feature.Action) $($feature.Label)"
|
||||
} else {
|
||||
$stepName = $feature.Label
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($script:ApplyProgressCallback) {
|
||||
& $script:ApplyProgressCallback $currentStep $totalSteps $stepName
|
||||
}
|
||||
|
||||
ExecuteParameter -paramKey $paramKey
|
||||
}
|
||||
}
|
||||
22
Scripts/FileIO/LoadAppPresetsFromJson.ps1
Normal file
22
Scripts/FileIO/LoadAppPresetsFromJson.ps1
Normal file
@@ -0,0 +1,22 @@
|
||||
# Read Apps.json and return the list of preset objects (Name + AppIds).
|
||||
# Returns an empty array if the file cannot be read or contains no presets.
|
||||
function LoadAppPresetsFromJson {
|
||||
try {
|
||||
$jsonContent = Get-Content -Path $script:AppsListFilePath -Raw | ConvertFrom-Json
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to read Apps.json: $_"
|
||||
return @()
|
||||
}
|
||||
|
||||
if (-not $jsonContent.Presets) {
|
||||
return @()
|
||||
}
|
||||
|
||||
return @($jsonContent.Presets | ForEach-Object {
|
||||
[PSCustomObject]@{
|
||||
Name = $_.Name
|
||||
AppIds = @($_.AppIds)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -39,6 +39,7 @@ function LoadAppsDetailsFromJson {
|
||||
IsChecked = $isChecked
|
||||
Description = $appData.Description
|
||||
SelectedByDefault = $appData.SelectedByDefault
|
||||
Recommendation = $appData.Recommendation
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,60 +11,64 @@ function ApplySettingsToUiControls {
|
||||
return $false
|
||||
}
|
||||
|
||||
# First, reset all tweaks to "No Change" (index 0) or unchecked
|
||||
if ($uiControlMappings) {
|
||||
foreach ($comboName in $uiControlMappings.Keys) {
|
||||
$control = $window.FindName($comboName)
|
||||
if ($control -is [System.Windows.Controls.CheckBox]) {
|
||||
$control.IsChecked = $false
|
||||
}
|
||||
elseif ($control -is [System.Windows.Controls.ComboBox]) {
|
||||
$control.SelectedIndex = 0
|
||||
if (-not $uiControlMappings) {
|
||||
return $true
|
||||
}
|
||||
|
||||
# Build control cache and reverse index (featureId -> control info) in a single pass
|
||||
$controlCache = @{}
|
||||
$featureIdIndex = @{}
|
||||
|
||||
foreach ($comboName in $uiControlMappings.Keys) {
|
||||
$control = $window.FindName($comboName)
|
||||
if (-not $control) { continue }
|
||||
$controlCache[$comboName] = $control
|
||||
|
||||
$mapping = $uiControlMappings[$comboName]
|
||||
if ($mapping.Type -eq 'group') {
|
||||
$i = 1
|
||||
foreach ($val in $mapping.Values) {
|
||||
foreach ($fid in $val.FeatureIds) {
|
||||
$featureIdIndex[$fid] = @{ ComboName = $comboName; Control = $control; Index = $i; MappingType = 'group' }
|
||||
}
|
||||
$i++
|
||||
}
|
||||
}
|
||||
elseif ($mapping.Type -eq 'feature') {
|
||||
$featureIdIndex[$mapping.FeatureId] = @{ ComboName = $comboName; Control = $control; MappingType = 'feature' }
|
||||
}
|
||||
|
||||
# Reset control to default state
|
||||
if ($control -is [System.Windows.Controls.CheckBox]) {
|
||||
$control.IsChecked = $false
|
||||
}
|
||||
elseif ($control -is [System.Windows.Controls.ComboBox]) {
|
||||
$control.SelectedIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
# Apply settings from JSON
|
||||
# Apply settings using O(1) lookups
|
||||
foreach ($setting in $settingsJson.Settings) {
|
||||
if ($setting.Value -ne $true) { continue }
|
||||
$paramName = $setting.Name
|
||||
if ($setting.Name -eq 'CreateRestorePoint') { continue }
|
||||
|
||||
# Skip RestorePointCheckBox, this is always checked by default
|
||||
if ($paramName -eq 'CreateRestorePoint') {
|
||||
continue
|
||||
$entry = $featureIdIndex[$setting.Name]
|
||||
if (-not $entry) { continue }
|
||||
|
||||
$control = $entry.Control
|
||||
if (-not $control -or $control.Visibility -ne 'Visible') { continue }
|
||||
|
||||
if ($entry.MappingType -eq 'group') {
|
||||
if ($control -is [System.Windows.Controls.ComboBox]) {
|
||||
$control.SelectedIndex = $entry.Index
|
||||
}
|
||||
}
|
||||
|
||||
if ($uiControlMappings) {
|
||||
foreach ($comboName in $uiControlMappings.Keys) {
|
||||
$mapping = $uiControlMappings[$comboName]
|
||||
if ($mapping.Type -eq 'group') {
|
||||
$i = 1
|
||||
foreach ($val in $mapping.Values) {
|
||||
if ($val.FeatureIds -contains $paramName) {
|
||||
$control = $window.FindName($comboName)
|
||||
if ($control -and $control.Visibility -eq 'Visible') {
|
||||
if ($control -is [System.Windows.Controls.ComboBox]) {
|
||||
$control.SelectedIndex = $i
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
$i++
|
||||
}
|
||||
}
|
||||
elseif ($mapping.Type -eq 'feature') {
|
||||
if ($mapping.FeatureId -eq $paramName) {
|
||||
$control = $window.FindName($comboName)
|
||||
if ($control -and $control.Visibility -eq 'Visible') {
|
||||
if ($control -is [System.Windows.Controls.CheckBox]) {
|
||||
$control.IsChecked = $true
|
||||
}
|
||||
elseif ($control -is [System.Windows.Controls.ComboBox]) {
|
||||
$control.SelectedIndex = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if ($control -is [System.Windows.Controls.CheckBox]) {
|
||||
$control.IsChecked = $true
|
||||
}
|
||||
elseif ($control -is [System.Windows.Controls.ComboBox]) {
|
||||
$control.SelectedIndex = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ function SetWindowThemeResources {
|
||||
$window.Resources.Add("InputFocusColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#1f1f1f")))
|
||||
$window.Resources.Add("ScrollBarThumbColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#3d3d3d")))
|
||||
$window.Resources.Add("ScrollBarThumbHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#4b4b4b")))
|
||||
$window.Resources.Add("TitlebarButtonHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#2d2d2d")))
|
||||
$window.Resources.Add("TitlebarButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#292929")))
|
||||
$window.Resources.Add("AppIdColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#afafaf")))
|
||||
$window.Resources.Add("SearchHighlightColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#4A4A2A")))
|
||||
$window.Resources.Add("SearchHighlightActiveColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#8A7000")))
|
||||
@@ -60,6 +62,8 @@ function SetWindowThemeResources {
|
||||
$window.Resources.Add("InputFocusColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#fbfbfb")))
|
||||
$window.Resources.Add("ScrollBarThumbColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#b9b9b9")))
|
||||
$window.Resources.Add("ScrollBarThumbHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#8b8b8b")))
|
||||
$window.Resources.Add("TitlebarButtonHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e1e1e1")))
|
||||
$window.Resources.Add("TitlebarButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e6e6e6")))
|
||||
$window.Resources.Add("AppIdColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#666666")))
|
||||
$window.Resources.Add("SearchHighlightColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFF4CE")))
|
||||
$window.Resources.Add("SearchHighlightActiveColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFD966")))
|
||||
|
||||
@@ -143,7 +143,7 @@ function Show-AppSelectionWindow {
|
||||
|
||||
# Load apps after window is shown (allows UI to render first)
|
||||
$window.Add_ContentRendered({
|
||||
$window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ LoadApps })
|
||||
$window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ LoadApps }) | Out-Null
|
||||
})
|
||||
|
||||
# Show the window and return dialog result
|
||||
|
||||
@@ -216,7 +216,7 @@ function Show-ApplyModal {
|
||||
$script:ApplyProgressCallback = $null
|
||||
$script:ApplySubStepCallback = $null
|
||||
}
|
||||
})
|
||||
}) | Out-Null
|
||||
|
||||
# Button handlers
|
||||
$applyCloseBtn.Add_Click({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function Show-MainWindow {
|
||||
function Show-MainWindow {
|
||||
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase,System.Windows.Forms | Out-Null
|
||||
|
||||
# Get current Windows build version
|
||||
@@ -210,21 +210,113 @@ function Show-MainWindow {
|
||||
$onlyInstalledAppsBox = $window.FindName('OnlyInstalledAppsBox')
|
||||
$loadingAppsIndicator = $window.FindName('LoadingAppsIndicator')
|
||||
$appSelectionStatus = $window.FindName('AppSelectionStatus')
|
||||
$defaultAppsBtn = $window.FindName('DefaultAppsBtn')
|
||||
$headerNameBtn = $window.FindName('HeaderNameBtn')
|
||||
$headerDescriptionBtn = $window.FindName('HeaderDescriptionBtn')
|
||||
$headerAppIdBtn = $window.FindName('HeaderAppIdBtn')
|
||||
$sortArrowName = $window.FindName('SortArrowName')
|
||||
$sortArrowDescription = $window.FindName('SortArrowDescription')
|
||||
$sortArrowAppId = $window.FindName('SortArrowAppId')
|
||||
$presetsBtn = $window.FindName('PresetsBtn')
|
||||
$presetsPopup = $window.FindName('PresetsPopup')
|
||||
$presetDefaultApps = $window.FindName('PresetDefaultApps')
|
||||
$presetLastUsed = $window.FindName('PresetLastUsed')
|
||||
$jsonPresetsPanel = $window.FindName('JsonPresetsPanel')
|
||||
$presetsArrow = $window.FindName('PresetsArrow')
|
||||
$clearAppSelectionBtn = $window.FindName('ClearAppSelectionBtn')
|
||||
|
||||
# Load JSON-defined presets and build dynamic preset checkboxes
|
||||
$script:JsonPresetCheckboxes = @()
|
||||
foreach ($preset in (LoadAppPresetsFromJson)) {
|
||||
$checkbox = New-Object System.Windows.Controls.CheckBox
|
||||
$checkbox.Content = $preset.Name
|
||||
$checkbox.IsThreeState = $true
|
||||
$checkbox.Style = $window.Resources['PresetCheckBoxStyle']
|
||||
$checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $preset.Name)
|
||||
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'PresetAppIds' -Value $preset.AppIds
|
||||
$jsonPresetsPanel.Children.Add($checkbox) | Out-Null
|
||||
$script:JsonPresetCheckboxes += $checkbox
|
||||
|
||||
$checkbox.Add_Click({
|
||||
if ($script:UpdatingPresets) { return }
|
||||
$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
|
||||
})
|
||||
}
|
||||
|
||||
# Track the last selected checkbox for shift-click range selection
|
||||
$script:MainWindowLastSelectedCheckbox = $null
|
||||
|
||||
# Track current app loading operation to prevent race conditions
|
||||
$script:CurrentAppLoadTimer = $null
|
||||
$script:CurrentAppLoadJob = $null
|
||||
$script:CurrentAppLoadJobStartTime = $null
|
||||
# Guard flag: true while a load is in progress; prevents concurrent loads
|
||||
$script:IsLoadingApps = $false
|
||||
# Flag set when Default Mode is clicked before apps have finished loading
|
||||
$script:PendingDefaultMode = $false
|
||||
# Holds apps data preloaded before ShowDialog() so the first load skips the background job
|
||||
$script:PreloadedAppData = $null
|
||||
|
||||
# Set script-level variable for GUI window reference
|
||||
$script:GuiWindow = $window
|
||||
|
||||
# Updates app selection status text in the App Selection tab
|
||||
# Guard flag to prevent preset handlers from firing when we update their state programmatically
|
||||
$script:UpdatingPresets = $false
|
||||
|
||||
# Sort state for the app table
|
||||
$script:SortColumn = 'Name'
|
||||
$script:SortAscending = $true
|
||||
|
||||
function UpdateSortArrows {
|
||||
$ease = New-Object System.Windows.Media.Animation.CubicEase
|
||||
$ease.EasingMode = 'EaseOut'
|
||||
$arrows = @{
|
||||
'Name' = $sortArrowName
|
||||
'Description' = $sortArrowDescription
|
||||
'AppId' = $sortArrowAppId
|
||||
}
|
||||
foreach ($col in $arrows.Keys) {
|
||||
$tb = $arrows[$col]
|
||||
# Active column: full opacity, rotate to indicate direction (0 = up/asc, 180 = down/desc)
|
||||
# Inactive columns: dim, reset to 0
|
||||
if ($col -eq $script:SortColumn) {
|
||||
$targetAngle = if ($script:SortAscending) { 0 } else { 180 }
|
||||
$tb.Opacity = 1.0
|
||||
} else {
|
||||
$targetAngle = 0
|
||||
$tb.Opacity = 0.3
|
||||
}
|
||||
$anim = New-Object System.Windows.Media.Animation.DoubleAnimation
|
||||
$anim.To = $targetAngle
|
||||
$anim.Duration = [System.Windows.Duration]::new([System.TimeSpan]::FromMilliseconds(200))
|
||||
$anim.EasingFunction = $ease
|
||||
$tb.RenderTransform.BeginAnimation([System.Windows.Media.RotateTransform]::AngleProperty, $anim)
|
||||
}
|
||||
}
|
||||
|
||||
function SortApps {
|
||||
$children = @($appsPanel.Children)
|
||||
$key = switch ($script:SortColumn) {
|
||||
'Name' { { $_.AppName } }
|
||||
'Description' { { $_.AppDescription } }
|
||||
'AppId' { { $_.Tag } }
|
||||
}
|
||||
$sorted = $children | Sort-Object $key -Descending:(-not $script:SortAscending)
|
||||
$appsPanel.Children.Clear()
|
||||
foreach ($checkbox in $sorted) {
|
||||
$appsPanel.Children.Add($checkbox) | Out-Null
|
||||
}
|
||||
UpdateSortArrows
|
||||
}
|
||||
|
||||
function SetSortColumn($column) {
|
||||
if ($script:SortColumn -eq $column) {
|
||||
$script:SortAscending = -not $script:SortAscending
|
||||
} else {
|
||||
$script:SortColumn = $column
|
||||
$script:SortAscending = $true
|
||||
}
|
||||
SortApps
|
||||
}
|
||||
|
||||
function UpdateAppSelectionStatus {
|
||||
$selectedCount = 0
|
||||
foreach ($child in $appsPanel.Children) {
|
||||
@@ -235,6 +327,80 @@ function Show-MainWindow {
|
||||
$appSelectionStatus.Text = "$selectedCount app(s) selected for removal"
|
||||
}
|
||||
|
||||
# Applies a preset by checking/unchecking apps that match the given filter
|
||||
# When -Exclusive is set, all apps are unchecked first so only matching apps end up selected
|
||||
function ApplyPresetToApps {
|
||||
param (
|
||||
[scriptblock]$MatchFilter,
|
||||
[bool]$Check,
|
||||
[switch]$Exclusive
|
||||
)
|
||||
foreach ($child in $appsPanel.Children) {
|
||||
if ($child -is [System.Windows.Controls.CheckBox]) {
|
||||
if ($Exclusive) {
|
||||
$child.IsChecked = (& $MatchFilter $child)
|
||||
} elseif (& $MatchFilter $child) {
|
||||
$child.IsChecked = $Check
|
||||
}
|
||||
}
|
||||
}
|
||||
UpdatePresetStates
|
||||
}
|
||||
|
||||
# Update preset checkboxes to reflect checked/indeterminate/unchecked state
|
||||
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
|
||||
foreach ($child in $appsPanel.Children) {
|
||||
if ($child -is [System.Windows.Controls.CheckBox]) {
|
||||
if (& $MatchFilter $child) {
|
||||
$total++
|
||||
if ($checkedTags.ContainsKey($child.Tag)) { $checked++ }
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($total -eq 0) {
|
||||
$checkbox.IsChecked = $false
|
||||
$checkbox.IsEnabled = $false
|
||||
} else {
|
||||
$checkbox.IsEnabled = $true
|
||||
if ($checked -eq 0) {
|
||||
$checkbox.IsChecked = $false
|
||||
} elseif ($checked -eq $total) {
|
||||
$checkbox.IsChecked = $true
|
||||
} else {
|
||||
$checkbox.IsChecked = [System.Nullable[bool]]$null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
# 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 }
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$script:UpdatingPresets = $false
|
||||
}
|
||||
}
|
||||
|
||||
# Dynamically builds Tweaks UI from Features.json
|
||||
function BuildDynamicTweaks {
|
||||
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0"
|
||||
@@ -309,7 +475,7 @@ function Show-MainWindow {
|
||||
$combo = New-Object System.Windows.Controls.ComboBox
|
||||
$combo.Name = $comboName
|
||||
$combo.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $labelText)
|
||||
foreach ($it in $items) { $cbItem = New-Object System.Windows.Controls.ComboBoxItem; $cbItem.Content = $it; $combo.Items.Add($cbItem) | Out-Null }
|
||||
foreach ($item in $items) { $comboItem = New-Object System.Windows.Controls.ComboBoxItem; $comboItem.Content = $item; $combo.Items.Add($comboItem) | Out-Null }
|
||||
$combo.SelectedIndex = 0
|
||||
$parent.Children.Add($combo) | Out-Null
|
||||
|
||||
@@ -512,189 +678,243 @@ function Show-MainWindow {
|
||||
try { $lblBorderObj = $window.FindName("$comboName`_LabelBorder") } catch {}
|
||||
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
|
||||
}
|
||||
$script:UiControlMappings[$comboName] = @{ Type='feature'; FeatureId = $feature.FeatureId; Action = $feature.Action }
|
||||
$script:UiControlMappings[$comboName] = @{ Type='feature'; FeatureId = $feature.FeatureId; Action = $feature.Action; Label = $feature.Label }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Build a feature-label lookup so GenerateOverview can resolve feature IDs without reloading JSON
|
||||
$script:FeatureLabelLookup = @{}
|
||||
foreach ($f in $featuresJson.Features) {
|
||||
$script:FeatureLabelLookup[$f.FeatureId] = $f.Action + ' ' + $f.Label
|
||||
}
|
||||
}
|
||||
|
||||
# Helper function to complete app loading with the WinGet list
|
||||
# Helper function to load apps and populate the app list panel
|
||||
function script:LoadAppsWithList($listOfApps) {
|
||||
$appsToAdd = LoadAppsDetailsFromJson -OnlyInstalled:$onlyInstalledAppsBox.IsChecked -InstalledList $listOfApps -InitialCheckedFromJson:$false
|
||||
|
||||
# Reset the last selected checkbox when loading a new list
|
||||
$script:MainWindowLastSelectedCheckbox = $null
|
||||
|
||||
# Sort apps alphabetically and add to panel
|
||||
$appsToAdd | Sort-Object -Property FriendlyName | ForEach-Object {
|
||||
$checkbox = New-Object System.Windows.Controls.CheckBox
|
||||
$checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $_.FriendlyName)
|
||||
$checkbox.Tag = $_.AppId
|
||||
$checkbox.IsChecked = $_.IsChecked
|
||||
$checkbox.Style = $window.Resources["AppsPanelCheckBoxStyle"]
|
||||
$loaderScriptPath = $script:LoadAppsDetailsScriptPath
|
||||
$appsFilePath = $script:AppsListFilePath
|
||||
$onlyInstalled = [bool]$onlyInstalledAppsBox.IsChecked
|
||||
|
||||
# Build table row content: App Name | Description | App ID
|
||||
$row = New-Object System.Windows.Controls.Grid
|
||||
$c0 = New-Object System.Windows.Controls.ColumnDefinition; $c0.Width = [System.Windows.GridLength]::new(160)
|
||||
$c1 = New-Object System.Windows.Controls.ColumnDefinition; $c1.Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star)
|
||||
$c2 = New-Object System.Windows.Controls.ColumnDefinition; $c2.Width = [System.Windows.GridLength]::new(286)
|
||||
$row.ColumnDefinitions.Add($c0); $row.ColumnDefinitions.Add($c1); $row.ColumnDefinitions.Add($c2)
|
||||
|
||||
$tbName = New-Object System.Windows.Controls.TextBlock
|
||||
$tbName.Text = $_.FriendlyName
|
||||
$tbName.Style = $window.Resources["AppNameTextStyle"]
|
||||
[System.Windows.Controls.Grid]::SetColumn($tbName, 0)
|
||||
|
||||
$tbDesc = New-Object System.Windows.Controls.TextBlock
|
||||
$tbDesc.Text = $_.Description
|
||||
$tbDesc.Style = $window.Resources["AppDescTextStyle"]
|
||||
$tbDesc.ToolTip = $_.Description
|
||||
[System.Windows.Controls.Grid]::SetColumn($tbDesc, 1)
|
||||
|
||||
$tbId = New-Object System.Windows.Controls.TextBlock
|
||||
$tbId.Text = $_.AppId
|
||||
$tbId.Style = $window.Resources["AppIdTextStyle"]
|
||||
$tbId.ToolTip = $_.AppId
|
||||
[System.Windows.Controls.Grid]::SetColumn($tbId, 2)
|
||||
|
||||
$row.Children.Add($tbName) | Out-Null
|
||||
$row.Children.Add($tbDesc) | Out-Null
|
||||
$row.Children.Add($tbId) | Out-Null
|
||||
$checkbox.Content = $row
|
||||
|
||||
# Store metadata in checkbox for later use
|
||||
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name "AppName" -Value $_.FriendlyName
|
||||
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name "AppDescription" -Value $_.Description
|
||||
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name "SelectedByDefault" -Value $_.SelectedByDefault
|
||||
|
||||
# Add event handler to update status
|
||||
$checkbox.Add_Checked({ UpdateAppSelectionStatus })
|
||||
$checkbox.Add_Unchecked({ UpdateAppSelectionStatus })
|
||||
|
||||
# Attach shift-click behavior for range selection
|
||||
AttachShiftClickBehavior -checkbox $checkbox -appsPanel $appsPanel -lastSelectedCheckboxRef ([ref]$script:MainWindowLastSelectedCheckbox) -updateStatusCallback { UpdateAppSelectionStatus }
|
||||
|
||||
$appsPanel.Children.Add($checkbox) | Out-Null
|
||||
# Use preloaded data if available; otherwise load in background job
|
||||
if (-not $onlyInstalled -and $script:PreloadedAppData) {
|
||||
$rawAppData = $script:PreloadedAppData
|
||||
$script:PreloadedAppData = $null
|
||||
} else {
|
||||
# Load apps details in a background job to keep the UI responsive
|
||||
$rawAppData = Invoke-NonBlocking -ScriptBlock {
|
||||
param($loaderScript, $appsListFilePath, $installedList, $onlyInstalled)
|
||||
$script:AppsListFilePath = $appsListFilePath
|
||||
. $loaderScript
|
||||
LoadAppsDetailsFromJson -OnlyInstalled:$onlyInstalled -InstalledList $installedList -InitialCheckedFromJson:$false
|
||||
} -ArgumentList $loaderScriptPath, $appsFilePath, $listOfApps, $onlyInstalled
|
||||
}
|
||||
|
||||
# Hide loading indicator and navigation blocker, update status
|
||||
$appsToAdd = @($rawAppData | Where-Object { $_ -and ($_.AppId -or $_.FriendlyName) } | Sort-Object -Property FriendlyName)
|
||||
|
||||
$loadingAppsIndicator.Visibility = 'Collapsed'
|
||||
|
||||
if ($appsToAdd.Count -eq 0) {
|
||||
$window.FindName('DeploymentApplyBtn').IsEnabled = $true
|
||||
return
|
||||
}
|
||||
|
||||
$brushSafe = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#4CAF50')
|
||||
$brushUnsafe = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#F44336')
|
||||
$brushDefault = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#FFC107')
|
||||
$brushSafe.Freeze(); $brushUnsafe.Freeze(); $brushDefault.Freeze()
|
||||
|
||||
# Create WPF controls; pump the Dispatcher every batch so the spinner keeps animating.
|
||||
$batchSize = 20
|
||||
for ($i = 0; $i -lt $appsToAdd.Count; $i++) {
|
||||
$app = $appsToAdd[$i]
|
||||
|
||||
$checkbox = New-Object System.Windows.Controls.CheckBox
|
||||
$automationName = if ($app.FriendlyName) { $app.FriendlyName } elseif ($app.AppId) { $app.AppId } else { $null }
|
||||
if ($automationName) { $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $automationName) }
|
||||
$checkbox.Tag = $app.AppId
|
||||
$checkbox.IsChecked = $app.IsChecked
|
||||
$checkbox.Style = $window.Resources['AppsPanelCheckBoxStyle']
|
||||
|
||||
# Build table row: Recommendation dot | Name | Description | App ID
|
||||
$row = New-Object System.Windows.Controls.Grid
|
||||
$row.Style = $window.Resources['AppTableRowStyle']
|
||||
$c0 = New-Object System.Windows.Controls.ColumnDefinition; $c0.Width = $window.Resources['AppTableDotColWidth']
|
||||
$c1 = New-Object System.Windows.Controls.ColumnDefinition; $c1.Width = $window.Resources['AppTableNameColWidth']
|
||||
$c2 = New-Object System.Windows.Controls.ColumnDefinition; $c2.Width = $window.Resources['AppTableDescColWidth']
|
||||
$c3 = New-Object System.Windows.Controls.ColumnDefinition; $c3.Width = $window.Resources['AppTableIdColWidth']
|
||||
$row.ColumnDefinitions.Add($c0); $row.ColumnDefinitions.Add($c1)
|
||||
$row.ColumnDefinitions.Add($c2); $row.ColumnDefinitions.Add($c3)
|
||||
|
||||
$dot = New-Object System.Windows.Shapes.Ellipse
|
||||
$dot.Style = $window.Resources['AppRecommendationDotStyle']
|
||||
$dot.Fill = switch ($app.Recommendation) { 'safe' { $brushSafe } 'unsafe' { $brushUnsafe } default { $brushDefault } }
|
||||
$dot.ToolTip = switch ($app.Recommendation) {
|
||||
'safe' { '[Recommended] Safe to remove for most users' }
|
||||
'unsafe' { '[Not Recommended] Only remove if you know what you are doing' }
|
||||
default { "[Optional] Remove if you don't need this app" }
|
||||
}
|
||||
[System.Windows.Controls.Grid]::SetColumn($dot, 0)
|
||||
|
||||
$tbName = New-Object System.Windows.Controls.TextBlock
|
||||
$tbName.Text = $app.FriendlyName
|
||||
$tbName.Style = $window.Resources['AppNameTextStyle']
|
||||
[System.Windows.Controls.Grid]::SetColumn($tbName, 1)
|
||||
|
||||
$tbDesc = New-Object System.Windows.Controls.TextBlock
|
||||
$tbDesc.Text = $app.Description
|
||||
$tbDesc.Style = $window.Resources['AppDescTextStyle']
|
||||
$tbDesc.ToolTip = $app.Description
|
||||
[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
|
||||
[System.Windows.Controls.Grid]::SetColumn($tbId, 3)
|
||||
|
||||
$row.Children.Add($dot) | Out-Null
|
||||
$row.Children.Add($tbName) | Out-Null
|
||||
$row.Children.Add($tbDesc) | Out-Null
|
||||
$row.Children.Add($tbId) | Out-Null
|
||||
$checkbox.Content = $row
|
||||
|
||||
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
|
||||
|
||||
$checkbox.Add_Checked({ UpdateAppSelectionStatus })
|
||||
$checkbox.Add_Unchecked({ UpdateAppSelectionStatus })
|
||||
AttachShiftClickBehavior -checkbox $checkbox -appsPanel $appsPanel `
|
||||
-lastSelectedCheckboxRef ([ref]$script:MainWindowLastSelectedCheckbox) `
|
||||
-updateStatusCallback { UpdateAppSelectionStatus }
|
||||
|
||||
$appsPanel.Children.Add($checkbox) | Out-Null
|
||||
|
||||
if (($i + 1) % $batchSize -eq 0) { DoEvents }
|
||||
}
|
||||
|
||||
SortApps
|
||||
|
||||
# If Default Mode was clicked while apps were still loading, apply defaults now
|
||||
if ($script:PendingDefaultMode) {
|
||||
$script:PendingDefaultMode = $false
|
||||
ApplyPresetToApps -MatchFilter { param($c) $c.SelectedByDefault -eq $true } -Exclusive
|
||||
}
|
||||
|
||||
UpdateAppSelectionStatus
|
||||
|
||||
# Re-enable Apply button now that the full, correctly-checked app list is ready
|
||||
$window.FindName('DeploymentApplyBtn').IsEnabled = $true
|
||||
}
|
||||
|
||||
# Loads apps into the UI
|
||||
function LoadAppsIntoMainUI {
|
||||
# Cancel any existing load operation to prevent race conditions
|
||||
if ($script:CurrentAppLoadTimer -and $script:CurrentAppLoadTimer.IsEnabled) {
|
||||
$script:CurrentAppLoadTimer.Stop()
|
||||
}
|
||||
if ($script:CurrentAppLoadJob) {
|
||||
Remove-Job -Job $script:CurrentAppLoadJob -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
$script:CurrentAppLoadTimer = $null
|
||||
$script:CurrentAppLoadJob = $null
|
||||
$script:CurrentAppLoadJobStartTime = $null
|
||||
|
||||
# Show loading indicator and navigation blocker, clear existing apps immediately
|
||||
# Prevent concurrent loads
|
||||
if ($script:IsLoadingApps) { return }
|
||||
$script:IsLoadingApps = $true
|
||||
|
||||
# Show loading indicator and clear existing apps
|
||||
$loadingAppsIndicator.Visibility = 'Visible'
|
||||
$appsPanel.Children.Clear()
|
||||
|
||||
|
||||
# Disable Apply button while apps are loading so it can't be clicked with a partial list
|
||||
$window.FindName('DeploymentApplyBtn').IsEnabled = $false
|
||||
|
||||
# Update navigation buttons to disable Next/Previous
|
||||
UpdateNavigationButtons
|
||||
|
||||
# Force UI to update and render all changes (loading indicator, blocker, disabled buttons)
|
||||
$window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Render, [action]{})
|
||||
|
||||
# Schedule the actual loading work to run after UI has updated
|
||||
$window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{
|
||||
$listOfApps = ""
|
||||
|
||||
if ($onlyInstalledAppsBox.IsChecked -and ($script:WingetInstalled -eq $true)) {
|
||||
# Start job to get list of installed apps via WinGet (async helper)
|
||||
$asyncJob = GetInstalledAppsViaWinget -Async
|
||||
$script:CurrentAppLoadJob = $asyncJob.Job
|
||||
$script:CurrentAppLoadJobStartTime = $asyncJob.StartTime
|
||||
|
||||
# Create timer to poll job status without blocking UI
|
||||
$script:CurrentAppLoadTimer = New-Object System.Windows.Threading.DispatcherTimer
|
||||
$script:CurrentAppLoadTimer.Interval = [TimeSpan]::FromMilliseconds(100)
|
||||
|
||||
$script:CurrentAppLoadTimer.Add_Tick({
|
||||
# Check if this timer was cancelled (another load started)
|
||||
if (-not $script:CurrentAppLoadJob -or -not $script:CurrentAppLoadTimer -or -not $script:CurrentAppLoadJobStartTime) {
|
||||
if ($script:CurrentAppLoadTimer) { $script:CurrentAppLoadTimer.Stop() }
|
||||
return
|
||||
}
|
||||
|
||||
$elapsed = (Get-Date) - $script:CurrentAppLoadJobStartTime
|
||||
|
||||
# Check if job is complete or timed out (10 seconds)
|
||||
if ($script:CurrentAppLoadJob.State -eq 'Completed') {
|
||||
$script:CurrentAppLoadTimer.Stop()
|
||||
$listOfApps = Receive-Job -Job $script:CurrentAppLoadJob
|
||||
Remove-Job -Job $script:CurrentAppLoadJob -ErrorAction SilentlyContinue
|
||||
$script:CurrentAppLoadJob = $null
|
||||
$script:CurrentAppLoadTimer = $null
|
||||
$script:CurrentAppLoadJobStartTime = $null
|
||||
|
||||
# Continue with loading apps
|
||||
LoadAppsWithList $listOfApps
|
||||
}
|
||||
elseif ($elapsed.TotalSeconds -gt 10 -or $script:CurrentAppLoadJob.State -eq 'Failed') {
|
||||
$script:CurrentAppLoadTimer.Stop()
|
||||
Remove-Job -Job $script:CurrentAppLoadJob -Force -ErrorAction SilentlyContinue
|
||||
$script:CurrentAppLoadJob = $null
|
||||
$script:CurrentAppLoadTimer = $null
|
||||
$script:CurrentAppLoadJobStartTime = $null
|
||||
|
||||
# Show error that the script was unable to get list of apps from WinGet
|
||||
# Force a render so the loading indicator is visible, then schedule the
|
||||
# actual loading at Background priority so this call returns immediately.
|
||||
# This is critical when called from Add_Loaded: the window must finish
|
||||
# its initialization before we start a nested message pump via DoEvents.
|
||||
$window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Render, [action]{})
|
||||
$window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{
|
||||
try {
|
||||
$listOfApps = ""
|
||||
|
||||
if ($onlyInstalledAppsBox.IsChecked -and ($script:WingetInstalled -eq $true)) {
|
||||
$listOfApps = GetInstalledAppsViaWinget -TimeOut 10 -NonBlocking
|
||||
|
||||
if ($null -eq $listOfApps) {
|
||||
Show-MessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null
|
||||
$onlyInstalledAppsBox.IsChecked = $false
|
||||
|
||||
# Continue with loading all apps (unchecked now)
|
||||
LoadAppsWithList ""
|
||||
}
|
||||
})
|
||||
|
||||
$script:CurrentAppLoadTimer.Start()
|
||||
return # Exit here, timer will continue the work
|
||||
}
|
||||
}
|
||||
|
||||
# If checkbox is not checked or winget not installed, load all apps immediately
|
||||
LoadAppsWithList $listOfApps
|
||||
LoadAppsWithList $listOfApps
|
||||
}
|
||||
finally {
|
||||
$script:IsLoadingApps = $false
|
||||
}
|
||||
}) | Out-Null
|
||||
}
|
||||
|
||||
# Event handlers for app selection
|
||||
$onlyInstalledAppsBox.Add_Checked({
|
||||
LoadAppsIntoMainUI
|
||||
LoadAppsIntoMainUI
|
||||
})
|
||||
$onlyInstalledAppsBox.Add_Unchecked({
|
||||
LoadAppsIntoMainUI
|
||||
LoadAppsIntoMainUI
|
||||
})
|
||||
|
||||
# Quick selection buttons - only select apps actually in those categories
|
||||
$defaultAppsBtn.Add_Click({
|
||||
foreach ($child in $appsPanel.Children) {
|
||||
if ($child -is [System.Windows.Controls.CheckBox]) {
|
||||
if ($child.SelectedByDefault -eq $true) {
|
||||
$child.IsChecked = $true
|
||||
} else {
|
||||
$child.IsChecked = $false
|
||||
}
|
||||
}
|
||||
# Animate arrow when popup opens/closes, and lazily update preset states
|
||||
$presetsPopup.Add_Opened({
|
||||
UpdatePresetStates
|
||||
$animation = New-Object System.Windows.Media.Animation.DoubleAnimation
|
||||
$animation.To = 180
|
||||
$animation.Duration = [System.Windows.Duration]::new([System.TimeSpan]::FromMilliseconds(200))
|
||||
$animation.EasingFunction = New-Object System.Windows.Media.Animation.CubicEase
|
||||
$animation.EasingFunction.EasingMode = 'EaseOut'
|
||||
$presetsArrow.RenderTransform.BeginAnimation([System.Windows.Media.RotateTransform]::AngleProperty, $animation)
|
||||
})
|
||||
$presetsPopup.Add_Closed({
|
||||
$animation = New-Object System.Windows.Media.Animation.DoubleAnimation
|
||||
$animation.To = 0
|
||||
$animation.Duration = [System.Windows.Duration]::new([System.TimeSpan]::FromMilliseconds(200))
|
||||
$animation.EasingFunction = New-Object System.Windows.Media.Animation.CubicEase
|
||||
$animation.EasingFunction.EasingMode = 'EaseOut'
|
||||
$presetsArrow.RenderTransform.BeginAnimation([System.Windows.Media.RotateTransform]::AngleProperty, $animation)
|
||||
$presetsBtn.IsChecked = $false
|
||||
})
|
||||
|
||||
# Close popup when clicking anywhere outside the popup or the presets button.
|
||||
$window.Add_PreviewMouseDown({
|
||||
if (-not $presetsPopup.IsOpen) { return }
|
||||
if ($presetsPopup.Child -ne $null -and $presetsPopup.Child.IsMouseOver) { return }
|
||||
$src = $_.OriginalSource -as [System.Windows.DependencyObject]
|
||||
if ($src -ne $null) {
|
||||
$inBtn = $presetsBtn.IsAncestorOf($src) -or [System.Object]::ReferenceEquals($presetsBtn, $src)
|
||||
if (-not $inBtn) { $presetsPopup.IsOpen = $false }
|
||||
}
|
||||
})
|
||||
|
||||
# Toggle popup on button click
|
||||
$presetsBtn.Add_Click({
|
||||
$presetsPopup.IsOpen = -not $presetsPopup.IsOpen
|
||||
$presetsBtn.IsChecked = $presetsPopup.IsOpen
|
||||
})
|
||||
|
||||
# Preset: Default selection
|
||||
$presetDefaultApps.Add_Click({
|
||||
if ($script:UpdatingPresets) { return }
|
||||
$check = ($this.IsChecked -eq $true)
|
||||
if ($this.IsChecked -eq $null) { $this.IsChecked = $false; $check = $false }
|
||||
ApplyPresetToApps -MatchFilter { param($c) $c.SelectedByDefault -eq $true } -Check $check
|
||||
})
|
||||
|
||||
# Clear selection button + reset all preset checkboxes
|
||||
$clearAppSelectionBtn.Add_Click({
|
||||
foreach ($child in $appsPanel.Children) {
|
||||
if ($child -is [System.Windows.Controls.CheckBox]) {
|
||||
$child.IsChecked = $false
|
||||
}
|
||||
}
|
||||
ApplyPresetToApps -MatchFilter { param($c) $true } -Check $false
|
||||
})
|
||||
|
||||
# Column header sort handlers
|
||||
$headerNameBtn.Add_MouseLeftButtonUp({ SetSortColumn 'Name' })
|
||||
$headerDescriptionBtn.Add_MouseLeftButtonUp({ SetSortColumn 'Description' })
|
||||
$headerAppIdBtn.Add_MouseLeftButtonUp({ SetSortColumn 'AppId' })
|
||||
|
||||
# Helper function to scroll to an item if it's not visible, centering it in the viewport
|
||||
function ScrollToItemIfNotVisible {
|
||||
param (
|
||||
@@ -830,7 +1050,7 @@ function Show-MainWindow {
|
||||
# The 17px accounts for the scrollbar width + some padding
|
||||
$tweakSearchBorder.Margin = [System.Windows.Thickness]::new(0, 0, 17, 0)
|
||||
} else {
|
||||
$tweakSearchBorder.Margin = [System.Windows.Thickness]::new(0, 0, 0, 0)
|
||||
$tweakSearchBorder.Margin = [System.Windows.Thickness]::new(0)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -980,9 +1200,6 @@ function Show-MainWindow {
|
||||
|
||||
# Update progress indicators
|
||||
# Tab indices: 0=Home, 1=App Removal, 2=Tweaks, 3=Deployment Settings
|
||||
$blueColor = "#0067c0"
|
||||
$greyColor = "#808080"
|
||||
|
||||
$progressIndicator1 = $window.FindName('ProgressIndicator1') # App Removal
|
||||
$progressIndicator2 = $window.FindName('ProgressIndicator2') # Tweaks
|
||||
$progressIndicator3 = $window.FindName('ProgressIndicator3') # Deployment Settings
|
||||
@@ -998,23 +1215,23 @@ function Show-MainWindow {
|
||||
# Update indicator colors based on current tab
|
||||
# Indicator 1 (App Removal) - tab index 1
|
||||
if ($currentIndex -ge 1) {
|
||||
$progressIndicator1.Fill = $blueColor
|
||||
$progressIndicator1.Fill = $window.Resources['ProgressActiveColor']
|
||||
} else {
|
||||
$progressIndicator1.Fill = $greyColor
|
||||
$progressIndicator1.Fill = $window.Resources['ProgressInactiveColor']
|
||||
}
|
||||
|
||||
# Indicator 2 (Tweaks) - tab index 2
|
||||
if ($currentIndex -ge 2) {
|
||||
$progressIndicator2.Fill = $blueColor
|
||||
$progressIndicator2.Fill = $window.Resources['ProgressActiveColor']
|
||||
} else {
|
||||
$progressIndicator2.Fill = $greyColor
|
||||
$progressIndicator2.Fill = $window.Resources['ProgressInactiveColor']
|
||||
}
|
||||
|
||||
# Indicator 3 (Deployment Settings) - tab index 3
|
||||
if ($currentIndex -ge 3) {
|
||||
$progressIndicator3.Fill = $blueColor
|
||||
$progressIndicator3.Fill = $window.Resources['ProgressActiveColor']
|
||||
} else {
|
||||
$progressIndicator3.Fill = $greyColor
|
||||
$progressIndicator3.Fill = $window.Resources['ProgressInactiveColor']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1033,7 +1250,7 @@ function Show-MainWindow {
|
||||
$appRemovalScopeCombo.SelectedIndex = 0
|
||||
}
|
||||
1 {
|
||||
$userSelectionDescription.Text = "Changes will be applied to a different user profile on this system."
|
||||
$userSelectionDescription.Text = "Changes will be applied to a different user profile on this system. Note: changes may not apply correctly if the target user is currently logged in."
|
||||
$otherUserPanel.Visibility = 'Visible'
|
||||
$usernameValidationMessage.Text = ""
|
||||
# Hide "Current user only" option, show "Target user only" option
|
||||
@@ -1099,8 +1316,8 @@ function Show-MainWindow {
|
||||
|
||||
$username = $otherUsernameTextBox.Text.Trim()
|
||||
|
||||
$errorBrush = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#c42b1c"))
|
||||
$successBrush = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#28a745"))
|
||||
$errorBrush = $window.Resources['ValidationErrorColor']
|
||||
$successBrush = $window.Resources['ValidationSuccessColor']
|
||||
|
||||
if ($username.Length -eq 0) {
|
||||
$usernameValidationMessage.Text = "[X] Please enter a username"
|
||||
@@ -1128,9 +1345,6 @@ function Show-MainWindow {
|
||||
}
|
||||
|
||||
function GenerateOverview {
|
||||
# Load Features.json
|
||||
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0"
|
||||
|
||||
$changesList = @()
|
||||
|
||||
# Collect selected apps
|
||||
@@ -1180,13 +1394,14 @@ function Show-MainWindow {
|
||||
# For combobox: SelectedIndex 0 = No Change, so subtract 1 to index into Values
|
||||
$selectedValue = $mapping.Values[$control.SelectedIndex - 1]
|
||||
foreach ($fid in $selectedValue.FeatureIds) {
|
||||
$feature = $featuresJson.Features | Where-Object { $_.FeatureId -eq $fid }
|
||||
if ($feature) { $changesList += ($feature.Action + ' ' + $feature.Label) }
|
||||
$label = $script:FeatureLabelLookup[$fid]
|
||||
if ($label) { $changesList += $label }
|
||||
}
|
||||
}
|
||||
elseif ($mapping.Type -eq 'feature') {
|
||||
$feature = $featuresJson.Features | Where-Object { $_.FeatureId -eq $mapping.FeatureId }
|
||||
if ($feature) { $changesList += ($feature.Action + ' ' + $feature.Label) }
|
||||
$label = $script:FeatureLabelLookup[$mapping.FeatureId]
|
||||
if (-not $label) { $label = $mapping.Action + ' ' + $mapping.Label }
|
||||
$changesList += $label
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1207,7 +1422,7 @@ function Show-MainWindow {
|
||||
Show-MessageBox -Message $message -Title 'Selected Changes' -Button 'OK' -Icon 'None' -Width 600
|
||||
}
|
||||
|
||||
$previousBtn.Add_Click({
|
||||
$previousBtn.Add_Click({
|
||||
Hide-Bubble -Immediate
|
||||
if ($tabControl.SelectedIndex -gt 0) {
|
||||
$tabControl.SelectedIndex--
|
||||
@@ -1215,10 +1430,9 @@ function Show-MainWindow {
|
||||
}
|
||||
})
|
||||
|
||||
$nextBtn.Add_Click({
|
||||
$nextBtn.Add_Click({
|
||||
if ($tabControl.SelectedIndex -lt ($tabControl.Items.Count - 1)) {
|
||||
$tabControl.SelectedIndex++
|
||||
|
||||
UpdateNavigationButtons
|
||||
}
|
||||
})
|
||||
@@ -1240,11 +1454,11 @@ function Show-MainWindow {
|
||||
ApplySettingsToUiControls -window $window -settingsJson $defaultsJson -uiControlMappings $script:UiControlMappings
|
||||
}
|
||||
|
||||
# Select default apps
|
||||
foreach ($child in $appsPanel.Children) {
|
||||
if ($child -is [System.Windows.Controls.CheckBox]) {
|
||||
$child.IsChecked = ($child.SelectedByDefault -eq $true)
|
||||
}
|
||||
# Deselect all apps, then select default apps (defer if apps are still loading in the background)
|
||||
if ($script:IsLoadingApps) {
|
||||
$script:PendingDefaultMode = $true
|
||||
} else {
|
||||
ApplyPresetToApps -MatchFilter { param($c) $c.SelectedByDefault -eq $true } -Exclusive
|
||||
}
|
||||
|
||||
# Navigate directly to the Deployment Settings tab
|
||||
@@ -1375,8 +1589,17 @@ function Show-MainWindow {
|
||||
|
||||
# Store selected user mode
|
||||
switch ($userSelectionCombo.SelectedIndex) {
|
||||
1 { AddParameter User ($otherUsernameTextBox.Text.Trim()) }
|
||||
2 { AddParameter Sysprep }
|
||||
0 {
|
||||
Write-Host "Selected user mode: current user ($(GetUserName))"
|
||||
}
|
||||
1 {
|
||||
Write-Host "Selected user mode: $($otherUsernameTextBox.Text.Trim())"
|
||||
AddParameter User ($otherUsernameTextBox.Text.Trim())
|
||||
}
|
||||
2 {
|
||||
Write-Host "Selected user mode: default user profile (Sysprep)"
|
||||
AddParameter Sysprep
|
||||
}
|
||||
}
|
||||
|
||||
SaveSettings
|
||||
@@ -1452,7 +1675,6 @@ function Show-MainWindow {
|
||||
|
||||
# Handle Load Last Used settings and Load Last Used apps
|
||||
$loadLastUsedBtn = $window.FindName('LoadLastUsedBtn')
|
||||
$loadLastUsedAppsBtn = $window.FindName('LoadLastUsedAppsBtn')
|
||||
|
||||
$lastUsedSettingsJson = LoadJsonFile -filePath $script:SavedSettingsFilePath -expectedVersion "1.0" -optionalFile
|
||||
|
||||
@@ -1481,28 +1703,24 @@ function Show-MainWindow {
|
||||
$loadLastUsedBtn.Visibility = 'Collapsed'
|
||||
}
|
||||
|
||||
# Show option to load last used apps if they exist
|
||||
# Preset: Last used selection (wired to PresetLastUsed checkbox)
|
||||
if ($appsSetting -and $appsSetting.ToString().Trim().Length -gt 0) {
|
||||
$loadLastUsedAppsBtn.Add_Click({
|
||||
try {
|
||||
$savedApps = @()
|
||||
if ($appsSetting -is [string]) { $savedApps = $appsSetting.Split(',') }
|
||||
elseif ($appsSetting -is [array]) { $savedApps = $appsSetting }
|
||||
$savedApps = $savedApps | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }
|
||||
# Parse and store saved app IDs for UpdatePresetStates
|
||||
$script:SavedAppIds = @()
|
||||
if ($appsSetting -is [string]) { $script:SavedAppIds = $appsSetting.Split(',') }
|
||||
elseif ($appsSetting -is [array]) { $script:SavedAppIds = $appsSetting }
|
||||
$script:SavedAppIds = $script:SavedAppIds | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }
|
||||
|
||||
foreach ($child in $appsPanel.Children) {
|
||||
if ($child -is [System.Windows.Controls.CheckBox]) {
|
||||
if ($savedApps -contains $child.Tag) { $child.IsChecked = $true } else { $child.IsChecked = $false }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Show-MessageBox -Message "Failed to load last used app selection: $_" -Title "Error" -Button 'OK' -Icon 'Error'
|
||||
}
|
||||
$presetLastUsed.Add_Click({
|
||||
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
|
||||
})
|
||||
}
|
||||
else {
|
||||
$loadLastUsedAppsBtn.Visibility = 'Collapsed'
|
||||
$script:SavedAppIds = $null
|
||||
$presetLastUsed.Visibility = 'Collapsed'
|
||||
}
|
||||
|
||||
# Clear All Tweaks button
|
||||
@@ -1519,9 +1737,17 @@ function Show-MainWindow {
|
||||
$control.SelectedIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
# Preload app data to speed up loading when user navigates to App Removal tab
|
||||
try {
|
||||
$script:PreloadedAppData = LoadAppsDetailsFromJson -OnlyInstalled:$false -InstalledList '' -InitialCheckedFromJson:$false
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to preload apps list: $_"
|
||||
}
|
||||
|
||||
# Show the window
|
||||
return $window.ShowDialog()
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ if (Test-Path "$env:TEMP/Win11Debloat") {
|
||||
Write-Output "> Cleaning up..."
|
||||
|
||||
# Cleanup, remove Win11Debloat directory
|
||||
Get-ChildItem -Path "$env:TEMP/Win11Debloat" -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Config,Logs | Remove-Item -Recurse -Force
|
||||
Get-ChildItem -Path "$env:TEMP/Win11Debloat" -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Win11Debloat-Run.log,Config,Logs | Remove-Item -Recurse -Force
|
||||
}
|
||||
|
||||
Write-Output ""
|
||||
|
||||
15
Scripts/Helpers/AddParameter.ps1
Normal file
15
Scripts/Helpers/AddParameter.ps1
Normal file
@@ -0,0 +1,15 @@
|
||||
# Add parameter to script and write to file
|
||||
function AddParameter {
|
||||
param (
|
||||
$parameterName,
|
||||
$value = $true
|
||||
)
|
||||
|
||||
# Add parameter or update its value if key already exists
|
||||
if (-not $script:Params.ContainsKey($parameterName)) {
|
||||
$script:Params.Add($parameterName, $value)
|
||||
}
|
||||
else {
|
||||
$script:Params[$parameterName] = $value
|
||||
}
|
||||
}
|
||||
32
Scripts/Helpers/CheckIfUserExists.ps1
Normal file
32
Scripts/Helpers/CheckIfUserExists.ps1
Normal file
@@ -0,0 +1,32 @@
|
||||
function CheckIfUserExists {
|
||||
param (
|
||||
$userName
|
||||
)
|
||||
|
||||
if ($userName -match '[<>:"|?*]') {
|
||||
return $false
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($userName)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
try {
|
||||
$userExists = Test-Path "$env:SystemDrive\Users\$userName"
|
||||
|
||||
if ($userExists) {
|
||||
return $true
|
||||
}
|
||||
|
||||
$userExists = Test-Path ($env:USERPROFILE -Replace ('\\' + $env:USERNAME + '$'), "\$userName")
|
||||
|
||||
if ($userExists) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Error "Something went wrong when trying to find the user directory path for user $userName. Please ensure the user exists on this system"
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
27
Scripts/Helpers/CheckModernStandbySupport.ps1
Normal file
27
Scripts/Helpers/CheckModernStandbySupport.ps1
Normal file
@@ -0,0 +1,27 @@
|
||||
# Check if this machine supports S0 Modern Standby power state. Returns true if S0 Modern Standby is supported, false otherwise.
|
||||
function CheckModernStandbySupport {
|
||||
$count = 0
|
||||
|
||||
try {
|
||||
switch -Regex (powercfg /a) {
|
||||
':' {
|
||||
$count += 1
|
||||
}
|
||||
|
||||
'(.*S0.{1,}\))' {
|
||||
if ($count -eq 1) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host "Error: Unable to check for S0 Modern Standby support, powercfg command failed" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
Write-Host "Press any key to continue..."
|
||||
$null = [System.Console]::ReadKey()
|
||||
return $true
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
20
Scripts/Helpers/GenerateAppsList.ps1
Normal file
20
Scripts/Helpers/GenerateAppsList.ps1
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generates a list of apps to remove based on the Apps parameter
|
||||
function GenerateAppsList {
|
||||
if (-not ($script:Params["Apps"] -and $script:Params["Apps"] -is [string])) {
|
||||
return @()
|
||||
}
|
||||
|
||||
$appMode = $script:Params["Apps"].toLower()
|
||||
|
||||
switch ($appMode) {
|
||||
'default' {
|
||||
$appsList = LoadAppsFromFile $script:AppsListFilePath
|
||||
return $appsList
|
||||
}
|
||||
default {
|
||||
$appsList = $script:Params["Apps"].Split(',') | ForEach-Object { $_.Trim() }
|
||||
$validatedAppsList = ValidateAppslist $appsList
|
||||
return $validatedAppsList
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Scripts/Helpers/GetFriendlyTargetUserName.ps1
Normal file
9
Scripts/Helpers/GetFriendlyTargetUserName.ps1
Normal file
@@ -0,0 +1,9 @@
|
||||
function GetFriendlyTargetUserName {
|
||||
$target = GetTargetUserForAppRemoval
|
||||
|
||||
switch ($target) {
|
||||
"AllUsers" { return "all users" }
|
||||
"CurrentUser" { return "the current user" }
|
||||
default { return "user $target" }
|
||||
}
|
||||
}
|
||||
9
Scripts/Helpers/GetTargetUserForAppRemoval.ps1
Normal file
9
Scripts/Helpers/GetTargetUserForAppRemoval.ps1
Normal file
@@ -0,0 +1,9 @@
|
||||
# Target is determined from $script:Params["AppRemovalTarget"] or defaults to "AllUsers"
|
||||
# Target values: "AllUsers" (removes for all users + from image), "CurrentUser", or a specific username
|
||||
function GetTargetUserForAppRemoval {
|
||||
if ($script:Params.ContainsKey("AppRemovalTarget")) {
|
||||
return $script:Params["AppRemovalTarget"]
|
||||
}
|
||||
|
||||
return "AllUsers"
|
||||
}
|
||||
36
Scripts/Helpers/GetUserDirectory.ps1
Normal file
36
Scripts/Helpers/GetUserDirectory.ps1
Normal file
@@ -0,0 +1,36 @@
|
||||
# Returns the directory path of the specified user, exits script if user path can't be found
|
||||
function GetUserDirectory {
|
||||
param (
|
||||
$userName,
|
||||
$fileName = "",
|
||||
$exitIfPathNotFound = $true
|
||||
)
|
||||
|
||||
try {
|
||||
if (-not (CheckIfUserExists -userName $userName) -and $userName -ne "*") {
|
||||
Write-Error "User $userName does not exist on this system"
|
||||
AwaitKeyToExit
|
||||
}
|
||||
|
||||
$userDirectoryExists = Test-Path "$env:SystemDrive\Users\$userName"
|
||||
$userPath = "$env:SystemDrive\Users\$userName\$fileName"
|
||||
|
||||
if ((Test-Path $userPath) -or ($userDirectoryExists -and (-not $exitIfPathNotFound))) {
|
||||
return $userPath
|
||||
}
|
||||
|
||||
$userDirectoryExists = Test-Path ($env:USERPROFILE -Replace ('\\' + $env:USERNAME + '$'), "\$userName")
|
||||
$userPath = $env:USERPROFILE -Replace ('\\' + $env:USERNAME + '$'), "\$userName\$fileName"
|
||||
|
||||
if ((Test-Path $userPath) -or ($userDirectoryExists -and (-not $exitIfPathNotFound))) {
|
||||
return $userPath
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Error "Something went wrong when trying to find the user directory path for user $userName. Please ensure the user exists on this system"
|
||||
AwaitKeyToExit
|
||||
}
|
||||
|
||||
Write-Error "Unable to find user directory path for user $userName"
|
||||
AwaitKeyToExit
|
||||
}
|
||||
7
Scripts/Helpers/GetUserName.ps1
Normal file
7
Scripts/Helpers/GetUserName.ps1
Normal file
@@ -0,0 +1,7 @@
|
||||
function GetUserName {
|
||||
if ($script:Params.ContainsKey("User")) {
|
||||
return $script:Params.Item("User")
|
||||
}
|
||||
|
||||
return $env:USERNAME
|
||||
}
|
||||
16
Scripts/Threading/DoEvents.ps1
Normal file
16
Scripts/Threading/DoEvents.ps1
Normal file
@@ -0,0 +1,16 @@
|
||||
# Processes all pending WPF window messages (input, render, etc.) to keep the UI responsive
|
||||
# during long-running operations on the UI thread. Equivalent to Application.DoEvents().
|
||||
function DoEvents {
|
||||
if (-not $script:GuiWindow) { return }
|
||||
$frame = [System.Windows.Threading.DispatcherFrame]::new()
|
||||
$null = [System.Windows.Threading.Dispatcher]::CurrentDispatcher.BeginInvoke(
|
||||
[System.Windows.Threading.DispatcherPriority]::Background,
|
||||
[System.Windows.Threading.DispatcherOperationCallback]{
|
||||
param($f)
|
||||
$f.Continue = $false
|
||||
return $null
|
||||
},
|
||||
$frame
|
||||
)
|
||||
$null = [System.Windows.Threading.Dispatcher]::PushFrame($frame)
|
||||
}
|
||||
55
Scripts/Threading/Invoke-NonBlocking.ps1
Normal file
55
Scripts/Threading/Invoke-NonBlocking.ps1
Normal file
@@ -0,0 +1,55 @@
|
||||
# Runs a scriptblock in a background PowerShell runspace while keeping the UI responsive.
|
||||
# In GUI mode, the work executes on a separate thread and the UI thread pumps messages (~60fps).
|
||||
# In CLI mode, the scriptblock runs directly in the current session.
|
||||
function Invoke-NonBlocking {
|
||||
param(
|
||||
[scriptblock]$ScriptBlock,
|
||||
[object[]]$ArgumentList = @(),
|
||||
[int]$TimeoutSeconds = 0
|
||||
)
|
||||
|
||||
# CLI mode without timeout: run directly in-process
|
||||
if (-not $script:GuiWindow -and $TimeoutSeconds -eq 0) {
|
||||
return (& $ScriptBlock @ArgumentList)
|
||||
}
|
||||
|
||||
$ps = [powershell]::Create()
|
||||
try {
|
||||
$null = $ps.AddScript($ScriptBlock.ToString())
|
||||
foreach ($arg in $ArgumentList) {
|
||||
$null = $ps.AddArgument($arg)
|
||||
}
|
||||
|
||||
$handle = $ps.BeginInvoke()
|
||||
|
||||
if ($script:GuiWindow) {
|
||||
# GUI mode: pump UI messages while waiting
|
||||
$stopwatch = if ($TimeoutSeconds -gt 0) { [System.Diagnostics.Stopwatch]::StartNew() } else { $null }
|
||||
|
||||
while (-not $handle.IsCompleted) {
|
||||
if ($stopwatch -and $stopwatch.Elapsed.TotalSeconds -ge $TimeoutSeconds) {
|
||||
$ps.Stop()
|
||||
throw "Operation timed out after $TimeoutSeconds seconds"
|
||||
}
|
||||
DoEvents
|
||||
Start-Sleep -Milliseconds 16
|
||||
}
|
||||
}
|
||||
else {
|
||||
# CLI mode with timeout: block until completion or timeout
|
||||
if (-not $handle.AsyncWaitHandle.WaitOne($TimeoutSeconds * 1000)) {
|
||||
$ps.Stop()
|
||||
throw "Operation timed out after $TimeoutSeconds seconds"
|
||||
}
|
||||
}
|
||||
|
||||
$result = $ps.EndInvoke($handle)
|
||||
|
||||
if ($result.Count -eq 0) { return $null }
|
||||
if ($result.Count -eq 1) { return $result[0] }
|
||||
return @($result)
|
||||
}
|
||||
finally {
|
||||
$ps.Dispose()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user