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:
487
Config/Apps.json
487
Config/Apps.json
File diff suppressed because it is too large
Load Diff
66
README.md
66
README.md
@@ -97,23 +97,23 @@ Below is an overview of the key features and functionality offered by Win11Deblo
|
|||||||
#### AI Features
|
#### AI Features
|
||||||
|
|
||||||
- Disable & remove Microsoft Copilot.
|
- Disable & remove Microsoft Copilot.
|
||||||
- Disable Windows Recall. (W11 only)
|
- Disable Windows Recall.
|
||||||
- Disable Click to Do, AI text & image analysis tool. (W11 only)
|
- Disable Click to Do, AI text & image analysis tool.
|
||||||
- Prevent AI service (WSAIFabricSvc) from starting automatically. (W11 only)
|
- Prevent AI service (WSAIFabricSvc) from starting automatically.
|
||||||
- Disable AI Features in Edge. (W11 only)
|
- Disable AI Features in Edge.
|
||||||
- Disable AI Features in Paint. (W11 only)
|
- Disable AI Features in Paint.
|
||||||
- Disable AI Features in Notepad. (W11 only)
|
- Disable AI Features in Notepad.
|
||||||
|
|
||||||
#### System
|
#### System
|
||||||
|
|
||||||
- Disable the Drag Tray for sharing & moving files. (W11 only)
|
- Disable the Drag Tray for sharing & moving files.
|
||||||
- Restore the old Windows 10 style context menu. (W11 only)
|
- Restore the old Windows 10 style context menu.
|
||||||
- Turn off Enhance Pointer Precision, also known as mouse acceleration.
|
- Turn off Enhance Pointer Precision, also known as mouse acceleration.
|
||||||
- Disable the Sticky Keys keyboard shortcut. (W11 only)
|
- Disable the Sticky Keys keyboard shortcut.
|
||||||
- Disable Storage Sense automatic disk cleanup.
|
- Disable Storage Sense automatic disk cleanup.
|
||||||
- Disable fast start-up to ensure a full shutdown.
|
- Disable fast start-up to ensure a full shutdown.
|
||||||
- Disable BitLocker automatic device encryption.
|
- Disable BitLocker automatic device encryption.
|
||||||
- Disable network connectivity during Modern Standby to reduce battery drain. (W11 only)
|
- Disable network connectivity during Modern Standby to reduce battery drain.
|
||||||
|
|
||||||
#### Windows Update
|
#### Windows Update
|
||||||
|
|
||||||
@@ -129,49 +129,49 @@ Below is an overview of the key features and functionality offered by Win11Deblo
|
|||||||
|
|
||||||
#### Start Menu & Search
|
#### Start Menu & Search
|
||||||
|
|
||||||
- Remove or replace all pinned apps from the start menu. (W11 only)
|
- Remove or replace all pinned apps from the start menu.
|
||||||
- Hide the recommended section in the start menu. (W11 only)
|
- Hide the recommended section in the start menu.
|
||||||
- Hide the 'All Apps' section in the start menu. (W11 only)
|
- Hide the 'All Apps' section in the start menu.
|
||||||
- Disable the Phone Link mobile devices integration in the start menu. (W11 only)
|
- Disable the Phone Link mobile devices integration in the start menu.
|
||||||
- Disable Bing web search & Copilot integration in Windows search.
|
- Disable Bing web search & Copilot integration in Windows search.
|
||||||
- Disable Microsoft Store app suggestions in Windows search. (W11 only)
|
- Disable Microsoft Store app suggestions in Windows search.
|
||||||
- Disable Search Highlights (dynamic/branded content) in the taskbar search box. (W11 only)
|
- Disable Search Highlights (dynamic/branded content) in the taskbar search box.
|
||||||
- Disable local Windows search history.
|
- Disable local Windows search history.
|
||||||
|
|
||||||
#### Taskbar
|
#### Taskbar
|
||||||
|
|
||||||
- Align taskbar icons to the left. (W11 only)
|
- Align taskbar icons to the left.
|
||||||
- Hide or change the search icon/box on the taskbar. (W11 only)
|
- Hide or change the search icon/box on the taskbar.
|
||||||
- Hide the taskview button from the taskbar. (W11 only)
|
- Hide the taskview button from the taskbar.
|
||||||
- Disable widgets on the taskbar & lock screen.
|
- Disable widgets on the taskbar & lock screen.
|
||||||
- Hide the chat (meet now) icon from the taskbar. (W10 only)
|
- Hide the chat (meet now) icon from the taskbar.
|
||||||
- Enable the 'End Task' option in the taskbar right click menu. (W11 only)
|
- Enable the 'End Task' option in the taskbar right click menu.
|
||||||
- Enable the 'Last Active Click' behavior in the taskbar app area. This allows you to repeatedly click on an application's icon in the taskbar to switch focus between the open windows of that application.
|
- Enable the 'Last Active Click' behavior in the taskbar app area. This allows you to repeatedly click on an application's icon in the taskbar to switch focus between the open windows of that application.
|
||||||
- Choose how app icons are shown on the taskbar when using multiple monitors. (W11 only)
|
- Choose how app icons are shown on the taskbar when using multiple monitors.
|
||||||
- Choose combine mode for taskbar buttons and labels. (W11 only)
|
- Choose combine mode for taskbar buttons and labels.
|
||||||
|
|
||||||
#### File Explorer
|
#### File Explorer
|
||||||
|
|
||||||
- Change the default location that File Explorer opens to.
|
- Change the default location that File Explorer opens to.
|
||||||
- Show file extensions for known file types.
|
- Show file extensions for known file types.
|
||||||
- Show hidden files, folders and drives.
|
- Show hidden files, folders and drives.
|
||||||
- Hide the Home or Gallery section from the File Explorer navigation pane. (W11 only)
|
- Hide the Home or Gallery section from the File Explorer navigation pane.
|
||||||
- Hide duplicate removable drive entries from the File Explorer navigation pane, so only the entry under 'This PC' remains.
|
- Hide duplicate removable drive entries from the File Explorer navigation pane, so only the entry under 'This PC' remains.
|
||||||
- Add all common folders (Desktop, Downloads, etc.) back to 'This PC' in File Explorer. (W11 only)
|
- Add all common folders (Desktop, Downloads, etc.) back to 'This PC' in File Explorer.
|
||||||
- Hide the 3D objects, music or OneDrive folder from the File Explorer navigation pane. (W10 only)
|
- Hide the 3D objects, music or OneDrive folder from the File Explorer navigation pane.
|
||||||
- Hide the 'Include in library', 'Give access to' and 'Share' options from the context menu. (W10 only)
|
- Hide the 'Include in library', 'Give access to' and 'Share' options from the context menu.
|
||||||
|
|
||||||
#### Multi-tasking
|
#### Multi-tasking
|
||||||
|
|
||||||
- Disable window snapping. (W11 only)
|
- Disable window snapping.
|
||||||
- Disable Snap Assist suggestions when snapping a window. (W11 only)
|
- Disable Snap Assist suggestions when snapping a window.
|
||||||
- Disable Snap Layout suggestions when dragging windows to the top of screen and when hovering on the maximize button. (W11 only)
|
- Disable Snap Layout suggestions when dragging windows to the top of screen and when hovering on the maximize button.
|
||||||
- Change if tabs are shown when snapping or pressing Alt+Tab. (W11 only)
|
- Change if tabs are shown when snapping or pressing Alt+Tab.
|
||||||
|
|
||||||
#### Optional Windows Features
|
#### Optional Windows Features
|
||||||
|
|
||||||
- Enable Windows Sandbox, a lightweight desktop environment for safely running applications in isolation. (W11 only)
|
- Enable Windows Sandbox, a lightweight desktop environment for safely running applications in isolation.
|
||||||
- Enable Windows Subsystem for Linux which allows you to run a Linux environment directly on Windows. (W11 only)
|
- Enable Windows Subsystem for Linux which allows you to run a Linux environment directly on Windows.
|
||||||
|
|
||||||
#### Other
|
#### Other
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,16 @@
|
|||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
Foreground="{DynamicResource FgColor}">
|
Foreground="{DynamicResource FgColor}">
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
|
<!-- Sort column header hover style -->
|
||||||
|
<Style x:Key="SortHeaderBtnStyle" TargetType="StackPanel">
|
||||||
|
<Setter Property="Opacity" Value="1.0"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Opacity" Value="0.75"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
<!-- ComboBox Style -->
|
<!-- ComboBox Style -->
|
||||||
<Style TargetType="ComboBox">
|
<Style TargetType="ComboBox">
|
||||||
<Setter Property="Background" Value="{DynamicResource ComboBgColor}"/>
|
<Setter Property="Background" Value="{DynamicResource ComboBgColor}"/>
|
||||||
@@ -99,13 +109,16 @@
|
|||||||
PlacementTarget="{Binding ElementName=ToggleButton}"
|
PlacementTarget="{Binding ElementName=ToggleButton}"
|
||||||
VerticalOffset="1"
|
VerticalOffset="1"
|
||||||
HorizontalOffset="0">
|
HorizontalOffset="0">
|
||||||
<Grid x:Name="DropDown" MinWidth="{TemplateBinding ActualWidth}" MaxHeight="{TemplateBinding MaxDropDownHeight}">
|
<Grid x:Name="DropDown" MinWidth="{TemplateBinding ActualWidth}" MaxHeight="{TemplateBinding MaxDropDownHeight}" Margin="12">
|
||||||
<Border x:Name="DropDownBorder"
|
<Border x:Name="DropDownBorder"
|
||||||
Background="{DynamicResource ComboItemBgColor}"
|
Background="{DynamicResource ComboItemBgColor}"
|
||||||
BorderBrush="{DynamicResource BorderColor}"
|
BorderBrush="{DynamicResource BorderColor}"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
CornerRadius="4"
|
CornerRadius="4"
|
||||||
Padding="5,4,5,1">
|
Padding="5,4,5,1">
|
||||||
|
<Border.Effect>
|
||||||
|
<DropShadowEffect BlurRadius="12" Opacity="0.25" ShadowDepth="4"/>
|
||||||
|
</Border.Effect>
|
||||||
<ScrollViewer Margin="0,2,0,0"
|
<ScrollViewer Margin="0,2,0,0"
|
||||||
VerticalScrollBarVisibility="Auto"
|
VerticalScrollBarVisibility="Auto"
|
||||||
HorizontalScrollBarVisibility="Disabled">
|
HorizontalScrollBarVisibility="Disabled">
|
||||||
@@ -338,7 +351,10 @@
|
|||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Border x:Name="CheckBoxBorder" Grid.Column="0" Width="18" Height="18" Background="{DynamicResource CheckBoxBgColor}" BorderBrush="{DynamicResource CheckBoxBorderColor}" BorderThickness="1" CornerRadius="4" Margin="0,0,8,0">
|
<Border x:Name="CheckBoxBorder" Grid.Column="0" Width="18" Height="18" Background="{DynamicResource CheckBoxBgColor}" BorderBrush="{DynamicResource CheckBoxBorderColor}" BorderThickness="1" CornerRadius="4" Margin="0,0,8,0">
|
||||||
<TextBlock x:Name="CheckMark" Text="" FontFamily="Segoe Fluent Icons" FontSize="12" Foreground="{DynamicResource ButtonBg}" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Collapsed"/>
|
<Grid>
|
||||||
|
<TextBlock x:Name="CheckMark" Text="" FontFamily="Segoe Fluent Icons" FontSize="12" Foreground="{DynamicResource ButtonBg}" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Collapsed"/>
|
||||||
|
<TextBlock x:Name="IndeterminateMark" Text="" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource ButtonBg}" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Collapsed" Margin="1,1,0,0" />
|
||||||
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
<ContentPresenter Grid.Column="1" VerticalAlignment="Center" Margin="0,0,0,2"/>
|
<ContentPresenter Grid.Column="1" VerticalAlignment="Center" Margin="0,0,0,2"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -353,6 +369,13 @@
|
|||||||
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonBg}"/>
|
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonBg}"/>
|
||||||
<Setter TargetName="CheckMark" Property="Foreground" Value="White"/>
|
<Setter TargetName="CheckMark" Property="Foreground" Value="White"/>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
|
<Trigger Property="IsChecked" Value="{x:Null}">
|
||||||
|
<Setter TargetName="IndeterminateMark" Property="Visibility" Value="Visible"/>
|
||||||
|
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonBg}"/>
|
||||||
|
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonBg}"/>
|
||||||
|
<Setter TargetName="CheckBoxBorder" Property="Opacity" Value="0.8"/>
|
||||||
|
<Setter TargetName="IndeterminateMark" Property="Foreground" Value="White"/>
|
||||||
|
</Trigger>
|
||||||
<Trigger Property="IsEnabled" Value="False">
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonDisabled}"/>
|
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonDisabled}"/>
|
||||||
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
||||||
@@ -368,6 +391,15 @@
|
|||||||
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonHover}"/>
|
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonHover}"/>
|
||||||
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonHover}"/>
|
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonHover}"/>
|
||||||
</MultiTrigger>
|
</MultiTrigger>
|
||||||
|
<MultiTrigger>
|
||||||
|
<MultiTrigger.Conditions>
|
||||||
|
<Condition Property="IsMouseOver" Value="True"/>
|
||||||
|
<Condition Property="IsChecked" Value="{x:Null}"/>
|
||||||
|
</MultiTrigger.Conditions>
|
||||||
|
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonHover}"/>
|
||||||
|
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonHover}"/>
|
||||||
|
<Setter TargetName="CheckBoxBorder" Property="Opacity" Value="0.8"/>
|
||||||
|
</MultiTrigger>
|
||||||
</ControlTemplate.Triggers>
|
</ControlTemplate.Triggers>
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
@@ -407,10 +439,37 @@
|
|||||||
<Setter Property="Margin" Value="8,0,8,0"/>
|
<Setter Property="Margin" Value="8,0,8,0"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- Column widths for the apps table row grid -->
|
<!-- Column widths for the app table rows and header (dot | name | description | id) -->
|
||||||
<GridLength x:Key="AppTableCol0Width">160</GridLength>
|
<GridLength x:Key="AppTableDotColWidth">16</GridLength>
|
||||||
<GridLength x:Key="AppTableCol1Width">1*</GridLength>
|
<GridLength x:Key="AppTableNameColWidth">151</GridLength>
|
||||||
<GridLength x:Key="AppTableCol2Width">286</GridLength>
|
<GridLength x:Key="AppTableDescColWidth">1*</GridLength>
|
||||||
|
<GridLength x:Key="AppTableIdColWidth">261</GridLength>
|
||||||
|
|
||||||
|
<!-- Recommendation dot shape style for app table rows -->
|
||||||
|
<Style x:Key="AppRecommendationDotStyle" TargetType="Ellipse">
|
||||||
|
<Setter Property="Width" Value="9"/>
|
||||||
|
<Setter Property="Height" Value="9"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Left"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Container style for each dynamically-created app table row -->
|
||||||
|
<Style x:Key="AppTableRowStyle" TargetType="Grid">
|
||||||
|
<Setter Property="Margin" Value="0,1,0,0"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Style for dynamically-created preset checkboxes in the Quick Select popup -->
|
||||||
|
<Style x:Key="PresetCheckBoxStyle" TargetType="CheckBox" BasedOn="{StaticResource {x:Type CheckBox}}">
|
||||||
|
<Setter Property="Margin" Value="8,4"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Progress step indicator fill colors -->
|
||||||
|
<SolidColorBrush x:Key="ProgressActiveColor" Color="#0067c0"/>
|
||||||
|
<SolidColorBrush x:Key="ProgressInactiveColor" Color="#808080"/>
|
||||||
|
|
||||||
|
<!-- Validation feedback colors for username input -->
|
||||||
|
<SolidColorBrush x:Key="ValidationErrorColor" Color="#c42b1c"/>
|
||||||
|
<SolidColorBrush x:Key="ValidationSuccessColor" Color="#28a745"/>
|
||||||
|
|
||||||
<!-- Title Bar Button Style -->
|
<!-- Title Bar Button Style -->
|
||||||
<Style x:Key="TitleBarButton" TargetType="Button">
|
<Style x:Key="TitleBarButton" TargetType="Button">
|
||||||
@@ -450,10 +509,10 @@
|
|||||||
</Setter>
|
</Setter>
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHover}"/>
|
<Setter Property="Background" Value="{DynamicResource TitlebarButtonHover}"/>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Trigger Property="IsPressed" Value="True">
|
<Trigger Property="IsPressed" Value="True">
|
||||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressed}"/>
|
<Setter Property="Background" Value="{DynamicResource TitlebarButtonPressed}"/>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
</Style>
|
</Style>
|
||||||
@@ -480,15 +539,22 @@
|
|||||||
<Setter Property="BorderThickness" Value="1"/>
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
<Setter Property="Padding" Value="4"/>
|
<Setter Property="Padding" Value="4"/>
|
||||||
<Setter Property="HasDropShadow" Value="True"/>
|
<Setter Property="HasDropShadow" Value="True"/>
|
||||||
|
<Setter Property="HorizontalOffset" Value="-12"/>
|
||||||
|
<Setter Property="VerticalOffset" Value="-12"/>
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="ContextMenu">
|
<ControlTemplate TargetType="ContextMenu">
|
||||||
<Border Background="{TemplateBinding Background}"
|
<Border Margin="12">
|
||||||
BorderBrush="{TemplateBinding BorderBrush}"
|
<Border Background="{TemplateBinding Background}"
|
||||||
BorderThickness="{TemplateBinding BorderThickness}"
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
CornerRadius="4"
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
Padding="{TemplateBinding Padding}">
|
CornerRadius="4"
|
||||||
<StackPanel IsItemsHost="True"/>
|
Padding="{TemplateBinding Padding}">
|
||||||
|
<Border.Effect>
|
||||||
|
<DropShadowEffect BlurRadius="12" Opacity="0.25" ShadowDepth="4"/>
|
||||||
|
</Border.Effect>
|
||||||
|
<StackPanel IsItemsHost="True"/>
|
||||||
|
</Border>
|
||||||
</Border>
|
</Border>
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
@@ -710,9 +776,60 @@
|
|||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" Grid.Column="0">
|
<StackPanel Orientation="Horizontal" Grid.Column="0">
|
||||||
<Button x:Name="DefaultAppsBtn" Content="Select Default Apps" ToolTip="Select the default selection of apps" Style="{DynamicResource SecondaryButtonStyle}" Height="32" Padding="10,0" Margin="0,0,10,0" AutomationProperties.Name="Select Default Apps"/>
|
<ToggleButton x:Name="PresetsBtn" ToolTip="Select or clear app presets" Height="32" Padding="10,0" Margin="0,0,10,0" AutomationProperties.Name="App Presets">
|
||||||
<Button x:Name="LoadLastUsedAppsBtn" Content="Select Last Used Selection" ToolTip="Select the apps that were selected the last time Win11Debloat was run" Style="{DynamicResource SecondaryButtonStyle}" Height="32" Padding="10,0" Margin="0,0,10,0" AutomationProperties.Name="Select Last Used Selection"/>
|
<ToggleButton.Style>
|
||||||
|
<Style TargetType="ToggleButton">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBg}"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ToggleButton">
|
||||||
|
<Border x:Name="Border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="4" Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,0,0,1"/>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHover}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressed}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHover}"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</ToggleButton.Style>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="Quick Select" FontSize="13" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||||
|
<TextBlock x:Name="PresetsArrow" Text="" FontFamily="Segoe Fluent Icons" FontSize="10" VerticalAlignment="Center" RenderTransformOrigin="0.5,0.5">
|
||||||
|
<TextBlock.RenderTransform>
|
||||||
|
<RotateTransform x:Name="PresetsArrowRotation" Angle="0"/>
|
||||||
|
</TextBlock.RenderTransform>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</ToggleButton>
|
||||||
<Button x:Name="ClearAppSelectionBtn" Content="Clear Selection" ToolTip="Clear all selected apps" Style="{DynamicResource SecondaryButtonStyle}" Height="32" Padding="10,0" Margin="0,0,10,0" AutomationProperties.Name="Clear Selection"/>
|
<Button x:Name="ClearAppSelectionBtn" Content="Clear Selection" ToolTip="Clear all selected apps" Style="{DynamicResource SecondaryButtonStyle}" Height="32" Padding="10,0" Margin="0,0,10,0" AutomationProperties.Name="Clear Selection"/>
|
||||||
|
<Popup x:Name="PresetsPopup" PlacementTarget="{Binding ElementName=PresetsBtn}" Placement="Bottom" StaysOpen="True" AllowsTransparency="True" VerticalOffset="2">
|
||||||
|
<Border Background="{DynamicResource CardBgColor}" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" CornerRadius="6" Padding="4,6" Margin="12">
|
||||||
|
<Border.Effect>
|
||||||
|
<DropShadowEffect BlurRadius="12" Opacity="0.25" ShadowDepth="4"/>
|
||||||
|
</Border.Effect>
|
||||||
|
<StackPanel x:Name="PresetsPanel" MinWidth="220">
|
||||||
|
<CheckBox x:Name="PresetDefaultApps" Content="Default selection" IsThreeState="True" Foreground="{DynamicResource FgColor}" Margin="8,4" AutomationProperties.Name="Default selection"/>
|
||||||
|
<CheckBox x:Name="PresetLastUsed" Content="Last used selection" IsThreeState="True" Foreground="{DynamicResource FgColor}" Margin="8,4" AutomationProperties.Name="Last used selection"/>
|
||||||
|
<Separator Margin="4,6" Background="{DynamicResource BorderColor}"/>
|
||||||
|
<StackPanel x:Name="JsonPresetsPanel"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Popup>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<CheckBox x:Name="OnlyInstalledAppsBox" Grid.Column="2" Content="Only show installed apps" IsChecked="False" Foreground="{DynamicResource FgColor}" VerticalAlignment="Center" AutomationProperties.Name="Only show installed apps"/>
|
<CheckBox x:Name="OnlyInstalledAppsBox" Grid.Column="2" Content="Only show installed apps" IsChecked="False" Foreground="{DynamicResource FgColor}" VerticalAlignment="Center" AutomationProperties.Name="Only show installed apps"/>
|
||||||
@@ -754,15 +871,31 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<!-- Column Headers -->
|
<!-- Column Headers -->
|
||||||
<Border Grid.Row="0" Background="{DynamicResource TableHeaderColor}" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1,1,1,0" CornerRadius="4,4,0,0">
|
<Border Grid.Row="0" Background="{DynamicResource TableHeaderColor}" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1,1,1,0" CornerRadius="4,4,0,0">
|
||||||
<Grid Margin="26,6,8,8">
|
<Grid Margin="42,6,23,8">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="160"/>
|
<ColumnDefinition Width="{StaticResource AppTableDotColWidth}"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="{StaticResource AppTableNameColWidth}"/>
|
||||||
<ColumnDefinition Width="300"/>
|
<ColumnDefinition Width="{StaticResource AppTableDescColWidth}"/>
|
||||||
|
<ColumnDefinition Width="{StaticResource AppTableIdColWidth}"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<TextBlock Grid.Column="0" Text="Name" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource FgColor}" Margin="16,0,0,0"/>
|
<StackPanel x:Name="HeaderNameBtn" Grid.Column="1" Orientation="Horizontal" Cursor="Hand" VerticalAlignment="Center" Style="{StaticResource SortHeaderBtnStyle}">
|
||||||
<TextBlock Grid.Column="1" Text="Description" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource FgColor}" Margin="24,0,0,0"/>
|
<TextBlock Text="Name" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource FgColor}"/>
|
||||||
<TextBlock Grid.Column="2" Text="App ID" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource FgColor}"/>
|
<TextBlock x:Name="SortArrowName" Text="" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource FgColor}" VerticalAlignment="Center" Margin="5,1,0,0" Opacity="0.3" RenderTransformOrigin="0.5,0.5">
|
||||||
|
<TextBlock.RenderTransform><RotateTransform Angle="0"/></TextBlock.RenderTransform>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel x:Name="HeaderDescriptionBtn" Grid.Column="2" Orientation="Horizontal" Cursor="Hand" VerticalAlignment="Center" Margin="8,0,0,0" Style="{StaticResource SortHeaderBtnStyle}">
|
||||||
|
<TextBlock Text="Description" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource FgColor}"/>
|
||||||
|
<TextBlock x:Name="SortArrowDescription" Text="" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource FgColor}" VerticalAlignment="Center" Margin="5,1,0,0" Opacity="0.3" RenderTransformOrigin="0.5,0.5">
|
||||||
|
<TextBlock.RenderTransform><RotateTransform Angle="0"/></TextBlock.RenderTransform>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel x:Name="HeaderAppIdBtn" Grid.Column="3" Orientation="Horizontal" Cursor="Hand" VerticalAlignment="Center" Style="{StaticResource SortHeaderBtnStyle}">
|
||||||
|
<TextBlock Text="App ID" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource FgColor}"/>
|
||||||
|
<TextBlock x:Name="SortArrowAppId" Text="" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource FgColor}" VerticalAlignment="Center" Margin="5,1,0,0" Opacity="0.3" RenderTransformOrigin="0.5,0.5">
|
||||||
|
<TextBlock.RenderTransform><RotateTransform Angle="0"/></TextBlock.RenderTransform>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
<!-- Apps content -->
|
<!-- Apps content -->
|
||||||
@@ -772,7 +905,26 @@
|
|||||||
<StackPanel x:Name="AppSelectionPanel" Margin="10,4,0,4"/>
|
<StackPanel x:Name="AppSelectionPanel" Margin="10,4,0,4"/>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
<Border x:Name="LoadingAppsIndicator" CornerRadius="0,0,4,4" Background="{DynamicResource CardBgColor}" Opacity="0.8" Visibility="Collapsed">
|
<Border x:Name="LoadingAppsIndicator" CornerRadius="0,0,4,4" Background="{DynamicResource CardBgColor}" Opacity="0.8" Visibility="Collapsed">
|
||||||
<TextBlock Text="Loading apps..." FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource FgColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="28" Foreground="{DynamicResource FgColor}" HorizontalAlignment="Center" Margin="0,0,0,8" RenderTransformOrigin="0.5,0.5">
|
||||||
|
<TextBlock.RenderTransform>
|
||||||
|
<RotateTransform Angle="0"/>
|
||||||
|
</TextBlock.RenderTransform>
|
||||||
|
<TextBlock.Triggers>
|
||||||
|
<EventTrigger RoutedEvent="Loaded">
|
||||||
|
<BeginStoryboard>
|
||||||
|
<Storyboard>
|
||||||
|
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
|
||||||
|
From="0" To="360"
|
||||||
|
Duration="0:0:1.5"
|
||||||
|
RepeatBehavior="Forever"/>
|
||||||
|
</Storyboard>
|
||||||
|
</BeginStoryboard>
|
||||||
|
</EventTrigger>
|
||||||
|
</TextBlock.Triggers>
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Text="Loading apps..." FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource FgColor}" HorizontalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
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) {
|
if ($SysRestore.RPSessionInterval -eq 0) {
|
||||||
# In GUI mode, skip the prompt and just try to enable it
|
# 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') {
|
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 {
|
||||||
try {
|
$enableResult = Invoke-NonBlocking -TimeoutSeconds 20 -ScriptBlock {
|
||||||
Enable-ComputerRestore -Drive "$env:SystemDrive"
|
try {
|
||||||
|
Enable-ComputerRestore -Drive "$env:SystemDrive"
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return "Error: Failed to enable System Restore: $_"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch {
|
}
|
||||||
return "Error: Failed to enable System Restore: $_"
|
catch {
|
||||||
}
|
$enableResult = "Error: Failed to enable System Restore: $_"
|
||||||
return $null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$enableSystemRestoreJobDone = $enableSystemRestoreJob | Wait-Job -TimeOut 20
|
if ($enableResult) {
|
||||||
|
Write-Host $enableResult -ForegroundColor Red
|
||||||
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
|
|
||||||
$failed = $true
|
$failed = $true
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
$result = Receive-Job $enableSystemRestoreJob
|
|
||||||
Remove-Job -Job $enableSystemRestoreJob -ErrorAction SilentlyContinue
|
|
||||||
if ($result) {
|
|
||||||
Write-Host $result -ForegroundColor Red
|
|
||||||
$failed = $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
@@ -38,46 +32,43 @@ function CreateSystemRestorePoint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not $failed) {
|
if (-not $failed) {
|
||||||
$createRestorePointJob = Start-Job {
|
try {
|
||||||
# Find existing restore points that are less than 24 hours old
|
$result = Invoke-NonBlocking -TimeoutSeconds 20 -ScriptBlock {
|
||||||
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 {
|
try {
|
||||||
Checkpoint-Computer -Description "Restore point created by Win11Debloat" -RestorePointType "MODIFY_SETTINGS"
|
$recentRestorePoints = Get-ComputerRestorePoint | Where-Object { (Get-Date) - [System.Management.ManagementDateTimeConverter]::ToDateTime($_.CreationTime) -le (New-TimeSpan -Hours 24) }
|
||||||
return @{ Success = $true; Message = "System restore point created successfully" }
|
|
||||||
}
|
}
|
||||||
catch {
|
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 ($result -and $result.Success) {
|
||||||
|
Write-Host $result.Message
|
||||||
if (-not $createRestorePointJobDone) {
|
}
|
||||||
Remove-Job -Job $createRestorePointJob -Force -ErrorAction SilentlyContinue
|
elseif ($result) {
|
||||||
Write-Host "Error: Failed to create system restore point, operation timed out" -ForegroundColor Red
|
Write-Host $result.Message -ForegroundColor Red
|
||||||
$failed = $true
|
$failed = $true
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$result = Receive-Job $createRestorePointJob
|
Write-Host "Error: Failed to create system restore point" -ForegroundColor Red
|
||||||
Remove-Job -Job $createRestorePointJob -ErrorAction SilentlyContinue
|
$failed = $true
|
||||||
if ($result.Success) {
|
|
||||||
Write-Host $result.Message
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Host $result.Message -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
|
IsChecked = $isChecked
|
||||||
Description = $appData.Description
|
Description = $appData.Description
|
||||||
SelectedByDefault = $appData.SelectedByDefault
|
SelectedByDefault = $appData.SelectedByDefault
|
||||||
|
Recommendation = $appData.Recommendation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,60 +11,64 @@ function ApplySettingsToUiControls {
|
|||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
|
|
||||||
# First, reset all tweaks to "No Change" (index 0) or unchecked
|
if (-not $uiControlMappings) {
|
||||||
if ($uiControlMappings) {
|
return $true
|
||||||
foreach ($comboName in $uiControlMappings.Keys) {
|
}
|
||||||
$control = $window.FindName($comboName)
|
|
||||||
if ($control -is [System.Windows.Controls.CheckBox]) {
|
# Build control cache and reverse index (featureId -> control info) in a single pass
|
||||||
$control.IsChecked = $false
|
$controlCache = @{}
|
||||||
}
|
$featureIdIndex = @{}
|
||||||
elseif ($control -is [System.Windows.Controls.ComboBox]) {
|
|
||||||
$control.SelectedIndex = 0
|
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) {
|
foreach ($setting in $settingsJson.Settings) {
|
||||||
if ($setting.Value -ne $true) { continue }
|
if ($setting.Value -ne $true) { continue }
|
||||||
$paramName = $setting.Name
|
if ($setting.Name -eq 'CreateRestorePoint') { continue }
|
||||||
|
|
||||||
# Skip RestorePointCheckBox, this is always checked by default
|
$entry = $featureIdIndex[$setting.Name]
|
||||||
if ($paramName -eq 'CreateRestorePoint') {
|
if (-not $entry) { continue }
|
||||||
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
if ($uiControlMappings) {
|
if ($control -is [System.Windows.Controls.CheckBox]) {
|
||||||
foreach ($comboName in $uiControlMappings.Keys) {
|
$control.IsChecked = $true
|
||||||
$mapping = $uiControlMappings[$comboName]
|
}
|
||||||
if ($mapping.Type -eq 'group') {
|
elseif ($control -is [System.Windows.Controls.ComboBox]) {
|
||||||
$i = 1
|
$control.SelectedIndex = 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ function SetWindowThemeResources {
|
|||||||
$window.Resources.Add("InputFocusColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#1f1f1f")))
|
$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("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("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("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("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")))
|
$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("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("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("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("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("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")))
|
$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)
|
# Load apps after window is shown (allows UI to render first)
|
||||||
$window.Add_ContentRendered({
|
$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
|
# Show the window and return dialog result
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ function Show-ApplyModal {
|
|||||||
$script:ApplyProgressCallback = $null
|
$script:ApplyProgressCallback = $null
|
||||||
$script:ApplySubStepCallback = $null
|
$script:ApplySubStepCallback = $null
|
||||||
}
|
}
|
||||||
})
|
}) | Out-Null
|
||||||
|
|
||||||
# Button handlers
|
# Button handlers
|
||||||
$applyCloseBtn.Add_Click({
|
$applyCloseBtn.Add_Click({
|
||||||
|
|||||||
@@ -210,21 +210,113 @@ function Show-MainWindow {
|
|||||||
$onlyInstalledAppsBox = $window.FindName('OnlyInstalledAppsBox')
|
$onlyInstalledAppsBox = $window.FindName('OnlyInstalledAppsBox')
|
||||||
$loadingAppsIndicator = $window.FindName('LoadingAppsIndicator')
|
$loadingAppsIndicator = $window.FindName('LoadingAppsIndicator')
|
||||||
$appSelectionStatus = $window.FindName('AppSelectionStatus')
|
$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')
|
$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
|
# Track the last selected checkbox for shift-click range selection
|
||||||
$script:MainWindowLastSelectedCheckbox = $null
|
$script:MainWindowLastSelectedCheckbox = $null
|
||||||
|
|
||||||
# Track current app loading operation to prevent race conditions
|
# Guard flag: true while a load is in progress; prevents concurrent loads
|
||||||
$script:CurrentAppLoadTimer = $null
|
$script:IsLoadingApps = $false
|
||||||
$script:CurrentAppLoadJob = $null
|
# Flag set when Default Mode is clicked before apps have finished loading
|
||||||
$script:CurrentAppLoadJobStartTime = $null
|
$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
|
# Set script-level variable for GUI window reference
|
||||||
$script:GuiWindow = $window
|
$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 {
|
function UpdateAppSelectionStatus {
|
||||||
$selectedCount = 0
|
$selectedCount = 0
|
||||||
foreach ($child in $appsPanel.Children) {
|
foreach ($child in $appsPanel.Children) {
|
||||||
@@ -235,6 +327,80 @@ function Show-MainWindow {
|
|||||||
$appSelectionStatus.Text = "$selectedCount app(s) selected for removal"
|
$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
|
# Dynamically builds Tweaks UI from Features.json
|
||||||
function BuildDynamicTweaks {
|
function BuildDynamicTweaks {
|
||||||
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0"
|
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0"
|
||||||
@@ -309,7 +475,7 @@ function Show-MainWindow {
|
|||||||
$combo = New-Object System.Windows.Controls.ComboBox
|
$combo = New-Object System.Windows.Controls.ComboBox
|
||||||
$combo.Name = $comboName
|
$combo.Name = $comboName
|
||||||
$combo.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $labelText)
|
$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
|
$combo.SelectedIndex = 0
|
||||||
$parent.Children.Add($combo) | Out-Null
|
$parent.Children.Add($combo) | Out-Null
|
||||||
|
|
||||||
@@ -512,157 +678,177 @@ function Show-MainWindow {
|
|||||||
try { $lblBorderObj = $window.FindName("$comboName`_LabelBorder") } catch {}
|
try { $lblBorderObj = $window.FindName("$comboName`_LabelBorder") } catch {}
|
||||||
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
|
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) {
|
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
|
$script:MainWindowLastSelectedCheckbox = $null
|
||||||
|
|
||||||
# Sort apps alphabetically and add to panel
|
$loaderScriptPath = $script:LoadAppsDetailsScriptPath
|
||||||
$appsToAdd | Sort-Object -Property FriendlyName | ForEach-Object {
|
$appsFilePath = $script:AppsListFilePath
|
||||||
$checkbox = New-Object System.Windows.Controls.CheckBox
|
$onlyInstalled = [bool]$onlyInstalledAppsBox.IsChecked
|
||||||
$checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $_.FriendlyName)
|
|
||||||
$checkbox.Tag = $_.AppId
|
|
||||||
$checkbox.IsChecked = $_.IsChecked
|
|
||||||
$checkbox.Style = $window.Resources["AppsPanelCheckBoxStyle"]
|
|
||||||
|
|
||||||
# Build table row content: App Name | Description | App ID
|
# Use preloaded data if available; otherwise load in background job
|
||||||
$row = New-Object System.Windows.Controls.Grid
|
if (-not $onlyInstalled -and $script:PreloadedAppData) {
|
||||||
$c0 = New-Object System.Windows.Controls.ColumnDefinition; $c0.Width = [System.Windows.GridLength]::new(160)
|
$rawAppData = $script:PreloadedAppData
|
||||||
$c1 = New-Object System.Windows.Controls.ColumnDefinition; $c1.Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star)
|
$script:PreloadedAppData = $null
|
||||||
$c2 = New-Object System.Windows.Controls.ColumnDefinition; $c2.Width = [System.Windows.GridLength]::new(286)
|
} else {
|
||||||
$row.ColumnDefinitions.Add($c0); $row.ColumnDefinitions.Add($c1); $row.ColumnDefinitions.Add($c2)
|
# Load apps details in a background job to keep the UI responsive
|
||||||
|
$rawAppData = Invoke-NonBlocking -ScriptBlock {
|
||||||
$tbName = New-Object System.Windows.Controls.TextBlock
|
param($loaderScript, $appsListFilePath, $installedList, $onlyInstalled)
|
||||||
$tbName.Text = $_.FriendlyName
|
$script:AppsListFilePath = $appsListFilePath
|
||||||
$tbName.Style = $window.Resources["AppNameTextStyle"]
|
. $loaderScript
|
||||||
[System.Windows.Controls.Grid]::SetColumn($tbName, 0)
|
LoadAppsDetailsFromJson -OnlyInstalled:$onlyInstalled -InstalledList $installedList -InitialCheckedFromJson:$false
|
||||||
|
} -ArgumentList $loaderScriptPath, $appsFilePath, $listOfApps, $onlyInstalled
|
||||||
$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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Hide loading indicator and navigation blocker, update status
|
$appsToAdd = @($rawAppData | Where-Object { $_ -and ($_.AppId -or $_.FriendlyName) } | Sort-Object -Property FriendlyName)
|
||||||
|
|
||||||
$loadingAppsIndicator.Visibility = 'Collapsed'
|
$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
|
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
|
# Loads apps into the UI
|
||||||
function LoadAppsIntoMainUI {
|
function LoadAppsIntoMainUI {
|
||||||
# Cancel any existing load operation to prevent race conditions
|
# Prevent concurrent loads
|
||||||
if ($script:CurrentAppLoadTimer -and $script:CurrentAppLoadTimer.IsEnabled) {
|
if ($script:IsLoadingApps) { return }
|
||||||
$script:CurrentAppLoadTimer.Stop()
|
$script:IsLoadingApps = $true
|
||||||
}
|
|
||||||
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
|
# Show loading indicator and clear existing apps
|
||||||
$loadingAppsIndicator.Visibility = 'Visible'
|
$loadingAppsIndicator.Visibility = 'Visible'
|
||||||
$appsPanel.Children.Clear()
|
$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
|
# Update navigation buttons to disable Next/Previous
|
||||||
UpdateNavigationButtons
|
UpdateNavigationButtons
|
||||||
|
|
||||||
# Force UI to update and render all changes (loading indicator, blocker, disabled buttons)
|
# 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.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]{
|
$window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{
|
||||||
$listOfApps = ""
|
try {
|
||||||
|
$listOfApps = ""
|
||||||
|
|
||||||
if ($onlyInstalledAppsBox.IsChecked -and ($script:WingetInstalled -eq $true)) {
|
if ($onlyInstalledAppsBox.IsChecked -and ($script:WingetInstalled -eq $true)) {
|
||||||
# Start job to get list of installed apps via WinGet (async helper)
|
$listOfApps = GetInstalledAppsViaWinget -TimeOut 10 -NonBlocking
|
||||||
$asyncJob = GetInstalledAppsViaWinget -Async
|
|
||||||
$script:CurrentAppLoadJob = $asyncJob.Job
|
|
||||||
$script:CurrentAppLoadJobStartTime = $asyncJob.StartTime
|
|
||||||
|
|
||||||
# Create timer to poll job status without blocking UI
|
if ($null -eq $listOfApps) {
|
||||||
$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
|
Show-MessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null
|
||||||
$onlyInstalledAppsBox.IsChecked = $false
|
$onlyInstalledAppsBox.IsChecked = $false
|
||||||
|
|
||||||
# Continue with loading all apps (unchecked now)
|
|
||||||
LoadAppsWithList ""
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
$script:CurrentAppLoadTimer.Start()
|
LoadAppsWithList $listOfApps
|
||||||
return # Exit here, timer will continue the work
|
}
|
||||||
|
finally {
|
||||||
|
$script:IsLoadingApps = $false
|
||||||
}
|
}
|
||||||
|
|
||||||
# If checkbox is not checked or winget not installed, load all apps immediately
|
|
||||||
LoadAppsWithList $listOfApps
|
|
||||||
}) | Out-Null
|
}) | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,27 +860,61 @@ function Show-MainWindow {
|
|||||||
LoadAppsIntoMainUI
|
LoadAppsIntoMainUI
|
||||||
})
|
})
|
||||||
|
|
||||||
# Quick selection buttons - only select apps actually in those categories
|
# Animate arrow when popup opens/closes, and lazily update preset states
|
||||||
$defaultAppsBtn.Add_Click({
|
$presetsPopup.Add_Opened({
|
||||||
foreach ($child in $appsPanel.Children) {
|
UpdatePresetStates
|
||||||
if ($child -is [System.Windows.Controls.CheckBox]) {
|
$animation = New-Object System.Windows.Media.Animation.DoubleAnimation
|
||||||
if ($child.SelectedByDefault -eq $true) {
|
$animation.To = 180
|
||||||
$child.IsChecked = $true
|
$animation.Duration = [System.Windows.Duration]::new([System.TimeSpan]::FromMilliseconds(200))
|
||||||
} else {
|
$animation.EasingFunction = New-Object System.Windows.Media.Animation.CubicEase
|
||||||
$child.IsChecked = $false
|
$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 }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$clearAppSelectionBtn.Add_Click({
|
# Toggle popup on button click
|
||||||
foreach ($child in $appsPanel.Children) {
|
$presetsBtn.Add_Click({
|
||||||
if ($child -is [System.Windows.Controls.CheckBox]) {
|
$presetsPopup.IsOpen = -not $presetsPopup.IsOpen
|
||||||
$child.IsChecked = $false
|
$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({
|
||||||
|
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
|
# Helper function to scroll to an item if it's not visible, centering it in the viewport
|
||||||
function ScrollToItemIfNotVisible {
|
function ScrollToItemIfNotVisible {
|
||||||
param (
|
param (
|
||||||
@@ -830,7 +1050,7 @@ function Show-MainWindow {
|
|||||||
# The 17px accounts for the scrollbar width + some padding
|
# The 17px accounts for the scrollbar width + some padding
|
||||||
$tweakSearchBorder.Margin = [System.Windows.Thickness]::new(0, 0, 17, 0)
|
$tweakSearchBorder.Margin = [System.Windows.Thickness]::new(0, 0, 17, 0)
|
||||||
} else {
|
} 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
|
# Update progress indicators
|
||||||
# Tab indices: 0=Home, 1=App Removal, 2=Tweaks, 3=Deployment Settings
|
# Tab indices: 0=Home, 1=App Removal, 2=Tweaks, 3=Deployment Settings
|
||||||
$blueColor = "#0067c0"
|
|
||||||
$greyColor = "#808080"
|
|
||||||
|
|
||||||
$progressIndicator1 = $window.FindName('ProgressIndicator1') # App Removal
|
$progressIndicator1 = $window.FindName('ProgressIndicator1') # App Removal
|
||||||
$progressIndicator2 = $window.FindName('ProgressIndicator2') # Tweaks
|
$progressIndicator2 = $window.FindName('ProgressIndicator2') # Tweaks
|
||||||
$progressIndicator3 = $window.FindName('ProgressIndicator3') # Deployment Settings
|
$progressIndicator3 = $window.FindName('ProgressIndicator3') # Deployment Settings
|
||||||
@@ -998,23 +1215,23 @@ function Show-MainWindow {
|
|||||||
# Update indicator colors based on current tab
|
# Update indicator colors based on current tab
|
||||||
# Indicator 1 (App Removal) - tab index 1
|
# Indicator 1 (App Removal) - tab index 1
|
||||||
if ($currentIndex -ge 1) {
|
if ($currentIndex -ge 1) {
|
||||||
$progressIndicator1.Fill = $blueColor
|
$progressIndicator1.Fill = $window.Resources['ProgressActiveColor']
|
||||||
} else {
|
} else {
|
||||||
$progressIndicator1.Fill = $greyColor
|
$progressIndicator1.Fill = $window.Resources['ProgressInactiveColor']
|
||||||
}
|
}
|
||||||
|
|
||||||
# Indicator 2 (Tweaks) - tab index 2
|
# Indicator 2 (Tweaks) - tab index 2
|
||||||
if ($currentIndex -ge 2) {
|
if ($currentIndex -ge 2) {
|
||||||
$progressIndicator2.Fill = $blueColor
|
$progressIndicator2.Fill = $window.Resources['ProgressActiveColor']
|
||||||
} else {
|
} else {
|
||||||
$progressIndicator2.Fill = $greyColor
|
$progressIndicator2.Fill = $window.Resources['ProgressInactiveColor']
|
||||||
}
|
}
|
||||||
|
|
||||||
# Indicator 3 (Deployment Settings) - tab index 3
|
# Indicator 3 (Deployment Settings) - tab index 3
|
||||||
if ($currentIndex -ge 3) {
|
if ($currentIndex -ge 3) {
|
||||||
$progressIndicator3.Fill = $blueColor
|
$progressIndicator3.Fill = $window.Resources['ProgressActiveColor']
|
||||||
} else {
|
} else {
|
||||||
$progressIndicator3.Fill = $greyColor
|
$progressIndicator3.Fill = $window.Resources['ProgressInactiveColor']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1033,7 +1250,7 @@ function Show-MainWindow {
|
|||||||
$appRemovalScopeCombo.SelectedIndex = 0
|
$appRemovalScopeCombo.SelectedIndex = 0
|
||||||
}
|
}
|
||||||
1 {
|
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'
|
$otherUserPanel.Visibility = 'Visible'
|
||||||
$usernameValidationMessage.Text = ""
|
$usernameValidationMessage.Text = ""
|
||||||
# Hide "Current user only" option, show "Target user only" option
|
# Hide "Current user only" option, show "Target user only" option
|
||||||
@@ -1099,8 +1316,8 @@ function Show-MainWindow {
|
|||||||
|
|
||||||
$username = $otherUsernameTextBox.Text.Trim()
|
$username = $otherUsernameTextBox.Text.Trim()
|
||||||
|
|
||||||
$errorBrush = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#c42b1c"))
|
$errorBrush = $window.Resources['ValidationErrorColor']
|
||||||
$successBrush = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#28a745"))
|
$successBrush = $window.Resources['ValidationSuccessColor']
|
||||||
|
|
||||||
if ($username.Length -eq 0) {
|
if ($username.Length -eq 0) {
|
||||||
$usernameValidationMessage.Text = "[X] Please enter a username"
|
$usernameValidationMessage.Text = "[X] Please enter a username"
|
||||||
@@ -1128,9 +1345,6 @@ function Show-MainWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function GenerateOverview {
|
function GenerateOverview {
|
||||||
# Load Features.json
|
|
||||||
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0"
|
|
||||||
|
|
||||||
$changesList = @()
|
$changesList = @()
|
||||||
|
|
||||||
# Collect selected apps
|
# Collect selected apps
|
||||||
@@ -1180,13 +1394,14 @@ function Show-MainWindow {
|
|||||||
# For combobox: SelectedIndex 0 = No Change, so subtract 1 to index into Values
|
# For combobox: SelectedIndex 0 = No Change, so subtract 1 to index into Values
|
||||||
$selectedValue = $mapping.Values[$control.SelectedIndex - 1]
|
$selectedValue = $mapping.Values[$control.SelectedIndex - 1]
|
||||||
foreach ($fid in $selectedValue.FeatureIds) {
|
foreach ($fid in $selectedValue.FeatureIds) {
|
||||||
$feature = $featuresJson.Features | Where-Object { $_.FeatureId -eq $fid }
|
$label = $script:FeatureLabelLookup[$fid]
|
||||||
if ($feature) { $changesList += ($feature.Action + ' ' + $feature.Label) }
|
if ($label) { $changesList += $label }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elseif ($mapping.Type -eq 'feature') {
|
elseif ($mapping.Type -eq 'feature') {
|
||||||
$feature = $featuresJson.Features | Where-Object { $_.FeatureId -eq $mapping.FeatureId }
|
$label = $script:FeatureLabelLookup[$mapping.FeatureId]
|
||||||
if ($feature) { $changesList += ($feature.Action + ' ' + $feature.Label) }
|
if (-not $label) { $label = $mapping.Action + ' ' + $mapping.Label }
|
||||||
|
$changesList += $label
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1218,7 +1433,6 @@ function Show-MainWindow {
|
|||||||
$nextBtn.Add_Click({
|
$nextBtn.Add_Click({
|
||||||
if ($tabControl.SelectedIndex -lt ($tabControl.Items.Count - 1)) {
|
if ($tabControl.SelectedIndex -lt ($tabControl.Items.Count - 1)) {
|
||||||
$tabControl.SelectedIndex++
|
$tabControl.SelectedIndex++
|
||||||
|
|
||||||
UpdateNavigationButtons
|
UpdateNavigationButtons
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1240,11 +1454,11 @@ function Show-MainWindow {
|
|||||||
ApplySettingsToUiControls -window $window -settingsJson $defaultsJson -uiControlMappings $script:UiControlMappings
|
ApplySettingsToUiControls -window $window -settingsJson $defaultsJson -uiControlMappings $script:UiControlMappings
|
||||||
}
|
}
|
||||||
|
|
||||||
# Select default apps
|
# Deselect all apps, then select default apps (defer if apps are still loading in the background)
|
||||||
foreach ($child in $appsPanel.Children) {
|
if ($script:IsLoadingApps) {
|
||||||
if ($child -is [System.Windows.Controls.CheckBox]) {
|
$script:PendingDefaultMode = $true
|
||||||
$child.IsChecked = ($child.SelectedByDefault -eq $true)
|
} else {
|
||||||
}
|
ApplyPresetToApps -MatchFilter { param($c) $c.SelectedByDefault -eq $true } -Exclusive
|
||||||
}
|
}
|
||||||
|
|
||||||
# Navigate directly to the Deployment Settings tab
|
# Navigate directly to the Deployment Settings tab
|
||||||
@@ -1375,8 +1589,17 @@ function Show-MainWindow {
|
|||||||
|
|
||||||
# Store selected user mode
|
# Store selected user mode
|
||||||
switch ($userSelectionCombo.SelectedIndex) {
|
switch ($userSelectionCombo.SelectedIndex) {
|
||||||
1 { AddParameter User ($otherUsernameTextBox.Text.Trim()) }
|
0 {
|
||||||
2 { AddParameter Sysprep }
|
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
|
SaveSettings
|
||||||
@@ -1452,7 +1675,6 @@ function Show-MainWindow {
|
|||||||
|
|
||||||
# Handle Load Last Used settings and Load Last Used apps
|
# Handle Load Last Used settings and Load Last Used apps
|
||||||
$loadLastUsedBtn = $window.FindName('LoadLastUsedBtn')
|
$loadLastUsedBtn = $window.FindName('LoadLastUsedBtn')
|
||||||
$loadLastUsedAppsBtn = $window.FindName('LoadLastUsedAppsBtn')
|
|
||||||
|
|
||||||
$lastUsedSettingsJson = LoadJsonFile -filePath $script:SavedSettingsFilePath -expectedVersion "1.0" -optionalFile
|
$lastUsedSettingsJson = LoadJsonFile -filePath $script:SavedSettingsFilePath -expectedVersion "1.0" -optionalFile
|
||||||
|
|
||||||
@@ -1481,28 +1703,24 @@ function Show-MainWindow {
|
|||||||
$loadLastUsedBtn.Visibility = 'Collapsed'
|
$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) {
|
if ($appsSetting -and $appsSetting.ToString().Trim().Length -gt 0) {
|
||||||
$loadLastUsedAppsBtn.Add_Click({
|
# Parse and store saved app IDs for UpdatePresetStates
|
||||||
try {
|
$script:SavedAppIds = @()
|
||||||
$savedApps = @()
|
if ($appsSetting -is [string]) { $script:SavedAppIds = $appsSetting.Split(',') }
|
||||||
if ($appsSetting -is [string]) { $savedApps = $appsSetting.Split(',') }
|
elseif ($appsSetting -is [array]) { $script:SavedAppIds = $appsSetting }
|
||||||
elseif ($appsSetting -is [array]) { $savedApps = $appsSetting }
|
$script:SavedAppIds = $script:SavedAppIds | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }
|
||||||
$savedApps = $savedApps | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }
|
|
||||||
|
|
||||||
foreach ($child in $appsPanel.Children) {
|
$presetLastUsed.Add_Click({
|
||||||
if ($child -is [System.Windows.Controls.CheckBox]) {
|
if ($script:UpdatingPresets) { return }
|
||||||
if ($savedApps -contains $child.Tag) { $child.IsChecked = $true } else { $child.IsChecked = $false }
|
$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
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Show-MessageBox -Message "Failed to load last used app selection: $_" -Title "Error" -Button 'OK' -Icon 'Error'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$loadLastUsedAppsBtn.Visibility = 'Collapsed'
|
$script:SavedAppIds = $null
|
||||||
|
$presetLastUsed.Visibility = 'Collapsed'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Clear All Tweaks button
|
# Clear All Tweaks button
|
||||||
@@ -1522,6 +1740,14 @@ function Show-MainWindow {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# 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
|
# Show the window
|
||||||
return $window.ShowDialog()
|
return $window.ShowDialog()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ if (Test-Path "$env:TEMP/Win11Debloat") {
|
|||||||
Write-Output "> Cleaning up..."
|
Write-Output "> Cleaning up..."
|
||||||
|
|
||||||
# Cleanup, remove Win11Debloat directory
|
# 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 ""
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
497
Win11Debloat.ps1
Executable file → Normal file
497
Win11Debloat.ps1
Executable file → Normal file
@@ -119,6 +119,7 @@ $script:AboutWindowSchema = "$PSScriptRoot/Schemas/AboutWindow.xaml"
|
|||||||
$script:ApplyChangesWindowSchema = "$PSScriptRoot/Schemas/ApplyChangesWindow.xaml"
|
$script:ApplyChangesWindowSchema = "$PSScriptRoot/Schemas/ApplyChangesWindow.xaml"
|
||||||
$script:SharedStylesSchema = "$PSScriptRoot/Schemas/SharedStyles.xaml"
|
$script:SharedStylesSchema = "$PSScriptRoot/Schemas/SharedStyles.xaml"
|
||||||
$script:BubbleHintSchema = "$PSScriptRoot/Schemas/BubbleHint.xaml"
|
$script:BubbleHintSchema = "$PSScriptRoot/Schemas/BubbleHint.xaml"
|
||||||
|
$script:LoadAppsDetailsScriptPath = "$PSScriptRoot/Scripts/FileIO/LoadAppsDetailsFromJson.ps1"
|
||||||
|
|
||||||
$script:ControlParams = 'WhatIf', 'Confirm', 'Verbose', 'Debug', 'LogPath', 'Silent', 'Sysprep', 'User', 'NoRestartExplorer', 'RunDefaults', 'RunDefaultsLite', 'RunSavedSettings', 'RunAppsListGenerator', 'CLI', 'AppRemovalTarget'
|
$script:ControlParams = 'WhatIf', 'Confirm', 'Verbose', 'Debug', 'LogPath', 'Silent', 'Sysprep', 'User', 'NoRestartExplorer', 'RunDefaults', 'RunDefaultsLite', 'RunSavedSettings', 'RunAppsListGenerator', 'CLI', 'AppRemovalTarget'
|
||||||
|
|
||||||
@@ -218,15 +219,16 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
|||||||
|
|
||||||
##################################################################################################################
|
##################################################################################################################
|
||||||
# #
|
# #
|
||||||
# FUNCTION IMPORTS/DEFINITIONS #
|
# FUNCTION IMPORTS #
|
||||||
# #
|
# #
|
||||||
##################################################################################################################
|
##################################################################################################################
|
||||||
|
|
||||||
# Load app removal functions
|
# App removal functions
|
||||||
. "$PSScriptRoot/Scripts/AppRemoval/ForceRemoveEdge.ps1"
|
. "$PSScriptRoot/Scripts/AppRemoval/ForceRemoveEdge.ps1"
|
||||||
. "$PSScriptRoot/Scripts/AppRemoval/RemoveApps.ps1"
|
. "$PSScriptRoot/Scripts/AppRemoval/RemoveApps.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/AppRemoval/GetInstalledAppsViaWinget.ps1"
|
||||||
|
|
||||||
# Load CLI functions
|
# CLI functions
|
||||||
. "$PSScriptRoot/Scripts/CLI/AwaitKeyToExit.ps1"
|
. "$PSScriptRoot/Scripts/CLI/AwaitKeyToExit.ps1"
|
||||||
. "$PSScriptRoot/Scripts/CLI/ShowCLILastUsedSettings.ps1"
|
. "$PSScriptRoot/Scripts/CLI/ShowCLILastUsedSettings.ps1"
|
||||||
. "$PSScriptRoot/Scripts/CLI/ShowCLIDefaultModeAppRemovalOptions.ps1"
|
. "$PSScriptRoot/Scripts/CLI/ShowCLIDefaultModeAppRemovalOptions.ps1"
|
||||||
@@ -236,7 +238,8 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
|||||||
. "$PSScriptRoot/Scripts/CLI/PrintPendingChanges.ps1"
|
. "$PSScriptRoot/Scripts/CLI/PrintPendingChanges.ps1"
|
||||||
. "$PSScriptRoot/Scripts/CLI/PrintHeader.ps1"
|
. "$PSScriptRoot/Scripts/CLI/PrintHeader.ps1"
|
||||||
|
|
||||||
# Load Feature functions
|
# Features functions
|
||||||
|
. "$PSScriptRoot/Scripts/Features/ExecuteChanges.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Features/CreateSystemRestorePoint.ps1"
|
. "$PSScriptRoot/Scripts/Features/CreateSystemRestorePoint.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Features/DisableStoreSearchSuggestions.ps1"
|
. "$PSScriptRoot/Scripts/Features/DisableStoreSearchSuggestions.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Features/EnableWindowsFeature.ps1"
|
. "$PSScriptRoot/Scripts/Features/EnableWindowsFeature.ps1"
|
||||||
@@ -244,7 +247,17 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
|||||||
. "$PSScriptRoot/Scripts/Features/ReplaceStartMenu.ps1"
|
. "$PSScriptRoot/Scripts/Features/ReplaceStartMenu.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Features/RestartExplorer.ps1"
|
. "$PSScriptRoot/Scripts/Features/RestartExplorer.ps1"
|
||||||
|
|
||||||
# Load GUI functions
|
# File I/O functions
|
||||||
|
. "$PSScriptRoot/Scripts/FileIO/LoadJsonFile.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/FileIO/SaveSettings.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/FileIO/LoadSettings.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/FileIO/SaveCustomAppsListToFile.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/FileIO/ValidateAppslist.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/FileIO/LoadAppsFromFile.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/FileIO/LoadAppsDetailsFromJson.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/FileIO/LoadAppPresetsFromJson.ps1"
|
||||||
|
|
||||||
|
# GUI functions
|
||||||
. "$PSScriptRoot/Scripts/GUI/GetSystemUsesDarkMode.ps1"
|
. "$PSScriptRoot/Scripts/GUI/GetSystemUsesDarkMode.ps1"
|
||||||
. "$PSScriptRoot/Scripts/GUI/SetWindowThemeResources.ps1"
|
. "$PSScriptRoot/Scripts/GUI/SetWindowThemeResources.ps1"
|
||||||
. "$PSScriptRoot/Scripts/GUI/AttachShiftClickBehavior.ps1"
|
. "$PSScriptRoot/Scripts/GUI/AttachShiftClickBehavior.ps1"
|
||||||
@@ -256,467 +269,19 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
|||||||
. "$PSScriptRoot/Scripts/GUI/Show-AboutDialog.ps1"
|
. "$PSScriptRoot/Scripts/GUI/Show-AboutDialog.ps1"
|
||||||
. "$PSScriptRoot/Scripts/GUI/Show-Bubble.ps1"
|
. "$PSScriptRoot/Scripts/GUI/Show-Bubble.ps1"
|
||||||
|
|
||||||
# Load File I/O functions
|
# Helper functions
|
||||||
. "$PSScriptRoot/Scripts/FileIO/LoadJsonFile.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/AddParameter.ps1"
|
||||||
. "$PSScriptRoot/Scripts/FileIO/SaveSettings.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/CheckIfUserExists.ps1"
|
||||||
. "$PSScriptRoot/Scripts/FileIO/LoadSettings.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/CheckModernStandbySupport.ps1"
|
||||||
. "$PSScriptRoot/Scripts/FileIO/SaveCustomAppsListToFile.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/GenerateAppsList.ps1"
|
||||||
. "$PSScriptRoot/Scripts/FileIO/ValidateAppslist.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/GetFriendlyTargetUserName.ps1"
|
||||||
. "$PSScriptRoot/Scripts/FileIO/LoadAppsFromFile.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/GetTargetUserForAppRemoval.ps1"
|
||||||
. "$PSScriptRoot/Scripts/FileIO/LoadAppsDetailsFromJson.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/GetUserDirectory.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/Helpers/GetUserName.ps1"
|
||||||
# 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().
|
# Threading functions
|
||||||
function DoEvents {
|
. "$PSScriptRoot/Scripts/Threading/DoEvents.ps1"
|
||||||
if (-not $script:GuiWindow) { return }
|
. "$PSScriptRoot/Scripts/Threading/Invoke-NonBlocking.ps1"
|
||||||
$frame = [System.Windows.Threading.DispatcherFrame]::new()
|
|
||||||
[System.Windows.Threading.Dispatcher]::CurrentDispatcher.BeginInvoke(
|
|
||||||
[System.Windows.Threading.DispatcherPriority]::Background,
|
|
||||||
[System.Windows.Threading.DispatcherOperationCallback]{
|
|
||||||
param($f)
|
|
||||||
$f.Continue = $false
|
|
||||||
return $null
|
|
||||||
},
|
|
||||||
$frame
|
|
||||||
)
|
|
||||||
[System.Windows.Threading.Dispatcher]::PushFrame($frame)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 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 = @()
|
|
||||||
)
|
|
||||||
|
|
||||||
if (-not $script:GuiWindow) {
|
|
||||||
return (& $ScriptBlock @ArgumentList)
|
|
||||||
}
|
|
||||||
|
|
||||||
$ps = [powershell]::Create()
|
|
||||||
try {
|
|
||||||
$null = $ps.AddScript($ScriptBlock.ToString())
|
|
||||||
foreach ($arg in $ArgumentList) {
|
|
||||||
$null = $ps.AddArgument($arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
$handle = $ps.BeginInvoke()
|
|
||||||
|
|
||||||
while (-not $handle.IsCompleted) {
|
|
||||||
DoEvents
|
|
||||||
Start-Sleep -Milliseconds 16
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $ps.EndInvoke($handle)
|
|
||||||
|
|
||||||
if ($result.Count -eq 0) { return $null }
|
|
||||||
if ($result.Count -eq 1) { return $result[0] }
|
|
||||||
return @($result)
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
$ps.Dispose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Run winget list and return installed apps (sync or async)
|
|
||||||
function GetInstalledAppsViaWinget {
|
|
||||||
param (
|
|
||||||
[int]$TimeOut = 10,
|
|
||||||
[switch]$Async
|
|
||||||
)
|
|
||||||
|
|
||||||
if (-not $script:WingetInstalled) { return $null }
|
|
||||||
|
|
||||||
if ($Async) {
|
|
||||||
$wingetListJob = Start-Job { return winget list --accept-source-agreements --disable-interactivity }
|
|
||||||
return @{ Job = $wingetListJob; StartTime = Get-Date }
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$wingetListJob = Start-Job { return winget list --accept-source-agreements --disable-interactivity }
|
|
||||||
$jobDone = $wingetListJob | Wait-Job -TimeOut $TimeOut
|
|
||||||
if (-not $jobDone) {
|
|
||||||
Remove-Job -Job $wingetListJob -Force -ErrorAction SilentlyContinue
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$result = Receive-Job -Job $wingetListJob
|
|
||||||
Remove-Job -Job $wingetListJob -ErrorAction SilentlyContinue
|
|
||||||
return $result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function GetUserName {
|
|
||||||
if ($script:Params.ContainsKey("User")) {
|
|
||||||
return $script:Params.Item("User")
|
|
||||||
}
|
|
||||||
|
|
||||||
return $env:USERNAME
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 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"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function GetFriendlyTargetUserName {
|
|
||||||
$target = GetTargetUserForAppRemoval
|
|
||||||
|
|
||||||
switch ($target) {
|
|
||||||
"AllUsers" { return "all users" }
|
|
||||||
"CurrentUser" { return "the current user" }
|
|
||||||
default { return "user $target" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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
|
|
||||||
# Parameters:
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user