mirror of
https://github.com/Raphire/Win11Debloat.git
synced 2026-02-17 07:56:24 +00:00
1502 lines
64 KiB
PowerShell
1502 lines
64 KiB
PowerShell
|
|
function Show-MainWindow {
|
||
|
|
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase,System.Windows.Forms | Out-Null
|
||
|
|
|
||
|
|
# Get current Windows build version
|
||
|
|
$WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' CurrentBuild
|
||
|
|
|
||
|
|
$usesDarkMode = GetSystemUsesDarkMode
|
||
|
|
|
||
|
|
# Load XAML from file
|
||
|
|
$xaml = Get-Content -Path $script:MainWindowSchema -Raw
|
||
|
|
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
|
||
|
|
try {
|
||
|
|
$window = [System.Windows.Markup.XamlReader]::Load($reader)
|
||
|
|
}
|
||
|
|
finally {
|
||
|
|
$reader.Close()
|
||
|
|
}
|
||
|
|
|
||
|
|
SetWindowThemeResources -window $window -usesDarkMode $usesDarkMode
|
||
|
|
|
||
|
|
# Get named elements
|
||
|
|
$titleBar = $window.FindName('TitleBar')
|
||
|
|
$kofiBtn = $window.FindName('KofiBtn')
|
||
|
|
$menuBtn = $window.FindName('MenuBtn')
|
||
|
|
$closeBtn = $window.FindName('CloseBtn')
|
||
|
|
$menuDocumentation = $window.FindName('MenuDocumentation')
|
||
|
|
$menuReportBug = $window.FindName('MenuReportBug')
|
||
|
|
$menuLogs = $window.FindName('MenuLogs')
|
||
|
|
$menuAbout = $window.FindName('MenuAbout')
|
||
|
|
|
||
|
|
# Title bar event handlers
|
||
|
|
$titleBar.Add_MouseLeftButtonDown({
|
||
|
|
if ($_.OriginalSource -is [System.Windows.Controls.Grid] -or $_.OriginalSource -is [System.Windows.Controls.Border] -or $_.OriginalSource -is [System.Windows.Controls.TextBlock]) {
|
||
|
|
$window.DragMove()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
$kofiBtn.Add_Click({
|
||
|
|
Start-Process "https://ko-fi.com/raphire"
|
||
|
|
})
|
||
|
|
|
||
|
|
$menuBtn.Add_Click({
|
||
|
|
$menuBtn.ContextMenu.PlacementTarget = $menuBtn
|
||
|
|
$menuBtn.ContextMenu.Placement = [System.Windows.Controls.Primitives.PlacementMode]::Bottom
|
||
|
|
$menuBtn.ContextMenu.IsOpen = $true
|
||
|
|
})
|
||
|
|
|
||
|
|
$menuDocumentation.Add_Click({
|
||
|
|
Start-Process "https://github.com/Raphire/Win11Debloat/wiki"
|
||
|
|
})
|
||
|
|
|
||
|
|
$menuReportBug.Add_Click({
|
||
|
|
Start-Process "https://github.com/Raphire/Win11Debloat/issues"
|
||
|
|
})
|
||
|
|
|
||
|
|
$menuLogs.Add_Click({
|
||
|
|
$logsFolder = Join-Path $PSScriptRoot "../../Logs"
|
||
|
|
if (Test-Path $logsFolder) {
|
||
|
|
Start-Process "explorer.exe" -ArgumentList $logsFolder
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
Show-MessageBox -Message "No logs folder found at: $logsFolder" -Title "Logs" -Button 'OK' -Icon 'Information'
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
$menuAbout.Add_Click({
|
||
|
|
Show-AboutDialog -Owner $window
|
||
|
|
})
|
||
|
|
|
||
|
|
$closeBtn.Add_Click({
|
||
|
|
$window.Close()
|
||
|
|
})
|
||
|
|
|
||
|
|
# Ensure closing the main window stops all execution
|
||
|
|
$window.Add_Closing({
|
||
|
|
$script:CancelRequested = $true
|
||
|
|
})
|
||
|
|
|
||
|
|
# Implement window resize functionality
|
||
|
|
$resizeLeft = $window.FindName('ResizeLeft')
|
||
|
|
$resizeRight = $window.FindName('ResizeRight')
|
||
|
|
$resizeTop = $window.FindName('ResizeTop')
|
||
|
|
$resizeBottom = $window.FindName('ResizeBottom')
|
||
|
|
$resizeTopLeft = $window.FindName('ResizeTopLeft')
|
||
|
|
$resizeTopRight = $window.FindName('ResizeTopRight')
|
||
|
|
$resizeBottomLeft = $window.FindName('ResizeBottomLeft')
|
||
|
|
$resizeBottomRight = $window.FindName('ResizeBottomRight')
|
||
|
|
|
||
|
|
$script:resizing = $false
|
||
|
|
$script:resizeEdges = $null
|
||
|
|
$script:resizeStart = $null
|
||
|
|
$script:windowStart = $null
|
||
|
|
$script:resizeElement = $null
|
||
|
|
|
||
|
|
$resizeHandler = {
|
||
|
|
param($sender, $e)
|
||
|
|
|
||
|
|
$script:resizing = $true
|
||
|
|
$script:resizeElement = $sender
|
||
|
|
$script:resizeStart = [System.Windows.Forms.Cursor]::Position
|
||
|
|
$script:windowStart = @{
|
||
|
|
Left = $window.Left
|
||
|
|
Top = $window.Top
|
||
|
|
Width = $window.ActualWidth
|
||
|
|
Height = $window.ActualHeight
|
||
|
|
}
|
||
|
|
|
||
|
|
# Parse direction tag into edge flags for cleaner resize logic
|
||
|
|
$direction = $sender.Tag
|
||
|
|
$script:resizeEdges = @{
|
||
|
|
Left = $direction -match 'Left'
|
||
|
|
Right = $direction -match 'Right'
|
||
|
|
Top = $direction -match 'Top'
|
||
|
|
Bottom = $direction -match 'Bottom'
|
||
|
|
}
|
||
|
|
|
||
|
|
$sender.CaptureMouse()
|
||
|
|
$e.Handled = $true
|
||
|
|
}
|
||
|
|
|
||
|
|
$moveHandler = {
|
||
|
|
param($sender, $e)
|
||
|
|
if (-not $script:resizing) { return }
|
||
|
|
|
||
|
|
$current = [System.Windows.Forms.Cursor]::Position
|
||
|
|
$deltaX = $current.X - $script:resizeStart.X
|
||
|
|
$deltaY = $current.Y - $script:resizeStart.Y
|
||
|
|
|
||
|
|
# Handle horizontal resize
|
||
|
|
if ($script:resizeEdges.Left) {
|
||
|
|
$newWidth = [Math]::Max($window.MinWidth, $script:windowStart.Width - $deltaX)
|
||
|
|
if ($newWidth -ne $window.Width) {
|
||
|
|
$window.Left = $script:windowStart.Left + ($script:windowStart.Width - $newWidth)
|
||
|
|
$window.Width = $newWidth
|
||
|
|
}
|
||
|
|
}
|
||
|
|
elseif ($script:resizeEdges.Right) {
|
||
|
|
$window.Width = [Math]::Max($window.MinWidth, $script:windowStart.Width + $deltaX)
|
||
|
|
}
|
||
|
|
|
||
|
|
# Handle vertical resize
|
||
|
|
if ($script:resizeEdges.Top) {
|
||
|
|
$newHeight = [Math]::Max($window.MinHeight, $script:windowStart.Height - $deltaY)
|
||
|
|
if ($newHeight -ne $window.Height) {
|
||
|
|
$window.Top = $script:windowStart.Top + ($script:windowStart.Height - $newHeight)
|
||
|
|
$window.Height = $newHeight
|
||
|
|
}
|
||
|
|
}
|
||
|
|
elseif ($script:resizeEdges.Bottom) {
|
||
|
|
$window.Height = [Math]::Max($window.MinHeight, $script:windowStart.Height + $deltaY)
|
||
|
|
}
|
||
|
|
|
||
|
|
$e.Handled = $true
|
||
|
|
}
|
||
|
|
|
||
|
|
$releaseHandler = {
|
||
|
|
param($sender, $e)
|
||
|
|
if ($script:resizing -and $script:resizeElement) {
|
||
|
|
$script:resizing = $false
|
||
|
|
$script:resizeEdges = $null
|
||
|
|
$script:resizeElement.ReleaseMouseCapture()
|
||
|
|
$script:resizeElement = $null
|
||
|
|
$e.Handled = $true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Set tags and add event handlers for resize borders
|
||
|
|
$resizeLeft.Tag = 'Left'
|
||
|
|
$resizeLeft.Add_PreviewMouseLeftButtonDown($resizeHandler)
|
||
|
|
$resizeLeft.Add_MouseMove($moveHandler)
|
||
|
|
$resizeLeft.Add_MouseLeftButtonUp($releaseHandler)
|
||
|
|
|
||
|
|
$resizeRight.Tag = 'Right'
|
||
|
|
$resizeRight.Add_PreviewMouseLeftButtonDown($resizeHandler)
|
||
|
|
$resizeRight.Add_MouseMove($moveHandler)
|
||
|
|
$resizeRight.Add_MouseLeftButtonUp($releaseHandler)
|
||
|
|
|
||
|
|
$resizeTop.Tag = 'Top'
|
||
|
|
$resizeTop.Add_PreviewMouseLeftButtonDown($resizeHandler)
|
||
|
|
$resizeTop.Add_MouseMove($moveHandler)
|
||
|
|
$resizeTop.Add_MouseLeftButtonUp($releaseHandler)
|
||
|
|
|
||
|
|
$resizeBottom.Tag = 'Bottom'
|
||
|
|
$resizeBottom.Add_PreviewMouseLeftButtonDown($resizeHandler)
|
||
|
|
$resizeBottom.Add_MouseMove($moveHandler)
|
||
|
|
$resizeBottom.Add_MouseLeftButtonUp($releaseHandler)
|
||
|
|
|
||
|
|
$resizeTopLeft.Tag = 'TopLeft'
|
||
|
|
$resizeTopLeft.Add_PreviewMouseLeftButtonDown($resizeHandler)
|
||
|
|
$resizeTopLeft.Add_MouseMove($moveHandler)
|
||
|
|
$resizeTopLeft.Add_MouseLeftButtonUp($releaseHandler)
|
||
|
|
|
||
|
|
$resizeTopRight.Tag = 'TopRight'
|
||
|
|
$resizeTopRight.Add_PreviewMouseLeftButtonDown($resizeHandler)
|
||
|
|
$resizeTopRight.Add_MouseMove($moveHandler)
|
||
|
|
$resizeTopRight.Add_MouseLeftButtonUp($releaseHandler)
|
||
|
|
|
||
|
|
$resizeBottomLeft.Tag = 'BottomLeft'
|
||
|
|
$resizeBottomLeft.Add_PreviewMouseLeftButtonDown($resizeHandler)
|
||
|
|
$resizeBottomLeft.Add_MouseMove($moveHandler)
|
||
|
|
$resizeBottomLeft.Add_MouseLeftButtonUp($releaseHandler)
|
||
|
|
|
||
|
|
$resizeBottomRight.Tag = 'BottomRight'
|
||
|
|
$resizeBottomRight.Add_PreviewMouseLeftButtonDown($resizeHandler)
|
||
|
|
$resizeBottomRight.Add_MouseMove($moveHandler)
|
||
|
|
$resizeBottomRight.Add_MouseLeftButtonUp($releaseHandler)
|
||
|
|
|
||
|
|
# Integrated App Selection UI
|
||
|
|
$appsPanel = $window.FindName('AppSelectionPanel')
|
||
|
|
$onlyInstalledAppsBox = $window.FindName('OnlyInstalledAppsBox')
|
||
|
|
$loadingAppsIndicator = $window.FindName('LoadingAppsIndicator')
|
||
|
|
$appSelectionStatus = $window.FindName('AppSelectionStatus')
|
||
|
|
$defaultAppsBtn = $window.FindName('DefaultAppsBtn')
|
||
|
|
$clearAppSelectionBtn = $window.FindName('ClearAppSelectionBtn')
|
||
|
|
|
||
|
|
# 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
|
||
|
|
|
||
|
|
# Apply Tab UI Elements
|
||
|
|
$consoleOutput = $window.FindName('ConsoleOutput')
|
||
|
|
$consoleScrollViewer = $window.FindName('ConsoleScrollViewer')
|
||
|
|
$finishBtn = $window.FindName('FinishBtn')
|
||
|
|
$finishBtnText = $window.FindName('FinishBtnText')
|
||
|
|
|
||
|
|
# Set script-level variables for Write-ToConsole function
|
||
|
|
$script:GuiConsoleOutput = $consoleOutput
|
||
|
|
$script:GuiConsoleScrollViewer = $consoleScrollViewer
|
||
|
|
$script:GuiWindow = $window
|
||
|
|
|
||
|
|
# Updates app selection status text in the App Selection tab
|
||
|
|
function UpdateAppSelectionStatus {
|
||
|
|
$selectedCount = 0
|
||
|
|
foreach ($child in $appsPanel.Children) {
|
||
|
|
if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) {
|
||
|
|
$selectedCount++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
$appSelectionStatus.Text = "$selectedCount app(s) selected for removal"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Dynamically builds Tweaks UI from Features.json
|
||
|
|
function BuildDynamicTweaks {
|
||
|
|
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0"
|
||
|
|
|
||
|
|
if (-not $featuresJson) {
|
||
|
|
Show-MessageBox -Message "Unable to load Features.json file!" -Title "Error" -Button 'OK' -Icon 'Error' | Out-Null
|
||
|
|
Exit
|
||
|
|
}
|
||
|
|
|
||
|
|
# Column containers
|
||
|
|
$col0 = $window.FindName('Column0Panel')
|
||
|
|
$col1 = $window.FindName('Column1Panel')
|
||
|
|
$col2 = $window.FindName('Column2Panel')
|
||
|
|
$columns = @($col0, $col1, $col2) | Where-Object { $_ -ne $null }
|
||
|
|
|
||
|
|
# Clear all columns for fully dynamic panel creation
|
||
|
|
foreach ($col in $columns) {
|
||
|
|
if ($col) { $col.Children.Clear() }
|
||
|
|
}
|
||
|
|
|
||
|
|
$script:UiControlMappings = @{}
|
||
|
|
$script:CategoryCardMap = @{}
|
||
|
|
|
||
|
|
function CreateLabeledCombo($parent, $labelText, $comboName, $items) {
|
||
|
|
# If only 2 items (No Change + one option), use a checkbox instead
|
||
|
|
if ($items.Count -eq 2) {
|
||
|
|
$checkbox = New-Object System.Windows.Controls.CheckBox
|
||
|
|
$checkbox.Content = $labelText
|
||
|
|
$checkbox.Name = $comboName
|
||
|
|
$checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $labelText)
|
||
|
|
$checkbox.IsChecked = $false
|
||
|
|
$checkbox.Style = $window.Resources["FeatureCheckboxStyle"]
|
||
|
|
$parent.Children.Add($checkbox) | Out-Null
|
||
|
|
|
||
|
|
# Register the checkbox with the window's name scope
|
||
|
|
try {
|
||
|
|
[System.Windows.NameScope]::SetNameScope($checkbox, [System.Windows.NameScope]::GetNameScope($window))
|
||
|
|
$window.RegisterName($comboName, $checkbox)
|
||
|
|
}
|
||
|
|
catch {
|
||
|
|
# Name might already be registered, ignore
|
||
|
|
}
|
||
|
|
|
||
|
|
return $checkbox
|
||
|
|
}
|
||
|
|
|
||
|
|
# Otherwise use a combobox for multiple options
|
||
|
|
# Wrap label in a Border for search highlighting
|
||
|
|
$lblBorder = New-Object System.Windows.Controls.Border
|
||
|
|
$lblBorder.Style = $window.Resources['LabelBorderStyle']
|
||
|
|
$lblBorderName = "$comboName`_LabelBorder"
|
||
|
|
$lblBorder.Name = $lblBorderName
|
||
|
|
|
||
|
|
$lbl = New-Object System.Windows.Controls.TextBlock
|
||
|
|
$lbl.Text = $labelText
|
||
|
|
$lbl.Style = $window.Resources['LabelStyle']
|
||
|
|
$labelName = "$comboName`_Label"
|
||
|
|
$lbl.Name = $labelName
|
||
|
|
|
||
|
|
$lblBorder.Child = $lbl
|
||
|
|
$parent.Children.Add($lblBorder) | Out-Null
|
||
|
|
|
||
|
|
# Register the label border with the window's name scope
|
||
|
|
try {
|
||
|
|
[System.Windows.NameScope]::SetNameScope($lblBorder, [System.Windows.NameScope]::GetNameScope($window))
|
||
|
|
$window.RegisterName($lblBorderName, $lblBorder)
|
||
|
|
}
|
||
|
|
catch {
|
||
|
|
# Name might already be registered, ignore
|
||
|
|
}
|
||
|
|
|
||
|
|
$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 }
|
||
|
|
$combo.SelectedIndex = 0
|
||
|
|
$parent.Children.Add($combo) | Out-Null
|
||
|
|
|
||
|
|
# Register the combo box with the window's name scope
|
||
|
|
try {
|
||
|
|
[System.Windows.NameScope]::SetNameScope($combo, [System.Windows.NameScope]::GetNameScope($window))
|
||
|
|
$window.RegisterName($comboName, $combo)
|
||
|
|
}
|
||
|
|
catch {
|
||
|
|
# Name might already be registered, ignore
|
||
|
|
}
|
||
|
|
|
||
|
|
return $combo
|
||
|
|
}
|
||
|
|
|
||
|
|
function GetWikiUrlForCategory($category) {
|
||
|
|
if (-not $category) { return 'https://github.com/Raphire/Win11Debloat/wiki/Features' }
|
||
|
|
|
||
|
|
$slug = $category.ToLowerInvariant()
|
||
|
|
$slug = $slug -replace '&', ''
|
||
|
|
$slug = $slug -replace '[^a-z0-9\s-]', ''
|
||
|
|
$slug = $slug -replace '\s', '-'
|
||
|
|
|
||
|
|
return "https://github.com/Raphire/Win11Debloat/wiki/Features#$slug"
|
||
|
|
}
|
||
|
|
|
||
|
|
function GetOrCreateCategoryCard($categoryObj) {
|
||
|
|
$categoryName = $categoryObj.Name
|
||
|
|
$categoryIcon = $categoryObj.Icon
|
||
|
|
|
||
|
|
if ($script:CategoryCardMap.ContainsKey($categoryName)) { return $script:CategoryCardMap[$categoryName] }
|
||
|
|
|
||
|
|
# Create a new card Border + StackPanel and add to shortest column
|
||
|
|
$target = $columns | Sort-Object @{Expression={$_.Children.Count}; Ascending=$true}, @{Expression={$columns.IndexOf($_)}; Ascending=$true} | Select-Object -First 1
|
||
|
|
|
||
|
|
$border = New-Object System.Windows.Controls.Border
|
||
|
|
$border.Style = $window.Resources['CategoryCardBorderStyle']
|
||
|
|
$border.Tag = 'DynamicCategory'
|
||
|
|
|
||
|
|
$panel = New-Object System.Windows.Controls.StackPanel
|
||
|
|
$safe = ($categoryName -replace '[^a-zA-Z0-9_]','_')
|
||
|
|
$panel.Name = "Category_{0}_Panel" -f $safe
|
||
|
|
|
||
|
|
$headerRow = New-Object System.Windows.Controls.StackPanel
|
||
|
|
$headerRow.Orientation = 'Horizontal'
|
||
|
|
|
||
|
|
# Add category icon
|
||
|
|
$icon = New-Object System.Windows.Controls.TextBlock
|
||
|
|
# Convert HTML entity to character (e.g.,  -> actual character)
|
||
|
|
if ($categoryIcon -match '&#x([0-9A-Fa-f]+);') {
|
||
|
|
$hexValue = [Convert]::ToInt32($matches[1], 16)
|
||
|
|
$icon.Text = [char]$hexValue
|
||
|
|
}
|
||
|
|
$icon.Style = $window.Resources['CategoryHeaderIcon']
|
||
|
|
$headerRow.Children.Add($icon) | Out-Null
|
||
|
|
|
||
|
|
$header = New-Object System.Windows.Controls.TextBlock
|
||
|
|
$header.Text = $categoryName
|
||
|
|
$header.Style = $window.Resources['CategoryHeaderTextBlock']
|
||
|
|
$headerRow.Children.Add($header) | Out-Null
|
||
|
|
|
||
|
|
$helpIcon = New-Object System.Windows.Controls.TextBlock
|
||
|
|
$helpIcon.Text = '(?)'
|
||
|
|
$helpIcon.Style = $window.Resources['CategoryHelpLinkTextStyle']
|
||
|
|
|
||
|
|
$helpBtn = New-Object System.Windows.Controls.Button
|
||
|
|
$helpBtn.Content = $helpIcon
|
||
|
|
$helpBtn.ToolTip = "Open wiki for more info on '$categoryName' tweaks"
|
||
|
|
$helpBtn.Tag = (GetWikiUrlForCategory -category $categoryName)
|
||
|
|
$helpBtn.Style = $window.Resources['CategoryHelpLinkButtonStyle']
|
||
|
|
$helpBtn.Add_Click({
|
||
|
|
param($sender, $e)
|
||
|
|
if ($sender.Tag) { Start-Process $sender.Tag }
|
||
|
|
})
|
||
|
|
$headerRow.Children.Add($helpBtn) | Out-Null
|
||
|
|
|
||
|
|
$panel.Children.Add($headerRow) | Out-Null
|
||
|
|
|
||
|
|
$border.Child = $panel
|
||
|
|
$target.Children.Add($border) | Out-Null
|
||
|
|
|
||
|
|
$script:CategoryCardMap[$categoryName] = $panel
|
||
|
|
return $panel
|
||
|
|
}
|
||
|
|
|
||
|
|
# Determine categories present (from lists and features)
|
||
|
|
$categoriesPresent = @{}
|
||
|
|
if ($featuresJson.UiGroups) {
|
||
|
|
foreach ($g in $featuresJson.UiGroups) { if ($g.Category) { $categoriesPresent[$g.Category] = $true } }
|
||
|
|
}
|
||
|
|
foreach ($f in $featuresJson.Features) { if ($f.Category) { $categoriesPresent[$f.Category] = $true } }
|
||
|
|
|
||
|
|
# Create cards in the order defined in Features.json Categories (if present)
|
||
|
|
$orderedCategories = @()
|
||
|
|
if ($featuresJson.Categories) {
|
||
|
|
foreach ($c in $featuresJson.Categories) {
|
||
|
|
$categoryName = if ($c -is [string]) { $c } else { $c.Name }
|
||
|
|
if ($categoriesPresent.ContainsKey($categoryName)) {
|
||
|
|
# Store the full category object (or create one with default icon for string categories)
|
||
|
|
$categoryObj = if ($c -is [string]) { @{Name = $c; Icon = ''} } else { $c }
|
||
|
|
$orderedCategories += $categoryObj
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
# For backward compatibility, create category objects from keys
|
||
|
|
foreach ($catName in $categoriesPresent.Keys) {
|
||
|
|
$orderedCategories += @{Name = $catName; Icon = ''}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($categoryObj in $orderedCategories) {
|
||
|
|
$categoryName = $categoryObj.Name
|
||
|
|
|
||
|
|
# Create/get card for this category
|
||
|
|
$panel = GetOrCreateCategoryCard -categoryObj $categoryObj
|
||
|
|
if (-not $panel) { continue }
|
||
|
|
|
||
|
|
# Collect groups and features for this category, then sort by priority
|
||
|
|
$categoryItems = @()
|
||
|
|
|
||
|
|
# Add any groups for this category
|
||
|
|
if ($featuresJson.UiGroups) {
|
||
|
|
$groupIndex = 0
|
||
|
|
foreach ($group in $featuresJson.UiGroups) {
|
||
|
|
if ($group.Category -ne $categoryName) { $groupIndex++; continue }
|
||
|
|
$categoryItems += [PSCustomObject]@{
|
||
|
|
Type = 'group'
|
||
|
|
Data = $group
|
||
|
|
Priority = if ($null -ne $group.Priority) { $group.Priority } else { [int]::MaxValue }
|
||
|
|
OriginalIndex = $groupIndex
|
||
|
|
}
|
||
|
|
$groupIndex++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Add individual features for this category
|
||
|
|
$featureIndex = 0
|
||
|
|
foreach ($feature in $featuresJson.Features) {
|
||
|
|
if ($feature.Category -ne $categoryName) { $featureIndex++; continue }
|
||
|
|
|
||
|
|
# Check version and feature compatibility using Features.json
|
||
|
|
if (($feature.MinVersion -and $WinVersion -lt $feature.MinVersion) -or ($feature.MaxVersion -and $WinVersion -gt $feature.MaxVersion) -or ($feature.FeatureId -eq 'DisableModernStandbyNetworking' -and (-not $script:ModernStandbySupported))) {
|
||
|
|
$featureIndex++; continue
|
||
|
|
}
|
||
|
|
|
||
|
|
# Skip if feature part of a group
|
||
|
|
$inGroup = $false
|
||
|
|
if ($featuresJson.UiGroups) {
|
||
|
|
foreach ($g in $featuresJson.UiGroups) { foreach ($val in $g.Values) { if ($val.FeatureIds -contains $feature.FeatureId) { $inGroup = $true; break } }; if ($inGroup) { break } }
|
||
|
|
}
|
||
|
|
if ($inGroup) { $featureIndex++; continue }
|
||
|
|
|
||
|
|
$categoryItems += [PSCustomObject]@{
|
||
|
|
Type = 'feature'
|
||
|
|
Data = $feature
|
||
|
|
Priority = if ($null -ne $feature.Priority) { $feature.Priority } else { [int]::MaxValue }
|
||
|
|
OriginalIndex = $featureIndex
|
||
|
|
}
|
||
|
|
$featureIndex++
|
||
|
|
}
|
||
|
|
|
||
|
|
# Sort by priority first, then by original index for items with same/no priority
|
||
|
|
$sortedItems = $categoryItems | Sort-Object -Property Priority, OriginalIndex
|
||
|
|
|
||
|
|
# Render sorted items
|
||
|
|
foreach ($item in $sortedItems) {
|
||
|
|
if ($item.Type -eq 'group') {
|
||
|
|
$group = $item.Data
|
||
|
|
$items = @('No Change') + ($group.Values | ForEach-Object { $_.Label })
|
||
|
|
$comboName = 'Group_{0}Combo' -f $group.GroupId
|
||
|
|
$combo = CreateLabeledCombo -parent $panel -labelText $group.Label -comboName $comboName -items $items
|
||
|
|
# attach tooltip from UiGroups if present
|
||
|
|
if ($group.ToolTip) {
|
||
|
|
$tipBlock = New-Object System.Windows.Controls.TextBlock
|
||
|
|
$tipBlock.Text = $group.ToolTip
|
||
|
|
$tipBlock.TextWrapping = 'Wrap'
|
||
|
|
$tipBlock.MaxWidth = 420
|
||
|
|
$combo.ToolTip = $tipBlock
|
||
|
|
$lblBorderObj = $null
|
||
|
|
try { $lblBorderObj = $window.FindName("$comboName`_LabelBorder") } catch {}
|
||
|
|
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
|
||
|
|
}
|
||
|
|
$script:UiControlMappings[$comboName] = @{ Type='group'; Values = $group.Values; Label = $group.Label }
|
||
|
|
}
|
||
|
|
elseif ($item.Type -eq 'feature') {
|
||
|
|
$feature = $item.Data
|
||
|
|
$opt = 'Apply'
|
||
|
|
if ($feature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($feature.FeatureId -match '^Enable') { $opt = 'Enable' }
|
||
|
|
$items = @('No Change', $opt)
|
||
|
|
$comboName = ("Feature_{0}_Combo" -f $feature.FeatureId) -replace '[^a-zA-Z0-9_]',''
|
||
|
|
$combo = CreateLabeledCombo -parent $panel -labelText ($feature.Action + ' ' + $feature.Label) -comboName $comboName -items $items
|
||
|
|
# attach tooltip from Features.json if present
|
||
|
|
if ($feature.ToolTip) {
|
||
|
|
$tipBlock = New-Object System.Windows.Controls.TextBlock
|
||
|
|
$tipBlock.Text = $feature.ToolTip
|
||
|
|
$tipBlock.TextWrapping = 'Wrap'
|
||
|
|
$tipBlock.MaxWidth = 420
|
||
|
|
$combo.ToolTip = $tipBlock
|
||
|
|
$lblBorderObj = $null
|
||
|
|
try { $lblBorderObj = $window.FindName("$comboName`_LabelBorder") } catch {}
|
||
|
|
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
|
||
|
|
}
|
||
|
|
$script:UiControlMappings[$comboName] = @{ Type='feature'; FeatureId = $feature.FeatureId; Action = $feature.Action }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Helper function to complete app loading with the WinGet list
|
||
|
|
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 DisplayName | ForEach-Object {
|
||
|
|
$checkbox = New-Object System.Windows.Controls.CheckBox
|
||
|
|
$checkbox.Content = $_.DisplayName
|
||
|
|
$checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $_.DisplayName)
|
||
|
|
$checkbox.Tag = $_.AppId
|
||
|
|
$checkbox.IsChecked = $_.IsChecked
|
||
|
|
$checkbox.ToolTip = $_.Description
|
||
|
|
$checkbox.Style = $window.Resources["AppsPanelCheckBoxStyle"]
|
||
|
|
|
||
|
|
# Store metadata in checkbox for later use
|
||
|
|
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
|
||
|
|
}
|
||
|
|
|
||
|
|
# Hide loading indicator and navigation blocker, update status
|
||
|
|
$loadingAppsIndicator.Visibility = 'Collapsed'
|
||
|
|
|
||
|
|
UpdateAppSelectionStatus
|
||
|
|
}
|
||
|
|
|
||
|
|
# 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
|
||
|
|
$loadingAppsIndicator.Visibility = 'Visible'
|
||
|
|
$appsPanel.Children.Clear()
|
||
|
|
|
||
|
|
# 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
|
||
|
|
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
|
||
|
|
}) | Out-Null
|
||
|
|
}
|
||
|
|
|
||
|
|
# Event handlers for app selection
|
||
|
|
$onlyInstalledAppsBox.Add_Checked({
|
||
|
|
LoadAppsIntoMainUI
|
||
|
|
})
|
||
|
|
$onlyInstalledAppsBox.Add_Unchecked({
|
||
|
|
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
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
$clearAppSelectionBtn.Add_Click({
|
||
|
|
foreach ($child in $appsPanel.Children) {
|
||
|
|
if ($child -is [System.Windows.Controls.CheckBox]) {
|
||
|
|
$child.IsChecked = $false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
# Shared search highlighting configuration
|
||
|
|
$script:SearchHighlightColor = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFF4CE"))
|
||
|
|
$script:SearchHighlightColorDark = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#4A4A2A"))
|
||
|
|
|
||
|
|
# Helper function to get the appropriate highlight brush based on theme
|
||
|
|
function GetSearchHighlightBrush {
|
||
|
|
if ($usesDarkMode) { return $script:SearchHighlightColorDark }
|
||
|
|
return $script:SearchHighlightColor
|
||
|
|
}
|
||
|
|
|
||
|
|
# Helper function to scroll to an item if it's not visible, centering it in the viewport
|
||
|
|
function ScrollToItemIfNotVisible {
|
||
|
|
param (
|
||
|
|
[System.Windows.Controls.ScrollViewer]$scrollViewer,
|
||
|
|
[System.Windows.UIElement]$item,
|
||
|
|
[System.Windows.UIElement]$container
|
||
|
|
)
|
||
|
|
|
||
|
|
if (-not $scrollViewer -or -not $item -or -not $container) { return }
|
||
|
|
|
||
|
|
try {
|
||
|
|
$itemPosition = $item.TransformToAncestor($container).Transform([System.Windows.Point]::new(0, 0)).Y
|
||
|
|
$viewportHeight = $scrollViewer.ViewportHeight
|
||
|
|
$itemHeight = $item.ActualHeight
|
||
|
|
$currentOffset = $scrollViewer.VerticalOffset
|
||
|
|
|
||
|
|
# Check if the item is currently visible in the viewport
|
||
|
|
$itemTop = $itemPosition - $currentOffset
|
||
|
|
$itemBottom = $itemTop + $itemHeight
|
||
|
|
|
||
|
|
$isVisible = ($itemTop -ge 0) -and ($itemBottom -le $viewportHeight)
|
||
|
|
|
||
|
|
# Only scroll if the item is not visible
|
||
|
|
if (-not $isVisible) {
|
||
|
|
# Center the item in the viewport
|
||
|
|
$targetOffset = $itemPosition - ($viewportHeight / 2) + ($itemHeight / 2)
|
||
|
|
$scrollViewer.ScrollToVerticalOffset([Math]::Max(0, $targetOffset))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch {
|
||
|
|
# Fallback to simple bring into view
|
||
|
|
$item.BringIntoView()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Helper function to find the parent ScrollViewer of an element
|
||
|
|
function FindParentScrollViewer {
|
||
|
|
param ([System.Windows.UIElement]$element)
|
||
|
|
|
||
|
|
$parent = [System.Windows.Media.VisualTreeHelper]::GetParent($element)
|
||
|
|
while ($null -ne $parent) {
|
||
|
|
if ($parent -is [System.Windows.Controls.ScrollViewer]) {
|
||
|
|
return $parent
|
||
|
|
}
|
||
|
|
$parent = [System.Windows.Media.VisualTreeHelper]::GetParent($parent)
|
||
|
|
}
|
||
|
|
return $null
|
||
|
|
}
|
||
|
|
|
||
|
|
# App Search Box functionality
|
||
|
|
$appSearchBox = $window.FindName('AppSearchBox')
|
||
|
|
$appSearchPlaceholder = $window.FindName('AppSearchPlaceholder')
|
||
|
|
|
||
|
|
$appSearchBox.Add_TextChanged({
|
||
|
|
$searchText = $appSearchBox.Text.ToLower().Trim()
|
||
|
|
|
||
|
|
# Show/hide placeholder
|
||
|
|
$appSearchPlaceholder.Visibility = if ([string]::IsNullOrWhiteSpace($appSearchBox.Text)) { 'Visible' } else { 'Collapsed' }
|
||
|
|
|
||
|
|
# Clear all highlights first
|
||
|
|
foreach ($child in $appsPanel.Children) {
|
||
|
|
if ($child -is [System.Windows.Controls.CheckBox]) {
|
||
|
|
$child.Background = [System.Windows.Media.Brushes]::Transparent
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ([string]::IsNullOrWhiteSpace($searchText)) { return }
|
||
|
|
|
||
|
|
# Find and highlight all matching apps
|
||
|
|
$firstMatch = $null
|
||
|
|
$highlightBrush = GetSearchHighlightBrush
|
||
|
|
|
||
|
|
foreach ($child in $appsPanel.Children) {
|
||
|
|
if ($child -is [System.Windows.Controls.CheckBox] -and $child.Visibility -eq 'Visible') {
|
||
|
|
if ($child.Content.ToString().ToLower().Contains($searchText)) {
|
||
|
|
$child.Background = $highlightBrush
|
||
|
|
if ($null -eq $firstMatch) { $firstMatch = $child }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Scroll to first match if not visible
|
||
|
|
if ($firstMatch) {
|
||
|
|
$scrollViewer = FindParentScrollViewer -element $appsPanel
|
||
|
|
if ($scrollViewer) {
|
||
|
|
ScrollToItemIfNotVisible -scrollViewer $scrollViewer -item $firstMatch -container $appsPanel
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
# Tweak Search Box functionality
|
||
|
|
$tweakSearchBox = $window.FindName('TweakSearchBox')
|
||
|
|
$tweakSearchPlaceholder = $window.FindName('TweakSearchPlaceholder')
|
||
|
|
$tweakSearchBorder = $window.FindName('TweakSearchBorder')
|
||
|
|
$tweaksScrollViewer = $window.FindName('TweaksScrollViewer')
|
||
|
|
$tweaksGrid = $window.FindName('TweaksGrid')
|
||
|
|
$col0 = $window.FindName('Column0Panel')
|
||
|
|
$col1 = $window.FindName('Column1Panel')
|
||
|
|
$col2 = $window.FindName('Column2Panel')
|
||
|
|
|
||
|
|
# Monitor scrollbar visibility and adjust searchbar margin
|
||
|
|
$tweaksScrollViewer.Add_ScrollChanged({
|
||
|
|
if ($tweaksScrollViewer.ScrollableHeight -gt 0) {
|
||
|
|
# 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)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
# Helper function to clear all tweak highlights
|
||
|
|
function ClearTweakHighlights {
|
||
|
|
$columns = @($col0, $col1, $col2) | Where-Object { $_ -ne $null }
|
||
|
|
foreach ($column in $columns) {
|
||
|
|
foreach ($card in $column.Children) {
|
||
|
|
if ($card -is [System.Windows.Controls.Border] -and $card.Child -is [System.Windows.Controls.StackPanel]) {
|
||
|
|
foreach ($control in $card.Child.Children) {
|
||
|
|
if ($control -is [System.Windows.Controls.CheckBox] -or
|
||
|
|
($control -is [System.Windows.Controls.Border] -and $control.Name -like '*_LabelBorder')) {
|
||
|
|
$control.Background = [System.Windows.Media.Brushes]::Transparent
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Helper function to check if a ComboBox contains matching items
|
||
|
|
function ComboBoxContainsMatch {
|
||
|
|
param ([System.Windows.Controls.ComboBox]$comboBox, [string]$searchText)
|
||
|
|
|
||
|
|
foreach ($item in $comboBox.Items) {
|
||
|
|
$itemText = if ($item -is [System.Windows.Controls.ComboBoxItem]) { $item.Content.ToString().ToLower() } else { $item.ToString().ToLower() }
|
||
|
|
if ($itemText.Contains($searchText)) { return $true }
|
||
|
|
}
|
||
|
|
return $false
|
||
|
|
}
|
||
|
|
|
||
|
|
$tweakSearchBox.Add_TextChanged({
|
||
|
|
$searchText = $tweakSearchBox.Text.ToLower().Trim()
|
||
|
|
|
||
|
|
# Show/hide placeholder
|
||
|
|
$tweakSearchPlaceholder.Visibility = if ([string]::IsNullOrWhiteSpace($tweakSearchBox.Text)) { 'Visible' } else { 'Collapsed' }
|
||
|
|
|
||
|
|
# Clear all highlights
|
||
|
|
ClearTweakHighlights
|
||
|
|
|
||
|
|
if ([string]::IsNullOrWhiteSpace($searchText)) { return }
|
||
|
|
|
||
|
|
# Find and highlight all matching tweaks
|
||
|
|
$firstMatch = $null
|
||
|
|
$highlightBrush = GetSearchHighlightBrush
|
||
|
|
$columns = @($col0, $col1, $col2) | Where-Object { $_ -ne $null }
|
||
|
|
|
||
|
|
foreach ($column in $columns) {
|
||
|
|
foreach ($card in $column.Children) {
|
||
|
|
if ($card -is [System.Windows.Controls.Border] -and $card.Child -is [System.Windows.Controls.StackPanel]) {
|
||
|
|
$controlsList = @($card.Child.Children)
|
||
|
|
for ($i = 0; $i -lt $controlsList.Count; $i++) {
|
||
|
|
$control = $controlsList[$i]
|
||
|
|
$matchFound = $false
|
||
|
|
$controlToHighlight = $null
|
||
|
|
|
||
|
|
if ($control -is [System.Windows.Controls.CheckBox]) {
|
||
|
|
if ($control.Content.ToString().ToLower().Contains($searchText)) {
|
||
|
|
$matchFound = $true
|
||
|
|
$controlToHighlight = $control
|
||
|
|
}
|
||
|
|
}
|
||
|
|
elseif ($control -is [System.Windows.Controls.Border] -and $control.Name -like '*_LabelBorder') {
|
||
|
|
$labelText = if ($control.Child) { $control.Child.Text.ToLower() } else { "" }
|
||
|
|
$comboBox = if ($i + 1 -lt $controlsList.Count -and $controlsList[$i + 1] -is [System.Windows.Controls.ComboBox]) { $controlsList[$i + 1] } else { $null }
|
||
|
|
|
||
|
|
# Check label text or combo box items
|
||
|
|
if ($labelText.Contains($searchText) -or ($comboBox -and (ComboBoxContainsMatch -comboBox $comboBox -searchText $searchText))) {
|
||
|
|
$matchFound = $true
|
||
|
|
$controlToHighlight = $control
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($matchFound -and $controlToHighlight) {
|
||
|
|
$controlToHighlight.Background = $highlightBrush
|
||
|
|
if ($null -eq $firstMatch) { $firstMatch = $controlToHighlight }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Scroll to first match if not visible
|
||
|
|
if ($firstMatch -and $tweaksScrollViewer) {
|
||
|
|
ScrollToItemIfNotVisible -scrollViewer $tweaksScrollViewer -item $firstMatch -container $tweaksGrid
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
# Add Ctrl+F keyboard shortcut to focus search box on current tab
|
||
|
|
$window.Add_KeyDown({
|
||
|
|
param($sender, $e)
|
||
|
|
|
||
|
|
# Check if Ctrl+F was pressed
|
||
|
|
if ($e.Key -eq [System.Windows.Input.Key]::F -and
|
||
|
|
([System.Windows.Input.Keyboard]::Modifiers -band [System.Windows.Input.ModifierKeys]::Control)) {
|
||
|
|
|
||
|
|
$currentTab = $tabControl.SelectedItem
|
||
|
|
|
||
|
|
# Focus AppSearchBox if on App Removal tab
|
||
|
|
if ($currentTab.Header -eq "App Removal" -and $appSearchBox) {
|
||
|
|
$appSearchBox.Focus()
|
||
|
|
$e.Handled = $true
|
||
|
|
}
|
||
|
|
# Focus TweakSearchBox if on Tweaks tab
|
||
|
|
elseif ($currentTab.Header -eq "Tweaks" -and $tweakSearchBox) {
|
||
|
|
$tweakSearchBox.Focus()
|
||
|
|
$e.Handled = $true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
# Wizard Navigation
|
||
|
|
$tabControl = $window.FindName('MainTabControl')
|
||
|
|
$previousBtn = $window.FindName('PreviousBtn')
|
||
|
|
$nextBtn = $window.FindName('NextBtn')
|
||
|
|
$userSelectionCombo = $window.FindName('UserSelectionCombo')
|
||
|
|
$userSelectionDescription = $window.FindName('UserSelectionDescription')
|
||
|
|
$otherUserPanel = $window.FindName('OtherUserPanel')
|
||
|
|
$otherUsernameTextBox = $window.FindName('OtherUsernameTextBox')
|
||
|
|
$usernameTextBoxPlaceholder = $window.FindName('UsernameTextBoxPlaceholder')
|
||
|
|
$usernameValidationMessage = $window.FindName('UsernameValidationMessage')
|
||
|
|
$appRemovalScopeCombo = $window.FindName('AppRemovalScopeCombo')
|
||
|
|
$appRemovalScopeDescription = $window.FindName('AppRemovalScopeDescription')
|
||
|
|
$appRemovalScopeSection = $window.FindName('AppRemovalScopeSection')
|
||
|
|
$appRemovalScopeCurrentUser = $window.FindName('AppRemovalScopeCurrentUser')
|
||
|
|
$appRemovalScopeTargetUser = $window.FindName('AppRemovalScopeTargetUser')
|
||
|
|
|
||
|
|
# Navigation button handlers
|
||
|
|
function UpdateNavigationButtons {
|
||
|
|
$currentIndex = $tabControl.SelectedIndex
|
||
|
|
$totalTabs = $tabControl.Items.Count
|
||
|
|
|
||
|
|
$homeIndex = 0
|
||
|
|
$overviewIndex = $totalTabs - 2
|
||
|
|
$applyIndex = $totalTabs - 1
|
||
|
|
|
||
|
|
# Navigation button visibility
|
||
|
|
if ($currentIndex -eq $homeIndex) {
|
||
|
|
$nextBtn.Visibility = 'Collapsed'
|
||
|
|
$previousBtn.Visibility = 'Collapsed'
|
||
|
|
} elseif ($currentIndex -eq $overviewIndex) {
|
||
|
|
$nextBtn.Visibility = 'Collapsed'
|
||
|
|
$previousBtn.Visibility = 'Visible'
|
||
|
|
} elseif ($currentIndex -eq $applyIndex) {
|
||
|
|
$nextBtn.Visibility = 'Collapsed'
|
||
|
|
$previousBtn.Visibility = 'Collapsed'
|
||
|
|
} else {
|
||
|
|
$nextBtn.Visibility = 'Visible'
|
||
|
|
$previousBtn.Visibility = 'Visible'
|
||
|
|
}
|
||
|
|
|
||
|
|
# Update progress indicators
|
||
|
|
# Tab indices: 0=Home, 1=App Removal, 2=Tweaks, 3=Overview, 4=Apply
|
||
|
|
$blueColor = "#0067c0"
|
||
|
|
$greyColor = "#808080"
|
||
|
|
|
||
|
|
$progressIndicator1 = $window.FindName('ProgressIndicator1') # App Removal
|
||
|
|
$progressIndicator2 = $window.FindName('ProgressIndicator2') # Tweaks
|
||
|
|
$progressIndicator3 = $window.FindName('ProgressIndicator3') # Overview
|
||
|
|
$bottomNavGrid = $window.FindName('BottomNavGrid')
|
||
|
|
|
||
|
|
# Hide bottom navigation on home page and apply tab
|
||
|
|
if ($currentIndex -eq 0 -or $currentIndex -eq $applyIndex) {
|
||
|
|
$bottomNavGrid.Visibility = 'Collapsed'
|
||
|
|
} else {
|
||
|
|
$bottomNavGrid.Visibility = 'Visible'
|
||
|
|
}
|
||
|
|
|
||
|
|
# Update indicator colors based on current tab
|
||
|
|
# Indicator 1 (App Removal) - tab index 1
|
||
|
|
if ($currentIndex -ge 1) {
|
||
|
|
$progressIndicator1.Fill = $blueColor
|
||
|
|
} else {
|
||
|
|
$progressIndicator1.Fill = $greyColor
|
||
|
|
}
|
||
|
|
|
||
|
|
# Indicator 2 (Tweaks) - tab index 2
|
||
|
|
if ($currentIndex -ge 2) {
|
||
|
|
$progressIndicator2.Fill = $blueColor
|
||
|
|
} else {
|
||
|
|
$progressIndicator2.Fill = $greyColor
|
||
|
|
}
|
||
|
|
|
||
|
|
# Indicator 3 (Overview) - tab index 3
|
||
|
|
if ($currentIndex -ge 3) {
|
||
|
|
$progressIndicator3.Fill = $blueColor
|
||
|
|
} else {
|
||
|
|
$progressIndicator3.Fill = $greyColor
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Update user selection description and show/hide other user panel
|
||
|
|
$userSelectionCombo.Add_SelectionChanged({
|
||
|
|
switch ($userSelectionCombo.SelectedIndex) {
|
||
|
|
0 {
|
||
|
|
$userSelectionDescription.Text = "Changes will be applied to the currently logged-in user profile."
|
||
|
|
$otherUserPanel.Visibility = 'Collapsed'
|
||
|
|
$usernameValidationMessage.Text = ""
|
||
|
|
# Show "Current user only" option, hide "Target user only" option
|
||
|
|
$appRemovalScopeCurrentUser.Visibility = 'Visible'
|
||
|
|
$appRemovalScopeTargetUser.Visibility = 'Collapsed'
|
||
|
|
# Enable app removal scope selection for current user
|
||
|
|
$appRemovalScopeCombo.IsEnabled = $true
|
||
|
|
$appRemovalScopeCombo.SelectedIndex = 0
|
||
|
|
}
|
||
|
|
1 {
|
||
|
|
$userSelectionDescription.Text = "Changes will be applied to a different user profile on this system."
|
||
|
|
$otherUserPanel.Visibility = 'Visible'
|
||
|
|
$usernameValidationMessage.Text = ""
|
||
|
|
# Hide "Current user only" option, show "Target user only" option
|
||
|
|
$appRemovalScopeCurrentUser.Visibility = 'Collapsed'
|
||
|
|
$appRemovalScopeTargetUser.Visibility = 'Visible'
|
||
|
|
# Enable app removal scope selection for other user
|
||
|
|
$appRemovalScopeCombo.IsEnabled = $true
|
||
|
|
$appRemovalScopeCombo.SelectedIndex = 0
|
||
|
|
}
|
||
|
|
2 {
|
||
|
|
$userSelectionDescription.Text = "Changes will be applied to the default user template, affecting all new users created after this point. Useful for Sysprep deployment."
|
||
|
|
$otherUserPanel.Visibility = 'Collapsed'
|
||
|
|
$usernameValidationMessage.Text = ""
|
||
|
|
# Hide other user options since they don't apply to default user template
|
||
|
|
$appRemovalScopeCurrentUser.Visibility = 'Collapsed'
|
||
|
|
$appRemovalScopeTargetUser.Visibility = 'Collapsed'
|
||
|
|
# Lock app removal scope to "All users" when applying to sysprep
|
||
|
|
$appRemovalScopeCombo.IsEnabled = $false
|
||
|
|
$appRemovalScopeCombo.SelectedIndex = 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
# Helper function to update app removal scope description
|
||
|
|
function UpdateAppRemovalScopeDescription {
|
||
|
|
$selectedItem = $appRemovalScopeCombo.SelectedItem
|
||
|
|
if ($selectedItem) {
|
||
|
|
switch ($selectedItem.Content) {
|
||
|
|
"All users" {
|
||
|
|
$appRemovalScopeDescription.Text = "Apps will be removed for all users and from the Windows image to prevent reinstallation for new users."
|
||
|
|
}
|
||
|
|
"Current user only" {
|
||
|
|
$appRemovalScopeDescription.Text = "Apps will only be removed for the current user. Other users and new users will not be affected."
|
||
|
|
}
|
||
|
|
"Target user only" {
|
||
|
|
$appRemovalScopeDescription.Text = "Apps will only be removed for the specified target user. Other users and new users will not be affected."
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Update app removal scope description
|
||
|
|
$appRemovalScopeCombo.Add_SelectionChanged({
|
||
|
|
UpdateAppRemovalScopeDescription
|
||
|
|
})
|
||
|
|
|
||
|
|
$otherUsernameTextBox.Add_TextChanged({
|
||
|
|
# Show/hide placeholder
|
||
|
|
if ([string]::IsNullOrWhiteSpace($otherUsernameTextBox.Text)) {
|
||
|
|
$usernameTextBoxPlaceholder.Visibility = 'Visible'
|
||
|
|
} else {
|
||
|
|
$usernameTextBoxPlaceholder.Visibility = 'Collapsed'
|
||
|
|
}
|
||
|
|
|
||
|
|
ValidateOtherUsername
|
||
|
|
})
|
||
|
|
|
||
|
|
function ValidateOtherUsername {
|
||
|
|
# Only validate if "Other User" is selected
|
||
|
|
if ($userSelectionCombo.SelectedIndex -ne 1) {
|
||
|
|
return $true
|
||
|
|
}
|
||
|
|
|
||
|
|
$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"))
|
||
|
|
|
||
|
|
if ($username.Length -eq 0) {
|
||
|
|
$usernameValidationMessage.Text = "[X] Please enter a username"
|
||
|
|
$usernameValidationMessage.Foreground = $errorBrush
|
||
|
|
return $false
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($username -eq $env:USERNAME) {
|
||
|
|
$usernameValidationMessage.Text = "[X] Cannot enter your own username, use 'Current User' option instead"
|
||
|
|
$usernameValidationMessage.Foreground = $errorBrush
|
||
|
|
return $false
|
||
|
|
}
|
||
|
|
|
||
|
|
$userExists = CheckIfUserExists -Username $username
|
||
|
|
|
||
|
|
if ($userExists) {
|
||
|
|
$usernameValidationMessage.Text = "[OK] User found: $username"
|
||
|
|
$usernameValidationMessage.Foreground = $successBrush
|
||
|
|
return $true
|
||
|
|
}
|
||
|
|
|
||
|
|
$usernameValidationMessage.Text = "[X] User not found, please enter a valid username"
|
||
|
|
$usernameValidationMessage.Foreground = $errorBrush
|
||
|
|
return $false
|
||
|
|
}
|
||
|
|
|
||
|
|
function GenerateOverview {
|
||
|
|
# Load Features.json
|
||
|
|
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0"
|
||
|
|
$overviewChangesPanel = $window.FindName('OverviewChangesPanel')
|
||
|
|
$overviewChangesPanel.Children.Clear()
|
||
|
|
|
||
|
|
$changesList = @()
|
||
|
|
|
||
|
|
# Collect selected apps
|
||
|
|
$selectedAppsCount = 0
|
||
|
|
foreach ($child in $appsPanel.Children) {
|
||
|
|
if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) {
|
||
|
|
$selectedAppsCount++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if ($selectedAppsCount -gt 0) {
|
||
|
|
$changesList += "Remove $selectedAppsCount selected application(s)"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Update app removal scope section based on whether apps are selected
|
||
|
|
if ($selectedAppsCount -gt 0) {
|
||
|
|
# Enable app removal scope selection (unless locked by sysprep mode)
|
||
|
|
if ($userSelectionCombo.SelectedIndex -ne 2) {
|
||
|
|
$appRemovalScopeCombo.IsEnabled = $true
|
||
|
|
}
|
||
|
|
$appRemovalScopeSection.Opacity = 1.0
|
||
|
|
UpdateAppRemovalScopeDescription
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
# Disable app removal scope selection when no apps selected
|
||
|
|
$appRemovalScopeCombo.IsEnabled = $false
|
||
|
|
$appRemovalScopeSection.Opacity = 0.5
|
||
|
|
$appRemovalScopeDescription.Text = "No apps selected for removal."
|
||
|
|
}
|
||
|
|
|
||
|
|
# Collect all ComboBox/CheckBox selections from dynamically created controls
|
||
|
|
if ($script:UiControlMappings) {
|
||
|
|
foreach ($mappingKey in $script:UiControlMappings.Keys) {
|
||
|
|
$control = $window.FindName($mappingKey)
|
||
|
|
$isSelected = $false
|
||
|
|
|
||
|
|
# Check if it's a checkbox or combobox
|
||
|
|
if ($control -is [System.Windows.Controls.CheckBox]) {
|
||
|
|
$isSelected = $control.IsChecked -eq $true
|
||
|
|
}
|
||
|
|
elseif ($control -is [System.Windows.Controls.ComboBox]) {
|
||
|
|
$isSelected = $control.SelectedIndex -gt 0
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($control -and $isSelected) {
|
||
|
|
$mapping = $script:UiControlMappings[$mappingKey]
|
||
|
|
if ($mapping.Type -eq 'group') {
|
||
|
|
# 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) }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
elseif ($mapping.Type -eq 'feature') {
|
||
|
|
$feature = $featuresJson.Features | Where-Object { $_.FeatureId -eq $mapping.FeatureId }
|
||
|
|
if ($feature) { $changesList += ($feature.Action + ' ' + $feature.Label) }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($changesList.Count -eq 0) {
|
||
|
|
$textBlock = New-Object System.Windows.Controls.TextBlock
|
||
|
|
$textBlock.Text = "No changes selected"
|
||
|
|
$textBlock.Style = $window.Resources["OverviewNoChangesTextStyle"]
|
||
|
|
$overviewChangesPanel.Children.Add($textBlock) | Out-Null
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
foreach ($change in $changesList) {
|
||
|
|
$bullet = New-Object System.Windows.Controls.TextBlock
|
||
|
|
$bullet.Text = "- $change"
|
||
|
|
$bullet.Style = $window.Resources["OverviewChangeBulletStyle"]
|
||
|
|
$overviewChangesPanel.Children.Add($bullet) | Out-Null
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$previousBtn.Add_Click({
|
||
|
|
if ($tabControl.SelectedIndex -gt 0) {
|
||
|
|
$tabControl.SelectedIndex--
|
||
|
|
UpdateNavigationButtons
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
$nextBtn.Add_Click({
|
||
|
|
if ($tabControl.SelectedIndex -lt ($tabControl.Items.Count - 1)) {
|
||
|
|
$tabControl.SelectedIndex++
|
||
|
|
|
||
|
|
UpdateNavigationButtons
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
# Handle Home Start button
|
||
|
|
$homeStartBtn = $window.FindName('HomeStartBtn')
|
||
|
|
$homeStartBtn.Add_Click({
|
||
|
|
# Navigate to first tab after home (App Removal)
|
||
|
|
$tabControl.SelectedIndex = 1
|
||
|
|
UpdateNavigationButtons
|
||
|
|
})
|
||
|
|
|
||
|
|
# Handle Overview Apply Changes button - validates and immediately starts applying changes
|
||
|
|
$overviewApplyBtn = $window.FindName('OverviewApplyBtn')
|
||
|
|
$overviewApplyBtn.Add_Click({
|
||
|
|
if (-not (ValidateOtherUsername)) {
|
||
|
|
Show-MessageBox -Message "Please enter a valid username." -Title "Invalid Username" -Button 'OK' -Icon 'Warning' | Out-Null
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
# App Removal - collect selected apps from integrated UI
|
||
|
|
$selectedApps = @()
|
||
|
|
foreach ($child in $appsPanel.Children) {
|
||
|
|
if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) {
|
||
|
|
$selectedApps += $child.Tag
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($selectedApps.Count -gt 0) {
|
||
|
|
# Check if Microsoft Store is selected
|
||
|
|
if ($selectedApps -contains "Microsoft.WindowsStore") {
|
||
|
|
$result = Show-MessageBox -Message 'Are you sure you wish to uninstall the Microsoft Store? This app cannot easily be reinstalled.' -Title 'Are you sure?' -Button 'YesNo' -Icon 'Warning'
|
||
|
|
|
||
|
|
if ($result -eq 'No') {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
AddParameter 'RemoveApps'
|
||
|
|
AddParameter 'Apps' ($selectedApps -join ',')
|
||
|
|
|
||
|
|
# Add app removal target parameter based on selection
|
||
|
|
$selectedScopeItem = $appRemovalScopeCombo.SelectedItem
|
||
|
|
if ($selectedScopeItem) {
|
||
|
|
switch ($selectedScopeItem.Content) {
|
||
|
|
"All users" {
|
||
|
|
AddParameter 'AppRemovalTarget' 'AllUsers'
|
||
|
|
}
|
||
|
|
"Current user only" {
|
||
|
|
AddParameter 'AppRemovalTarget' 'CurrentUser'
|
||
|
|
}
|
||
|
|
"Target user only" {
|
||
|
|
# Use the target username from Other User panel
|
||
|
|
AddParameter 'AppRemovalTarget' ($otherUsernameTextBox.Text.Trim())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Apply dynamic tweaks selections
|
||
|
|
if ($script:UiControlMappings) {
|
||
|
|
foreach ($mappingKey in $script:UiControlMappings.Keys) {
|
||
|
|
$control = $window.FindName($mappingKey)
|
||
|
|
$isSelected = $false
|
||
|
|
$selectedIndex = 0
|
||
|
|
|
||
|
|
# Check if it's a checkbox or combobox
|
||
|
|
if ($control -is [System.Windows.Controls.CheckBox]) {
|
||
|
|
$isSelected = $control.IsChecked -eq $true
|
||
|
|
$selectedIndex = if ($isSelected) { 1 } else { 0 }
|
||
|
|
}
|
||
|
|
elseif ($control -is [System.Windows.Controls.ComboBox]) {
|
||
|
|
$isSelected = $control.SelectedIndex -gt 0
|
||
|
|
$selectedIndex = $control.SelectedIndex
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($control -and $isSelected) {
|
||
|
|
$mapping = $script:UiControlMappings[$mappingKey]
|
||
|
|
if ($mapping.Type -eq 'group') {
|
||
|
|
if ($selectedIndex -gt 0 -and $selectedIndex -le $mapping.Values.Count) {
|
||
|
|
$selectedValue = $mapping.Values[$selectedIndex - 1]
|
||
|
|
foreach ($fid in $selectedValue.FeatureIds) {
|
||
|
|
AddParameter $fid
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
elseif ($mapping.Type -eq 'feature') {
|
||
|
|
AddParameter $mapping.FeatureId
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$controlParamsCount = 0
|
||
|
|
foreach ($Param in $script:ControlParams) {
|
||
|
|
if ($script:Params.ContainsKey($Param)) {
|
||
|
|
$controlParamsCount++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Check if any changes were selected
|
||
|
|
$totalChanges = $script:Params.Count - $controlParamsCount
|
||
|
|
|
||
|
|
# Apps parameter does not count as a change itself
|
||
|
|
if ($script:Params.ContainsKey('Apps')) {
|
||
|
|
$totalChanges = $totalChanges - 1
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($totalChanges -eq 0) {
|
||
|
|
Show-MessageBox -Message 'No changes have been selected, please select at least one option to proceed.' -Title 'No Changes Selected' -Button 'OK' -Icon 'Information'
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
# Check RestorePointCheckBox
|
||
|
|
$restorePointCheckBox = $window.FindName('RestorePointCheckBox')
|
||
|
|
if ($restorePointCheckBox -and $restorePointCheckBox.IsChecked) {
|
||
|
|
AddParameter 'CreateRestorePoint'
|
||
|
|
}
|
||
|
|
|
||
|
|
# Store selected user mode
|
||
|
|
switch ($userSelectionCombo.SelectedIndex) {
|
||
|
|
1 { AddParameter User ($otherUsernameTextBox.Text.Trim()) }
|
||
|
|
2 { AddParameter Sysprep }
|
||
|
|
}
|
||
|
|
|
||
|
|
SaveSettings
|
||
|
|
|
||
|
|
# Navigate to Apply tab (last tab) and start applying changes
|
||
|
|
$tabControl.SelectedIndex = $tabControl.Items.Count - 1
|
||
|
|
|
||
|
|
# Clear console and set initial status
|
||
|
|
$consoleOutput.Text = ""
|
||
|
|
|
||
|
|
Write-ToConsole "Applying changes to $(if ($script:Params.ContainsKey("Sysprep")) { "default user template" } else { "user $(GetUserName)" })"
|
||
|
|
Write-ToConsole "Total changes to apply: $totalChanges"
|
||
|
|
Write-ToConsole ""
|
||
|
|
|
||
|
|
# Run changes in background to keep UI responsive
|
||
|
|
$window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{
|
||
|
|
try {
|
||
|
|
ExecuteAllChanges
|
||
|
|
|
||
|
|
# Check if user wants to restart explorer (from checkbox)
|
||
|
|
$restartExplorerCheckBox = $window.FindName('RestartExplorerCheckBox')
|
||
|
|
if ($restartExplorerCheckBox -and $restartExplorerCheckBox.IsChecked -and -not $script:CancelRequested) {
|
||
|
|
RestartExplorer
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-ToConsole ""
|
||
|
|
if ($script:CancelRequested) {
|
||
|
|
Write-ToConsole "Script execution was cancelled by the user. Some changes may not have been applied."
|
||
|
|
} else {
|
||
|
|
Write-ToConsole "All changes have been applied. Please check the output above for any errors."
|
||
|
|
}
|
||
|
|
|
||
|
|
$finishBtn.Dispatcher.Invoke([action]{
|
||
|
|
$finishBtn.IsEnabled = $true
|
||
|
|
$finishBtnText.Text = "Close Win11Debloat"
|
||
|
|
})
|
||
|
|
}
|
||
|
|
catch {
|
||
|
|
Write-ToConsole "Error: $($_.Exception.Message)"
|
||
|
|
$finishBtn.Dispatcher.Invoke([action]{
|
||
|
|
$finishBtn.IsEnabled = $true
|
||
|
|
$finishBtnText.Text = "Close Win11Debloat"
|
||
|
|
})
|
||
|
|
}
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
# Initialize UI elements on window load
|
||
|
|
$window.Add_Loaded({
|
||
|
|
BuildDynamicTweaks
|
||
|
|
|
||
|
|
LoadAppsIntoMainUI
|
||
|
|
|
||
|
|
# Update Current User label with username
|
||
|
|
if ($userSelectionCombo -and $userSelectionCombo.Items.Count -gt 0) {
|
||
|
|
$currentUserItem = $userSelectionCombo.Items[0]
|
||
|
|
if ($currentUserItem -is [System.Windows.Controls.ComboBoxItem]) {
|
||
|
|
$currentUserItem.Content = "Current User ($(GetUserName))"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Disable Restart Explorer option if NoRestartExplorer parameter is set
|
||
|
|
$restartExplorerCheckBox = $window.FindName('RestartExplorerCheckBox')
|
||
|
|
if ($restartExplorerCheckBox -and $script:Params.ContainsKey("NoRestartExplorer")) {
|
||
|
|
$restartExplorerCheckBox.IsChecked = $false
|
||
|
|
$restartExplorerCheckBox.IsEnabled = $false
|
||
|
|
}
|
||
|
|
|
||
|
|
# Force Apply Changes To setting if Sysprep or User parameters are set
|
||
|
|
if ($script:Params.ContainsKey("Sysprep")) {
|
||
|
|
$userSelectionCombo.SelectedIndex = 2
|
||
|
|
$userSelectionCombo.IsEnabled = $false
|
||
|
|
}
|
||
|
|
elseif ($script:Params.ContainsKey("User")) {
|
||
|
|
$userSelectionCombo.SelectedIndex = 1
|
||
|
|
$userSelectionCombo.IsEnabled = $false
|
||
|
|
$otherUsernameTextBox.Text = $script:Params.Item("User")
|
||
|
|
$otherUsernameTextBox.IsEnabled = $false
|
||
|
|
}
|
||
|
|
|
||
|
|
UpdateNavigationButtons
|
||
|
|
})
|
||
|
|
|
||
|
|
# Add event handler for tab changes
|
||
|
|
$tabControl.Add_SelectionChanged({
|
||
|
|
# Regenerate overview when switching to Overview tab
|
||
|
|
if ($tabControl.SelectedIndex -eq ($tabControl.Items.Count - 2)) {
|
||
|
|
GenerateOverview
|
||
|
|
}
|
||
|
|
UpdateNavigationButtons
|
||
|
|
})
|
||
|
|
|
||
|
|
# Handle Load Defaults button
|
||
|
|
$loadDefaultsBtn = $window.FindName('LoadDefaultsBtn')
|
||
|
|
$loadDefaultsBtn.Add_Click({
|
||
|
|
$defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
|
||
|
|
|
||
|
|
if (-not $defaultsJson) {
|
||
|
|
Show-MessageBox -Message "Failed to load default settings file" -Title "Error" -Button 'OK' -Icon 'Error'
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
ApplySettingsToUiControls -window $window -settingsJson $defaultsJson -uiControlMappings $script:UiControlMappings
|
||
|
|
})
|
||
|
|
|
||
|
|
# 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
|
||
|
|
|
||
|
|
$hasSettings = $false
|
||
|
|
$appsSetting = $null
|
||
|
|
if ($lastUsedSettingsJson -and $lastUsedSettingsJson.Settings) {
|
||
|
|
foreach ($s in $lastUsedSettingsJson.Settings) {
|
||
|
|
# Only count as hasSettings if a setting other than RemoveApps/Apps is present and true
|
||
|
|
if ($s.Value -eq $true -and $s.Name -ne 'RemoveApps' -and $s.Name -ne 'Apps') { $hasSettings = $true }
|
||
|
|
if ($s.Name -eq 'Apps' -and $s.Value) { $appsSetting = $s.Value }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Show option to load last used settings if they exist
|
||
|
|
if ($hasSettings) {
|
||
|
|
$loadLastUsedBtn.Add_Click({
|
||
|
|
try {
|
||
|
|
ApplySettingsToUiControls -window $window -settingsJson $lastUsedSettingsJson -uiControlMappings $script:UiControlMappings
|
||
|
|
}
|
||
|
|
catch {
|
||
|
|
Show-MessageBox -Message "Failed to load last used settings: $_" -Title "Error" -Button 'OK' -Icon 'Error'
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$loadLastUsedBtn.Visibility = 'Collapsed'
|
||
|
|
}
|
||
|
|
|
||
|
|
# Show option to load last used apps if they exist
|
||
|
|
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 '' }
|
||
|
|
|
||
|
|
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'
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$loadLastUsedAppsBtn.Visibility = 'Collapsed'
|
||
|
|
}
|
||
|
|
|
||
|
|
# Clear All Tweaks button
|
||
|
|
$clearAllTweaksBtn = $window.FindName('ClearAllTweaksBtn')
|
||
|
|
$clearAllTweaksBtn.Add_Click({
|
||
|
|
# Reset all ComboBoxes to index 0 (No Change) and uncheck all CheckBoxes
|
||
|
|
if ($script:UiControlMappings) {
|
||
|
|
foreach ($comboName in $script: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
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Also uncheck RestorePointCheckBox
|
||
|
|
$restorePointCheckBox = $window.FindName('RestorePointCheckBox')
|
||
|
|
if ($restorePointCheckBox) {
|
||
|
|
$restorePointCheckBox.IsChecked = $false
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
# Finish (Close Win11Debloat) button handler
|
||
|
|
$finishBtn.Add_Click({
|
||
|
|
$window.Close()
|
||
|
|
})
|
||
|
|
|
||
|
|
# Show the window
|
||
|
|
return $window.ShowDialog()
|
||
|
|
}
|