25 Commits

Author SHA1 Message Date
Raphire
c79c05f286 Refactor feature handling, update format in Features.json 2026-04-01 21:33:24 +02:00
Raphire
105198e396 Set Topmost property to False in RevertSettingsWindow 2026-03-27 20:42:03 +01:00
Raphire
f8f85ca861 Refactor checkbox style assignment in config window 2026-03-27 20:36:16 +01:00
Raphire
bd16457552 Merge branch 'master' into undo-tweaks 2026-03-27 20:34:41 +01:00
Jeffrey
774c8ecd92 Add ability to export/import settings configuration (#522) 2026-03-27 20:33:24 +01:00
Raphire
ad225cdf9d Adjust margin for revert button 2026-03-23 23:21:08 +01:00
Jeffrey
e05af92acc Add support for multiple AppIds for app removal (#526) 2026-03-23 22:59:04 +01:00
Raphire
2eddbe5638 Refactor/clean up window styles 2026-03-23 21:35:33 +01:00
Raphire
b5c576519b Remove unnecessary parameter handling 2026-03-23 21:08:16 +01:00
Raphire
b1cf364c7d Don't allow undo in combination with deployment-targeted parameters 2026-03-23 21:03:22 +01:00
Raphire
bc8fc1a284 Set default launch mode to CLI for deployment-targeted parameters 2026-03-23 20:56:22 +01:00
Raphire
e9bccccc09 Add error handling for ExecuteAllChanges to improve robustness 2026-03-23 20:47:46 +01:00
Raphire
b0125ddcd2 Refactor undo handling in ExecuteParameter function to improve clarity and error handling 2026-03-23 20:47:32 +01:00
Raphire
c15a18c376 Remove unnecessary call to updateState in Show-RevertSettingsModal function 2026-03-23 00:00:06 +01:00
Raphire
85bdf765e5 Remove unnecessary assignment of SelectedFeatureIds and RestartExplorer in cancel handler 2026-03-22 23:58:21 +01:00
Raphire
bcfed9daff Set RestartExplorer to false by default in Show-RevertSettingsModal function 2026-03-22 23:57:19 +01:00
Raphire
91a9beed0c Also check for required regfiles and assets 2026-03-22 23:48:56 +01:00
Raphire
cfc868ba91 Fix undo when using presets 2026-03-22 23:44:59 +01:00
Raphire
a54c3c6918 Add option to revert previous changes to windows defaults 2026-03-22 22:07:51 +01:00
Raphire
edd815fdbb Add shadow to bubble hint 2026-03-18 23:32:10 +01:00
Raphire
17ee530962 Refactor ImportRegistryFile function to improve error handling and streamline registry file validation 2026-03-18 22:49:44 +01:00
Raphire
999e442658 Simplify user validation messages and add user logged-in check function 2026-03-18 22:39:49 +01:00
Raphire
1d4cf4a801 Fix: Skip confirmation when running last used settings with -Silent parameter #521 2026-03-18 19:28:47 +01:00
Raphire
7a3431e56b Close app preset menu when main window loses focus 2026-03-17 21:28:35 +01:00
Raphire
1b41f05743 Fix inconsistent search highlighting after sorting 2026-03-16 19:25:53 +01:00
36 changed files with 2135 additions and 697 deletions

View File

@@ -647,7 +647,7 @@
},
{
"FriendlyName": "Microsoft Edge",
"AppId": "Microsoft.Edge",
"AppId": ["Microsoft.Edge", "XPFFTQ037JWMHS"],
"Description": "Windows' default browser, WARNING: Removing this app also removes the only browser from Windows Sandbox and could affect other apps",
"SelectedByDefault": false,
"Recommendation": "unsafe"

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent"
Topmost="True"
Topmost="False"
ShowInTaskbar="False">
<Border BorderBrush="{DynamicResource BorderColor}"

View File

@@ -9,55 +9,6 @@
Background="Transparent"
Foreground="{DynamicResource FgColor}">
<Window.Resources>
<!-- CheckBox Style -->
<Style TargetType="CheckBox">
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="4,2"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CheckBox">
<Border Background="{TemplateBinding Background}" BorderThickness="0" CornerRadius="4" Padding="{TemplateBinding Padding}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</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">
<TextBlock x:Name="CheckMark" Text="&#xE73E;" FontFamily="Segoe MDL2 Assets" FontSize="12" Foreground="{DynamicResource ButtonBg}" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Collapsed"/>
</Border>
<ContentPresenter Grid.Column="1" VerticalAlignment="Center" Margin="0,0,0,1"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource CheckBoxHoverColor}"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="CheckMark" Property="Visibility" Value="Visible"/>
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonBg}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonBg}"/>
<Setter TargetName="CheckMark" Property="Foreground" Value="White"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsChecked" Value="True"/>
</MultiTrigger.Conditions>
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonHover}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonHover}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- CheckBox style for apps panels -->
<Style x:Key="AppsPanelCheckBoxStyle" TargetType="CheckBox" BasedOn="{StaticResource {x:Type CheckBox}}">
<Setter Property="Margin" Value="2,3,2,3"/>
</Style>
<!-- Title Bar Button Style -->
<Style x:Key="TitleBarButton" TargetType="Button">
<Setter Property="Background" Value="Transparent"/>

View File

@@ -8,7 +8,7 @@
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent"
Topmost="True"
Topmost="False"
ShowInTaskbar="False">
<Border BorderBrush="{DynamicResource BorderColor}"
@@ -180,20 +180,20 @@
<StackPanel x:Name="ButtonPanel"
Orientation="Horizontal"
HorizontalAlignment="Center">
<Button x:Name="ApplyKofiBtn" Width="210" Height="32"
<Button x:Name="ApplyKofiBtn"
Width="200" Height="32" Margin="4,0"
Style="{DynamicResource SecondaryButtonStyle}"
Margin="0,0,12,0"
AutomationProperties.Name="Support the creator">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="&#xEB52;" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,0,8,-1"/>
<TextBlock Text="Support the creator" VerticalAlignment="Center" FontSize="14" Margin="0,0,0,1"/>
</StackPanel>
</Button>
<Button x:Name="ApplyCloseBtn" Width="100" Height="32"
<Button x:Name="ApplyCloseBtn"
Content="Close"
Width="200" Height="32" Margin="4,0"
Style="{DynamicResource PrimaryButtonStyle}"
AutomationProperties.Name="Close">
<TextBlock Text="Close" VerticalAlignment="Center" FontSize="14" Margin="0,0,0,1"/>
</Button>
AutomationProperties.Name="Close"/>
</StackPanel>
</Border>
</StackPanel>

View File

@@ -7,23 +7,33 @@
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border Name="BubbleBorder"
Grid.Row="0"
Background="{DynamicResource CardBgColor}"
BorderBrush="{DynamicResource ButtonBorderColor}"
BorderThickness="1"
CornerRadius="8"
Padding="10,7,10,7">
<Grid Grid.Row="0"
Margin="10,10,10,8">
<Border Name="BubbleBorder"
Background="{DynamicResource CardBgColor}"
BorderBrush="{DynamicResource ButtonBorderColor}"
BorderThickness="1"
CornerRadius="8"
Padding="10,7,10,7">
<Border.Effect>
<DropShadowEffect BlurRadius="9"
Opacity="0.16"
ShadowDepth="2"
Direction="270"
Color="Black"/>
</Border.Effect>
<TextBlock Name="BubbleText"
Text="View the selected changes here"
TextWrapping="Wrap"
MaxWidth="260"
Foreground="{DynamicResource FgColor}"/>
</Border>
Text="View the selected changes here"
TextWrapping="Wrap"
MaxWidth="260"
Foreground="{DynamicResource FgColor}"/>
</Border>
</Grid>
<Grid Grid.Row="1"
HorizontalAlignment="Center"
Margin="0,-1,0,0"
Margin="0,-9,0,0"
Panel.ZIndex="1"
Width="12"
Height="8">
<Polygon Name="BubblePointer"

View File

@@ -0,0 +1,77 @@
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Select Settings to Import/Export"
Width="440"
SizeToContent="Height"
MaxHeight="501"
ResizeMode="NoResize"
WindowStartupLocation="CenterOwner"
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent"
Topmost="False"
ShowInTaskbar="False">
<Border BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1"
CornerRadius="8"
Background="{DynamicResource CardBgColor}"
Margin="25">
<Border.Effect>
<DropShadowEffect Color="Black"
Opacity="0.15"
BlurRadius="20"
ShadowDepth="0"
Direction="0"/>
</Border.Effect>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Title Bar -->
<Grid Grid.Row="0" x:Name="TitleBar" Height="40" Background="Transparent">
<TextBlock x:Name="TitleText"
Foreground="{DynamicResource FgColor}"
FontSize="16"
FontWeight="SemiBold"
VerticalAlignment="Center"
Margin="16,0,0,0"/>
</Grid>
<!-- Content -->
<StackPanel Grid.Row="1" x:Name="ContentPanel" Margin="20,12,20,9">
<TextBlock x:Name="PromptText"
TextWrapping="Wrap"
FontSize="14"
LineHeight="20"
Foreground="{DynamicResource FgColor}"
Margin="0,0,0,14"/>
<!-- Checkboxes are added dynamically at runtime -->
<StackPanel x:Name="CheckboxPanel"/>
</StackPanel>
<!-- Button Footer -->
<Border Grid.Row="2"
Background="{DynamicResource BgColor}"
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="0,1,0,0"
Padding="16,12"
CornerRadius="0,0,8,8">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="OkButton"
Content="OK"
Height="32" MinWidth="80" Margin="4,0"
Style="{DynamicResource PrimaryButtonStyle}"/>
<Button x:Name="CancelButton"
Content="Cancel"
Height="32" MinWidth="80" Margin="4,0"
Style="{DynamicResource SecondaryButtonStyle}"/>
</StackPanel>
</Border>
</Grid>
</Border>
</Window>

View File

@@ -336,89 +336,6 @@
</Style.Triggers>
</Style>
<!-- CheckBox Style -->
<Style TargetType="CheckBox">
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="4,2"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CheckBox">
<Border Background="{TemplateBinding Background}" BorderThickness="0" CornerRadius="4" Padding="{TemplateBinding Padding}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</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">
<Grid>
<TextBlock x:Name="CheckMark" Text="&#xE73E;" FontFamily="Segoe Fluent Icons" FontSize="12" Foreground="{DynamicResource ButtonBg}" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Collapsed"/>
<TextBlock x:Name="IndeterminateMark" Text="&#xE738;" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource ButtonBg}" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Collapsed" Margin="1,1,0,0" />
</Grid>
</Border>
<ContentPresenter Grid.Column="1" VerticalAlignment="Center" Margin="0,0,0,2"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource CheckBoxHoverColor}"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="CheckMark" Property="Visibility" Value="Visible"/>
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonBg}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonBg}"/>
<Setter TargetName="CheckMark" Property="Foreground" Value="White"/>
</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">
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonDisabled}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
<Setter Property="Foreground" Value="{DynamicResource ButtonTextDisabled}"/>
<Setter Property="Opacity" Value="0.6"/>
<Setter TargetName="CheckMark" Property="Foreground" Value="{DynamicResource ButtonTextDisabled}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsChecked" Value="True"/>
</MultiTrigger.Conditions>
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonHover}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonHover}"/>
</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>
</Setter.Value>
</Setter>
</Style>
<!-- CheckBox style for feature toggles -->
<Style x:Key="FeatureCheckboxStyle" TargetType="CheckBox" BasedOn="{StaticResource {x:Type CheckBox}}">
<Setter Property="Margin" Value="-4,-2,-4,10"/>
<Setter Property="Padding" Value="4,2"/>
</Style>
<!-- CheckBox style for apps panels -->
<Style x:Key="AppsPanelCheckBoxStyle" TargetType="CheckBox" BasedOn="{StaticResource {x:Type CheckBox}}">
<Setter Property="Margin" Value="2,3,2,3"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style>
<!-- TextBlock style for App ID column in apps table -->
<Style x:Key="AppIdTextStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource AppIdColor}"/>
@@ -458,11 +375,6 @@
<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"/>
@@ -649,6 +561,29 @@
<Button x:Name="MenuBtn" Content="&#xE700;" FontFamily="Segoe Fluent Icons" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="Options" AutomationProperties.Name="Options">
<Button.ContextMenu>
<ContextMenu x:Name="MainMenu">
<ContextMenu.Resources>
<Style x:Key="{x:Static MenuItem.SeparatorStyleKey}" TargetType="Separator">
<Setter Property="Margin" Value="4,6"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Separator">
<Border Height="1" Background="{DynamicResource BorderColor}" SnapsToDevicePixels="True" HorizontalAlignment="Stretch"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ContextMenu.Resources>
<MenuItem x:Name="ImportConfigBtn" Header="Import config" AutomationProperties.Name="Import configuration">
<MenuItem.Icon>
<TextBlock Text="&#xe838;" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="ExportConfigBtn" Header="Export config" AutomationProperties.Name="Export configuration">
<MenuItem.Icon>
<TextBlock Text="&#xe74e;" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem x:Name="MenuDocumentation" Header="Documentation" AutomationProperties.Name="Documentation">
<MenuItem.Icon>
<TextBlock Text="&#xe736;" FontFamily="Segoe MDL2 Assets" FontSize="16" Foreground="{DynamicResource FgColor}"/>
@@ -753,6 +688,11 @@
</StackPanel>
</Button>
</StackPanel>
<!-- Revert link -->
<Button x:Name="HomeRevertLinkBtn" HorizontalAlignment="Center" Margin="0,11,0,0" Style="{DynamicResource ActionLinkButtonStyle}" AutomationProperties.Name="Revert previous changes">
<TextBlock Text="Revert previous changes" Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
</Button>
</StackPanel>
</Grid>
</TabItem>
@@ -1106,12 +1046,12 @@
<!-- Restore Point Option -->
<StackPanel>
<CheckBox x:Name="RestorePointCheckBox" Style="{StaticResource FeatureCheckboxStyle}" IsChecked="True" Content="Create a system restore point (Recommended)" AutomationProperties.Name="Create a system restore point (Recommended)"/>
<CheckBox x:Name="RestorePointCheckBox" Style="{DynamicResource FeatureCheckboxStyle}" IsChecked="True" Content="Create a system restore point (Recommended)" AutomationProperties.Name="Create a system restore point (Recommended)"/>
</StackPanel>
<!-- Restart Explorer Option -->
<StackPanel>
<CheckBox x:Name="RestartExplorerCheckBox" Style="{StaticResource FeatureCheckboxStyle}" Content="Restart the Windows Explorer process to apply all changes immediately" AutomationProperties.Name="Restart the Windows Explorer process to apply all changes immediately"/>
<CheckBox x:Name="RestartExplorerCheckBox" Style="{DynamicResource FeatureCheckboxStyle}" Content="Restart the Windows Explorer process to apply all changes immediately" AutomationProperties.Name="Restart the Windows Explorer process to apply all changes immediately"/>
</StackPanel>
</StackPanel>
</Border>
@@ -1140,20 +1080,8 @@
<!-- Review & Apply Section -->
<StackPanel Grid.Row="1" HorizontalAlignment="Stretch" Background="{DynamicResource BgColor}">
<Button x:Name="ReviewChangesBtn" Background="Transparent" BorderThickness="0" Cursor="Hand" HorizontalAlignment="Center" Margin="0,4,0,10" AutomationProperties.Name="Review selected changes">
<Button.Template>
<ControlTemplate TargetType="Button">
<TextBlock x:Name="LinkText" Text="Review selected changes" FontSize="14" Foreground="{DynamicResource ButtonBg}" FontWeight="SemiBold" HorizontalAlignment="Center"/>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="LinkText" Property="Foreground" Value="{DynamicResource ButtonHover}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="LinkText" Property="Foreground" Value="{DynamicResource ButtonPressed}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
<Button x:Name="ReviewChangesBtn" Style="{DynamicResource ActionLinkButtonStyle}" HorizontalAlignment="Center" Margin="0,4,0,10" AutomationProperties.Name="Review selected changes">
<TextBlock Text="Review selected changes" Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
</Button>
<Button x:Name="DeploymentApplyBtn" Style="{DynamicResource PrimaryButtonStyle}" Width="190" Height="44" HorizontalAlignment="Center" AutomationProperties.Name="Apply Changes">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">

View File

@@ -9,7 +9,7 @@
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent"
Topmost="True"
Topmost="False"
ShowInTaskbar="False">
<Border BorderBrush="{DynamicResource BorderColor}"

View File

@@ -0,0 +1,160 @@
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Revert Changes"
Width="620"
Height="620"
MinWidth="560"
MinHeight="420"
ResizeMode="CanResize"
WindowStartupLocation="CenterOwner"
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent"
Topmost="False"
ShowInTaskbar="False">
<Window.Resources>
<Style x:Key="RevertItemBorderStyle" TargetType="Border">
<Setter Property="BorderThickness" Value="0,0,0,1"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
<Setter Property="Padding" Value="4,6,4,6"/>
</Style>
<Style x:Key="RevertItemRowStyle" TargetType="StackPanel">
<Setter Property="Orientation" Value="Vertical"/>
</Style>
<Style x:Key="RevertItemUndoTextStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Opacity" Value="0.75"/>
<Setter Property="Margin" Value="26,-8,0,3"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
<Style x:Key="RevertEmptyTextStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Opacity" Value="0.85"/>
<Setter Property="Margin" Value="6,0,6,0"/>
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
</Window.Resources>
<Border BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1"
CornerRadius="8"
Background="{DynamicResource CardBgColor}"
Margin="25">
<Border.Effect>
<DropShadowEffect Color="Black"
Opacity="0.15"
BlurRadius="20"
ShadowDepth="0"
Direction="0"/>
</Border.Effect>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" x:Name="TitleBar" Height="40" Background="Transparent">
<TextBlock x:Name="TitleText"
Text="Revert Changes"
Foreground="{DynamicResource FgColor}"
FontSize="16"
FontWeight="SemiBold"
VerticalAlignment="Center"
Margin="16,0,0,0"/>
</Grid>
<StackPanel Grid.Row="1" Margin="20,12,1,12">
<TextBlock x:Name="MessageText"
Grid.Column="1"
Text="Select which previously applied tweaks should be reverted to Windows defaults."
TextWrapping="Wrap"
FontSize="14"
LineHeight="20"
Foreground="{DynamicResource FgColor}"
VerticalAlignment="Center"/>
</StackPanel>
<Grid Grid.Row="2" Margin="20,0,20,10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<DockPanel Grid.Row="0" Margin="0,0,0,8">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left">
<Button x:Name="RevertSelectAllBtn"
Content="Select All"
Height="30"
Padding="12,0"
Style="{DynamicResource SecondaryButtonStyle}"
Margin="0,0,8,0"
AutomationProperties.Name="Select All"/>
<Button x:Name="RevertClearBtn"
Content="Clear"
Height="30"
Padding="12,0"
Style="{DynamicResource SecondaryButtonStyle}"
AutomationProperties.Name="Clear"/>
</StackPanel>
<TextBlock x:Name="RevertSelectionCount"
Text="0 settings selected"
Foreground="{DynamicResource FgColor}"
VerticalAlignment="Center"
HorizontalAlignment="Right"
DockPanel.Dock="Right"
Margin="0,0,6,0"/>
</DockPanel>
<Border Grid.Row="1"
BorderThickness="1"
BorderBrush="{DynamicResource BorderColor}"
CornerRadius="6"
Background="{DynamicResource BgColor}">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" Padding="8" Margin="0,1,1,1">
<StackPanel x:Name="RevertItemsPanel"/>
</ScrollViewer>
</Border>
<CheckBox x:Name="RevertRestartExplorerCheckBox"
Grid.Row="2"
Content="Restart the Windows Explorer process to apply all changes immediately"
IsChecked="False"
Margin="2,10,0,0"
AutomationProperties.Name="Restart the Windows Explorer process to apply all changes immediately"/>
</Grid>
<!-- Button Panel -->
<Border Grid.Row="3"
Background="{DynamicResource BgColor}"
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="0,1,0,0"
Padding="16,12"
CornerRadius="0,0,8,8">
<StackPanel x:Name="ButtonPanel"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button x:Name="RevertApplyBtn"
Content="Revert Selected Settings"
Height="32" MinWidth="200" Margin="4,0"
Style="{DynamicResource PrimaryButtonStyle}"
IsEnabled="False"
AutomationProperties.Name="Revert Selected Settings"/>
<Button x:Name="RevertCancelBtn"
Content="Cancel"
Height="32" MinWidth="80" Margin="4,0"
Style="{DynamicResource SecondaryButtonStyle}"
AutomationProperties.Name="Cancel"/>
</StackPanel>
</Border>
</Grid>
</Border>
</Window>

View File

@@ -18,7 +18,7 @@
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,0,0,1"/>
</Border>
</ControlTemplate>
</Setter.Value>
@@ -90,6 +90,32 @@
</Style.Triggers>
</Style>
<!-- Shared link-style button used for text actions like review/revert links -->
<Style x:Key="ActionLinkButtonStyle" TargetType="Button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Foreground" Value="{DynamicResource ButtonBg}"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="{DynamicResource ButtonHover}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Foreground" Value="{DynamicResource ButtonPressed}"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- ProgressBar Style -->
<Style x:Key="ApplyProgressBarStyle" TargetType="ProgressBar">
<Setter Property="Background" Value="{DynamicResource ButtonBorderColor}"/>
@@ -125,6 +151,138 @@
<Setter Property="TextAlignment" Value="Center"/>
</Style>
<!-- Base CheckBox style used across windows -->
<Style TargetType="CheckBox">
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="4,2"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CheckBox">
<Border Background="{TemplateBinding Background}" BorderThickness="0" CornerRadius="4" Padding="{TemplateBinding Padding}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</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">
<Grid>
<TextBlock x:Name="CheckMark" Text="&#xE73E;" FontFamily="Segoe Fluent Icons" FontSize="12" Foreground="{DynamicResource ButtonBg}" HorizontalAlignment="Center" VerticalAlignment="Center" Opacity="0">
<TextBlock.Clip>
<RectangleGeometry x:Name="CheckMarkClip" Rect="0,0,0,16"/>
</TextBlock.Clip>
</TextBlock>
<TextBlock x:Name="IndeterminateMark" Text="&#xE738;" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource ButtonBg}" HorizontalAlignment="Center" VerticalAlignment="Center" Opacity="0" Margin="1,1,0,0">
<TextBlock.Clip>
<RectangleGeometry x:Name="IndeterminateMarkClip" Rect="0,0,0,16"/>
</TextBlock.Clip>
</TextBlock>
</Grid>
</Border>
<ContentPresenter Grid.Column="1" VerticalAlignment="Center" Margin="0,0,0,2"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource CheckBoxHoverColor}"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonBg}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonBg}"/>
<Setter TargetName="CheckMark" Property="Foreground" Value="White"/>
<Setter TargetName="CheckMark" Property="Opacity" Value="1"/>
<Setter TargetName="IndeterminateMark" Property="Opacity" Value="0"/>
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="CheckMark" Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.25" FillBehavior="HoldEnd"/>
<RectAnimation Storyboard.TargetName="CheckMarkClip" Storyboard.TargetProperty="Rect" From="0,0,0,16" To="0,0,16,16" Duration="0:0:0.25" FillBehavior="HoldEnd"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<RectAnimation Storyboard.TargetName="CheckMarkClip" Storyboard.TargetProperty="Rect" From="0,0,16,16" To="16,0,0,16" Duration="0:0:0.15" FillBehavior="HoldEnd"/>
<DoubleAnimation Storyboard.TargetName="CheckMark" Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:0.15" FillBehavior="HoldEnd"/>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
<Trigger Property="IsChecked" Value="{x:Null}">
<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"/>
<Setter TargetName="IndeterminateMark" Property="Opacity" Value="1"/>
<Setter TargetName="CheckMark" Property="Opacity" Value="0"/>
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="IndeterminateMark" Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.25" FillBehavior="HoldEnd"/>
<RectAnimation Storyboard.TargetName="IndeterminateMarkClip" Storyboard.TargetProperty="Rect" From="0,0,0,16" To="0,0,16,16" Duration="0:0:0.25" FillBehavior="HoldEnd"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<RectAnimation Storyboard.TargetName="IndeterminateMarkClip" Storyboard.TargetProperty="Rect" From="0,0,16,16" To="16,0,0,16" Duration="0:0:0.15" FillBehavior="HoldEnd"/>
<DoubleAnimation Storyboard.TargetName="IndeterminateMark" Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:0.15" FillBehavior="HoldEnd"/>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
<Trigger Property="IsChecked" Value="False">
<Setter TargetName="CheckMark" Property="Opacity" Value="0"/>
<Setter TargetName="IndeterminateMark" Property="Opacity" Value="0"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonDisabled}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
<Setter Property="Opacity" Value="0.4"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsChecked" Value="True"/>
</MultiTrigger.Conditions>
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonHover}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonHover}"/>
</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>
</Setter.Value>
</Setter>
</Style>
<!-- Shared checkbox style used for feature toggles across windows -->
<Style x:Key="FeatureCheckboxStyle" TargetType="CheckBox" BasedOn="{StaticResource {x:Type CheckBox}}">
<Setter Property="Margin" Value="-4,-2,-4,10"/>
<Setter Property="Padding" Value="4,2"/>
</Style>
<!-- Shared CheckBox style for app list items -->
<Style x:Key="AppsPanelCheckBoxStyle" TargetType="CheckBox" BasedOn="{StaticResource {x:Type CheckBox}}">
<Setter Property="Margin" Value="2,3,2,3"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style>
<!-- Shared CheckBox style for preset picker entries -->
<Style x:Key="PresetCheckBoxStyle" TargetType="CheckBox" BasedOn="{StaticResource {x:Type CheckBox}}">
<Setter Property="Margin" Value="8,4"/>
</Style>
<!-- ScrollBar Style -->
<Style TargetType="{x:Type ScrollBar}">
<Setter Property="Background" Value="Transparent"/>

View File

@@ -9,6 +9,9 @@ function RemoveApps {
$appIndex = 0
$appCount = @($appsList).Count
$edgeIds = @('Microsoft.Edge', 'XPFFTQ037JWMHS')
$edgeUninstallSucceeded = $false
$edgeScheduledTaskAdded = $false
Foreach ($app in $appsList) {
if ($script:CancelRequested) {
@@ -25,20 +28,27 @@ function RemoveApps {
Write-Host "Attempting to remove $app..."
# Use WinGet only to remove OneDrive and Edge
if (($app -eq "Microsoft.OneDrive") -or ($app -eq "Microsoft.Edge")) {
if (($app -eq "Microsoft.OneDrive") -or ($edgeIds -contains $app)) {
if ($script:WingetInstalled -eq $false) {
Write-Host "WinGet is either not installed or is outdated, $app could not be removed" -ForegroundColor Red
continue
}
$appName = $app -replace '\.', '_'
$isEdgeId = $edgeIds -contains $app
$appName = if ($isEdgeId) { 'Microsoft_Edge' } else { $app -replace '\.', '_' }
# Uninstall app via WinGet, or create a scheduled task to uninstall it later
if ($script:Params.ContainsKey("User")) {
ImportRegistryFile "Adding scheduled task to uninstall $app for user $(GetUserName)..." "Uninstall_$($appName).reg"
if (-not ($isEdgeId -and $edgeScheduledTaskAdded)) {
ImportRegistryFile "Adding scheduled task to uninstall $app for user $(GetUserName)..." "Uninstall_$($appName).reg"
if ($isEdgeId) { $edgeScheduledTaskAdded = $true }
}
}
elseif ($script:Params.ContainsKey("Sysprep")) {
ImportRegistryFile "Adding scheduled task to uninstall $app after for new users..." "Uninstall_$($appName).reg"
if (-not ($isEdgeId -and $edgeScheduledTaskAdded)) {
ImportRegistryFile "Adding scheduled task to uninstall $app after for new users..." "Uninstall_$($appName).reg"
if ($isEdgeId) { $edgeScheduledTaskAdded = $true }
}
}
else {
# Uninstall app via WinGet
@@ -47,21 +57,35 @@ function RemoveApps {
winget uninstall --accept-source-agreements --disable-interactivity --id $appId
} -ArgumentList $app
If (($app -eq "Microsoft.Edge") -and (Select-String -InputObject $wingetOutput -Pattern "Uninstall failed with exit code")) {
Write-Host "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red
$wingetFailed = Select-String -InputObject $wingetOutput -Pattern "Uninstall failed with exit code|No installed package found matching input criteria|No package found matching input criteria" -SimpleMatch:$false
if ($isEdgeId) {
if (-not $wingetFailed) {
$edgeUninstallSucceeded = $true
}
if ($script:GuiWindow) {
$result = Show-MessageBox -Message 'Unable to uninstall Microsoft Edge via WinGet. Would you like to forcefully uninstall it? NOT RECOMMENDED!' -Title 'Force Uninstall Microsoft Edge?' -Button 'YesNo' -Icon 'Warning'
# Prompt immediately after the final selected Edge ID attempt (if all attempts failed)
$hasRemainingEdgeIds = $false
if ($appIndex -lt $appCount) {
$remainingApps = @($appsList)[($appIndex)..($appCount - 1)]
$hasRemainingEdgeIds = @($remainingApps | Where-Object { $edgeIds -contains $_ }).Count -gt 0
}
if ($result -eq 'Yes') {
if (-not $hasRemainingEdgeIds -and -not $edgeUninstallSucceeded) {
Write-Host "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red
if ($script:GuiWindow) {
$result = Show-MessageBox -Message 'Unable to uninstall Microsoft Edge via WinGet. Would you like to forcefully uninstall it? NOT RECOMMENDED!' -Title 'Force Uninstall Microsoft Edge?' -Button 'YesNo' -Icon 'Warning'
if ($result -eq 'Yes') {
Write-Host ""
ForceRemoveEdge
}
}
elseif ($( Read-Host -Prompt "Would you like to forcefully uninstall Microsoft Edge? NOT RECOMMENDED! (y/n)" ) -eq 'y') {
Write-Host ""
ForceRemoveEdge
}
}
elseif ($( Read-Host -Prompt "Would you like to forcefully uninstall Microsoft Edge? NOT RECOMMENDED! (y/n)" ) -eq 'y') {
Write-Host ""
ForceRemoveEdge
}
}
}

View File

@@ -1,14 +1,33 @@
# Prints all pending changes that will be made by the script
function PrintPendingChanges {
Write-Output "Win11Debloat will make the following changes:"
$skippedParams = @()
$undoChanges = $script:Params.ContainsKey('Undo')
if ($undoChanges) {
Write-Output "Win11Debloat will make the following changes to revert the selected settings to Windows defaults:"
}
else {
Write-Output "Win11Debloat will make the following changes:"
}
if ($script:Params['CreateRestorePoint']) {
Write-Output "- $($script:Features['CreateRestorePoint'].Label)"
Write-Output "- $($script:Features['CreateRestorePoint'].Action)"
}
foreach ($parameterName in $script:Params.Keys) {
if ($script:ControlParams -contains $parameterName) {
continue
}
if ($parameterName -eq 'Apps' -or $parameterName -eq 'CreateRestorePoint') {
continue
}
if ($undoChanges) {
$undoFeature = GetUndoFeatureForParam -paramKey $parameterName
if (-not $undoFeature) {
$skippedParams += $parameterName
continue
}
}
# Print parameter description
switch ($parameterName) {
@@ -46,9 +65,18 @@ function PrintPendingChanges {
}
default {
if ($script:Features -and $script:Features.ContainsKey($parameterName)) {
$action = $script:Features[$parameterName].Action
$message = $script:Features[$parameterName].Label
Write-Output "- $action $message"
$message = if ($undoChanges -and $script:Features[$parameterName].UndoAction) {
$script:Features[$parameterName].UndoAction
}
else {
$script:Features[$parameterName].Action
}
if ($message) {
Write-Output "- $message"
}
else {
Write-Output "- $parameterName"
}
}
else {
# Fallback: show the parameter name if no feature description is available
@@ -59,6 +87,16 @@ function PrintPendingChanges {
}
}
if ($undoChanges -and $skippedParams.Count -gt 0) {
Write-Output ""
Write-Output "The following changes cannot be automatically undone and will be skipped:"
$uniqueSkipped = $skippedParams | Sort-Object -Unique
foreach ($skippedParam in $uniqueSkipped) {
Write-Output "- $($script:Features[$skippedParam].Action)"
}
}
Write-Output ""
Write-Output ""
Write-Output "Press enter to execute the script or press CTRL+C to quit..."

View File

@@ -17,7 +17,6 @@ function ShowCLIDefaultModeOptions {
PrintHeader 'Default Mode'
# Add default settings based on user input
try {
# Select app removal options based on user input
switch ($RemoveAppsInput) {
@@ -35,7 +34,6 @@ function ShowCLIDefaultModeOptions {
}
}
# Load settings from DefaultSettings.json and add to params
LoadSettings -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
}
catch {
@@ -45,8 +43,8 @@ function ShowCLIDefaultModeOptions {
SaveSettings
# Skip change summary if Silent parameter was passed
if ($Silent) {
# Skip change summary and confirmation prompt
return
}

View File

@@ -3,7 +3,6 @@ function ShowCLILastUsedSettings {
PrintHeader 'Custom Mode'
try {
# Load settings from LastUsedSettings.json and add to params
LoadSettings -filePath $script:SavedSettingsFilePath -expectedVersion "1.0"
}
catch {
@@ -11,6 +10,11 @@ function ShowCLILastUsedSettings {
AwaitKeyToExit
}
if ($Silent) {
# Skip change summary and confirmation prompt
return
}
PrintPendingChanges
PrintHeader 'Custom Mode'
}

View File

@@ -6,13 +6,35 @@ function ExecuteParameter {
[string]$paramKey
)
# Check if this feature has metadata in Features.json
# Check if this feature exists in Features.json
$feature = $null
if ($script:Features.ContainsKey($paramKey)) {
$feature = $script:Features[$paramKey]
}
# If feature has RegistryKey and ApplyText, use dynamic ImportRegistryFile
# Check if undo is requested and if this feature supports undo
$undoChanges = $script:Params.ContainsKey('Undo')
if ($undoChanges) {
$undoFeature = GetUndoFeatureForParam -paramKey $paramKey
if ($null -eq $undoFeature) {
# This parameter doesn't support undo, so skip it
return
}
$undoRegFile = $undoFeature.RegistryUndoKey
$undoFolderPath = Join-Path $script:RegfilesPath (Join-Path 'Undo' $undoRegFile)
if (Test-Path $undoFolderPath) {
$undoRegFile = Join-Path 'Undo' $undoRegFile
}
ImportRegistryFile "> $($undoFeature.UndoText)" $undoRegFile
return
}
# If feature has RegistryKey and ApplyText, dynamically import the registry file for this feature
if ($feature -and $feature.RegistryKey -and $feature.ApplyText) {
ImportRegistryFile "> $($feature.ApplyText)" $feature.RegistryKey
@@ -139,14 +161,37 @@ function ExecuteParameter {
# Executes all selected parameters/features
function ExecuteAllChanges {
# Build list of actionable parameters (skip control params and data-only params)
$undoChanges = $script:Params.ContainsKey('Undo')
$actionableKeys = @()
$paramsToRemove = @()
foreach ($paramKey in $script:Params.Keys) {
if ($script:ControlParams -contains $paramKey) { continue }
if ($paramKey -eq 'Apps') { continue }
if ($paramKey -eq 'CreateRestorePoint') { continue }
if ($undoChanges) {
$undoFeature = GetUndoFeatureForParam -paramKey $paramKey
if (-not $undoFeature) {
$paramsToRemove += $paramKey
continue
}
}
$actionableKeys += $paramKey
}
if ($undoChanges -and $paramsToRemove.Count -gt 0) {
foreach ($paramKey in ($paramsToRemove | Sort-Object -Unique)) {
if ($script:Params.ContainsKey($paramKey)) {
$null = $script:Params.Remove($paramKey)
}
}
}
if ($undoChanges -and $actionableKeys.Count -eq 0) {
throw "Undo was requested but none of the selected parameters support undo. No changes were reverted."
}
$totalSteps = $actionableKeys.Count
if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ }
$currentStep = 0
@@ -174,16 +219,13 @@ function ExecuteAllChanges {
$stepName = $paramKey
if ($script:Features.ContainsKey($paramKey)) {
$feature = $script:Features[$paramKey]
if ($feature.ApplyText) {
# Prefer explicit ApplyText when provided
if ($undoChanges -and $feature.UndoText) {
$stepName = $feature.UndoText
}
elseif ($feature.ApplyText) {
$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
}
} elseif ($feature.Action) {
$stepName = $feature.Action
}
}

View File

@@ -7,17 +7,25 @@ function ImportRegistryFile {
Write-Host $message
# Validate that the regfile exists in both locations
if (-not (Test-Path "$script:RegfilesPath\$path") -or -not (Test-Path "$script:RegfilesPath\Sysprep\$path")) {
Write-Host "Error: Unable to find registry file: $path" -ForegroundColor Red
$usesOfflineHive = $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User")
$regFilePath = if ($usesOfflineHive) {
"$script:RegfilesPath\Sysprep\$path"
}
else {
"$script:RegfilesPath\$path"
}
if (-not (Test-Path $regFilePath)) {
$errorMessage = "Unable to find registry file: $path ($regFilePath)"
Write-Host "Error: $errorMessage" -ForegroundColor Red
Write-Host ""
return
throw $errorMessage
}
# Reset exit code before running reg.exe for reliable success detection
$global:LASTEXITCODE = 0
if ($script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User")) {
if ($usesOfflineHive) {
# Sysprep targets Default user, User targets the specified user
$hiveDatPath = if ($script:Params.ContainsKey("Sysprep")) {
GetUserDirectory -userName "Default" -fileName "NTUSER.DAT"
@@ -26,26 +34,62 @@ function ImportRegistryFile {
}
$regResult = Invoke-NonBlocking -ScriptBlock {
param($datPath, $regFilePath)
$global:LASTEXITCODE = 0
reg load "HKU\Default" $datPath | Out-Null
$output = reg import $regFilePath 2>&1
$code = $LASTEXITCODE
reg unload "HKU\Default" | Out-Null
return @{ Output = $output; ExitCode = $code }
} -ArgumentList @($hiveDatPath, "$script:RegfilesPath\Sysprep\$path")
param($hivePath, $targetRegFilePath)
$result = @{
Output = @()
ExitCode = 0
Error = $null
}
try {
$global:LASTEXITCODE = 0
reg load "HKU\Default" $hivePath | Out-Null
$loadExitCode = $LASTEXITCODE
if ($loadExitCode -ne 0) {
throw "Failed to load user hive at '$hivePath' (exit code: $loadExitCode)"
}
$output = reg import $targetRegFilePath 2>&1
$importExitCode = $LASTEXITCODE
if ($output) {
$result.Output = @($output)
}
$result.ExitCode = $importExitCode
if ($importExitCode -ne 0) {
throw "Registry import failed with exit code $importExitCode for '$targetRegFilePath'"
}
}
catch {
$result.Error = $_.Exception.Message
$result.ExitCode = if ($LASTEXITCODE -ne 0) { $LASTEXITCODE } else { 1 }
}
finally {
$global:LASTEXITCODE = 0
reg unload "HKU\Default" | Out-Null
$unloadExitCode = $LASTEXITCODE
if ($unloadExitCode -ne 0 -and -not $result.Error) {
$result.Error = "Failed to unload temporary hive HKU\\Default (exit code: $unloadExitCode)"
$result.ExitCode = $unloadExitCode
}
}
return $result
} -ArgumentList @($hiveDatPath, $regFilePath)
}
else {
$regResult = Invoke-NonBlocking -ScriptBlock {
param($regFilePath)
param($targetRegFilePath)
$global:LASTEXITCODE = 0
$output = reg import $regFilePath 2>&1
return @{ Output = $output; ExitCode = $LASTEXITCODE }
} -ArgumentList "$script:RegfilesPath\$path"
$output = reg import $targetRegFilePath 2>&1
return @{ Output = @($output); ExitCode = $LASTEXITCODE; Error = $null }
} -ArgumentList $regFilePath
}
$regOutput = $regResult.Output
$hasSuccess = $regResult.ExitCode -eq 0
$regOutput = @($regResult.Output)
$hasSuccess = ($regResult.ExitCode -eq 0) -and -not $regResult.Error
if ($regOutput) {
foreach ($line in $regOutput) {
@@ -62,7 +106,11 @@ function ImportRegistryFile {
}
if (-not $hasSuccess) {
Write-Host "Failed importing registry file: $path" -ForegroundColor Red
$details = if ($regResult.Error) { $regResult.Error } else { "Exit code: $($regResult.ExitCode)" }
$errorMessage = "Failed importing registry file '$path'. $details"
Write-Host $errorMessage -ForegroundColor Red
Write-Host ""
throw $errorMessage
}
Write-Host ""

View File

@@ -10,7 +10,7 @@ function RestartExplorer {
foreach ($paramKey in $script:Params.Keys) {
if ($script:Features.ContainsKey($paramKey) -and $script:Features[$paramKey].RequiresReboot -eq $true) {
$feature = $script:Features[$paramKey]
Write-Host "Warning: '$($feature.Action) $($feature.Label)' requires a reboot to take full effect" -ForegroundColor Yellow
Write-Host "Warning: '$($feature.Action)' requires a reboot to take full effect" -ForegroundColor Yellow
}
}

View File

@@ -16,24 +16,36 @@ function LoadAppsDetailsFromJson {
}
foreach ($appData in $jsonContent.Apps) {
$appId = $appData.AppId.Trim()
if ($appId.length -eq 0) { continue }
# Handle AppId as array (could be single or multiple IDs)
$appIdArray = if ($appData.AppId -is [array]) { $appData.AppId } else { @($appData.AppId) }
$appIdArray = $appIdArray | ForEach-Object { $_.Trim() } | Where-Object { $_.length -gt 0 }
if ($appIdArray.Count -eq 0) { continue }
if ($OnlyInstalled) {
if (-not ($InstalledList -like ("*$appId*")) -and -not (Get-AppxPackage -Name $appId)) {
continue
}
if (($appId -eq "Microsoft.Edge") -and -not ($InstalledList -like "* Microsoft.Edge *")) {
continue
$isInstalled = $false
foreach ($appId in $appIdArray) {
if (($InstalledList -like ("*$appId*")) -or (Get-AppxPackage -Name $appId)) {
$isInstalled = $true
break
}
if (($appId -eq "Microsoft.Edge") -and ($InstalledList -like "* Microsoft.Edge *")) {
$isInstalled = $true
break
}
}
if (-not $isInstalled) { continue }
}
$friendlyName = if ($appData.FriendlyName) { $appData.FriendlyName } else { $appId }
$displayName = if ($appData.FriendlyName) { "$($appData.FriendlyName) ($appId)" } else { $appId }
# Use first AppId for fallback names, join all for display
$primaryAppId = $appIdArray[0]
$appIdDisplay = $appIdArray -join ', '
$friendlyName = if ($appData.FriendlyName) { $appData.FriendlyName } else { $primaryAppId }
$displayName = if ($appData.FriendlyName) { "$($appData.FriendlyName) ($appIdDisplay)" } else { $appIdDisplay }
$isChecked = if ($InitialCheckedFromJson) { $appData.SelectedByDefault } else { $false }
$apps += [PSCustomObject]@{
AppId = $appId
AppId = $appIdArray
AppIdDisplay = $appIdDisplay
FriendlyName = $friendlyName
DisplayName = $displayName
IsChecked = $isChecked

View File

@@ -16,10 +16,12 @@ function LoadAppsFromFile {
# JSON file format
$jsonContent = Get-Content -Path $appsFilePath -Raw | ConvertFrom-Json
Foreach ($appData in $jsonContent.Apps) {
$appId = $appData.AppId.Trim()
# Handle AppId as array (could be single or multiple IDs)
$appIdArray = if ($appData.AppId -is [array]) { $appData.AppId } else { @($appData.AppId) }
$appIdArray = $appIdArray | ForEach-Object { $_.Trim() } | Where-Object { $_.length -gt 0 }
$selectedByDefault = $appData.SelectedByDefault
if ($selectedByDefault -and $appId.length -gt 0) {
$appsList += $appId
if ($selectedByDefault -and $appIdArray.Count -gt 0) {
$appsList += $appIdArray
}
}
}

View File

@@ -16,10 +16,7 @@ function SaveSettings {
}
}
try {
$settings | ConvertTo-Json -Depth 10 | Set-Content $script:SavedSettingsFilePath
}
catch {
if (-not (SaveToFile -Config $settings -FilePath $script:SavedSettingsFilePath)) {
Write-Output ""
Write-Host "Error: Failed to save settings to LastUsedSettings.json file" -ForegroundColor Red
}

View File

@@ -0,0 +1,19 @@
# Saves configuration JSON to a file.
# Returns $true on success, $false on failure.
function SaveToFile {
param (
[Parameter(Mandatory=$true)]
[hashtable]$Config,
[Parameter(Mandatory=$true)]
[string]$FilePath
)
try {
$Config | ConvertTo-Json -Depth 10 | Set-Content -Path $FilePath -Encoding UTF8
return $true
}
catch {
return $false
}
}

View File

@@ -4,7 +4,7 @@ function ValidateAppslist {
$appsList
)
$supportedAppsList = (LoadAppsDetailsFromJson | ForEach-Object { $_.AppId })
$supportedAppsList = @(LoadAppsDetailsFromJson | ForEach-Object { @($_.AppId) }) | ForEach-Object { $_.Trim() } | Where-Object { $_.Length -gt 0 }
$validatedAppsList = @()
# Validate provided appsList against supportedAppsList

View File

@@ -83,13 +83,16 @@ function Show-AboutDialog {
})
# Show dialog
$aboutWindow.ShowDialog() | Out-Null
# Hide overlay after dialog closes
if ($overlay) {
try {
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' })
try {
$aboutWindow.ShowDialog() | Out-Null
}
finally {
# Hide overlay after dialog closes
if ($overlay) {
try {
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' })
}
catch { }
}
catch { }
}
}

View File

@@ -75,7 +75,8 @@ function Show-AppSelectionWindow {
$checkbox = New-Object System.Windows.Controls.CheckBox
$checkbox.Content = $_.DisplayName
$checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $_.DisplayName)
$checkbox.Tag = $_.AppId
$checkbox.Tag = $_.AppIdDisplay
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppIds' -Value @($_.AppId)
$checkbox.IsChecked = $_.IsChecked
$checkbox.ToolTip = $_.Description
$checkbox.Style = $window.Resources["AppsPanelCheckBoxStyle"]
@@ -118,9 +119,10 @@ function Show-AppSelectionWindow {
$selectedApps = @()
foreach ($child in $appsPanel.Children) {
if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) {
$selectedApps += $child.Tag
$selectedApps += @($child.AppIds)
}
}
$selectedApps = @($selectedApps | Where-Object { $_ } | Select-Object -Unique)
# Close form without saving if no apps were selected
if ($selectedApps.Count -eq 0) {

View File

@@ -162,7 +162,7 @@ function Show-ApplyModal {
foreach ($paramKey in $script:Params.Keys) {
if ($script:Features.ContainsKey($paramKey) -and $script:Features[$paramKey].RequiresReboot -eq $true) {
$feature = $script:Features[$paramKey]
$rebootFeatures += "$($feature.Action) $($feature.Label)"
$rebootFeatures += $feature.Action
}
}
@@ -179,7 +179,7 @@ function Show-ApplyModal {
$applyRebootPanel.Visibility = 'Visible'
}
else {
$script:ApplyCompletionMessageEl.Text = "Your clean system is ready. Thanks for using Win11Debloat!"
$script:ApplyCompletionMessageEl.Text = "Your system is ready. Thanks for using Win11Debloat!"
}
}
}
@@ -242,13 +242,16 @@ function Show-ApplyModal {
})
# Show dialog
$applyWindow.ShowDialog() | Out-Null
# Hide overlay after dialog closes
if ($overlay) {
try {
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' })
try {
$applyWindow.ShowDialog() | Out-Null
}
finally {
# Hide overlay after dialog closes
if ($overlay) {
try {
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' })
}
catch { }
}
catch { }
}
}

View File

@@ -0,0 +1,382 @@
function Show-ImportExportConfigWindow {
param (
[System.Windows.Window]$Owner,
[bool]$UsesDarkMode,
[string]$Title,
[string]$Prompt,
[string[]]$Categories = @('Applications', 'System Tweaks', 'Deployment Settings'),
[string[]]$DisabledCategories = @()
)
# Show overlay on owner window
$overlay = $null
$overlayWasAlreadyVisible = $false
try {
$overlay = $Owner.FindName('ModalOverlay')
if ($overlay) {
$overlayWasAlreadyVisible = ($overlay.Visibility -eq 'Visible')
if (-not $overlayWasAlreadyVisible) {
$Owner.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Visible' })
}
}
} catch { }
# Load XAML from schema file
$schemaPath = $script:ImportExportConfigSchema
if (-not $schemaPath -or -not (Test-Path $schemaPath)) {
Show-MessageBox -Message 'Import/Export window schema file could not be found.' -Title 'Error' -Button 'OK' -Icon 'Error' -Owner $Owner | Out-Null
if ($overlay -and -not $overlayWasAlreadyVisible) {
try { $Owner.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' }) } catch { }
}
return $null
}
$xaml = Get-Content -Path $schemaPath -Raw
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
try {
$dlg = [System.Windows.Markup.XamlReader]::Load($reader)
}
finally {
$reader.Close()
}
$dlg.Owner = $Owner
SetWindowThemeResources -window $dlg -usesDarkMode $UsesDarkMode
# Populate named elements
$dlg.Title = $Title
$dlg.FindName('TitleText').Text = $Title
$dlg.FindName('PromptText').Text = $Prompt
$titleBar = $dlg.FindName('TitleBar')
$titleBar.Add_MouseLeftButtonDown({ $dlg.DragMove() })
# Add a themed checkbox per category
$checkboxPanel = $dlg.FindName('CheckboxPanel')
$checkboxes = @{}
foreach ($cat in $Categories) {
$cb = New-Object System.Windows.Controls.CheckBox
$cb.Content = $cat
$cb.IsChecked = $true
$cb.Margin = [System.Windows.Thickness]::new(0,0,0,8)
$cb.FontSize = 14
$cb.Foreground = $dlg.FindResource('FgColor')
$cb.Style = $window.Resources["AppsPanelCheckBoxStyle"]
if ($DisabledCategories -contains $cat) {
$cb.IsChecked = $false
$cb.IsEnabled = $false
$cb.Opacity = 0.65
$cb.ToolTip = 'No selected settings available in this category.'
}
$checkboxPanel.Children.Add($cb) | Out-Null
$checkboxes[$cat] = $cb
}
$okBtn = $dlg.FindName('OkButton')
$cancelBtn = $dlg.FindName('CancelButton')
$okBtn.Add_Click({ $dlg.Tag = 'OK'; $dlg.Close() })
$cancelBtn.Add_Click({ $dlg.Tag = 'Cancel'; $dlg.Close() })
# Handle Escape key
$dlg.Add_KeyDown({
param($s, $e)
if ($e.Key -eq 'Escape') { $dlg.Tag = 'Cancel'; $dlg.Close() }
})
try {
$dlg.ShowDialog() | Out-Null
}
finally {
# Hide overlay
if ($overlay -and -not $overlayWasAlreadyVisible) {
try { $Owner.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' }) } catch { }
}
}
if ($dlg.Tag -ne 'OK') { return $null }
$selected = @()
foreach ($cat in $Categories) {
if ($checkboxes[$cat].IsEnabled -and $checkboxes[$cat].IsChecked) { $selected += $cat }
}
if ($selected.Count -eq 0) { return $null }
return $selected
}
function Get-SelectedApplications {
param (
[System.Windows.Controls.Panel]$AppsPanel
)
$selectedApps = @()
foreach ($child in $AppsPanel.Children) {
if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) {
$selectedApps += $child.Tag
}
}
return $selectedApps
}
function Get-SelectedTweakSettings {
param (
[System.Windows.Window]$Owner,
[hashtable]$UiControlMappings
)
$tweakSettings = @()
if (-not $UiControlMappings) {
return $tweakSettings
}
foreach ($mappingKey in $UiControlMappings.Keys) {
$control = $Owner.FindName($mappingKey)
if (-not $control) { continue }
$mapping = $UiControlMappings[$mappingKey]
if ($control -is [System.Windows.Controls.CheckBox] -and $control.IsChecked) {
if ($mapping.Type -eq 'feature') {
$tweakSettings += @{ Name = $mapping.FeatureId; Value = $true }
}
}
elseif ($control -is [System.Windows.Controls.ComboBox] -and $control.SelectedIndex -gt 0) {
if ($mapping.Type -eq 'group') {
$selectedValue = $mapping.Values[$control.SelectedIndex - 1]
foreach ($fid in $selectedValue.FeatureIds) {
$tweakSettings += @{ Name = $fid; Value = $true }
}
}
elseif ($mapping.Type -eq 'feature') {
$tweakSettings += @{ Name = $mapping.FeatureId; Value = $true }
}
}
}
return $tweakSettings
}
function Get-DeploymentSettings {
param (
[System.Windows.Window]$Owner,
[System.Windows.Controls.ComboBox]$UserSelectionCombo,
[System.Windows.Controls.TextBox]$OtherUsernameTextBox
)
$deploySettings = @(
@{ Name = 'UserSelectionIndex'; Value = $UserSelectionCombo.SelectedIndex }
)
if ($UserSelectionCombo.SelectedIndex -eq 1) {
$deploySettings += @{ Name = 'OtherUsername'; Value = $OtherUsernameTextBox.Text.Trim() }
}
$appRemovalScopeCombo = $Owner.FindName('AppRemovalScopeCombo')
if ($appRemovalScopeCombo) {
$deploySettings += @{ Name = 'AppRemovalScopeIndex'; Value = $appRemovalScopeCombo.SelectedIndex }
}
$restorePointCheckBox = $Owner.FindName('RestorePointCheckBox')
if ($restorePointCheckBox) {
$deploySettings += @{ Name = 'CreateRestorePoint'; Value = [bool]$restorePointCheckBox.IsChecked }
}
$restartExplorerCheckBox = $Owner.FindName('RestartExplorerCheckBox')
if ($restartExplorerCheckBox) {
$deploySettings += @{ Name = 'RestartExplorer'; Value = [bool]$restartExplorerCheckBox.IsChecked }
}
return $deploySettings
}
function Get-AvailableImportExportCategories {
param (
$Config
)
$availableCategories = @()
if ($Config.Apps) { $availableCategories += 'Applications' }
if ($Config.Tweaks) { $availableCategories += 'System Tweaks' }
if ($Config.Deployment) { $availableCategories += 'Deployment Settings' }
return $availableCategories
}
function Apply-ImportedApplications {
param (
[System.Windows.Controls.Panel]$AppsPanel,
[string[]]$AppIds
)
foreach ($child in $AppsPanel.Children) {
if ($child -is [System.Windows.Controls.CheckBox]) {
$child.IsChecked = ($AppIds -contains $child.Tag)
}
}
}
function Apply-ImportedTweakSettings {
param (
[System.Windows.Window]$Owner,
[hashtable]$UiControlMappings,
[array]$TweakSettings
)
$settingsJson = [PSCustomObject]@{ Settings = @($TweakSettings) }
ApplySettingsToUiControls -window $Owner -settingsJson $settingsJson -uiControlMappings $UiControlMappings
}
function Apply-ImportedDeploymentSettings {
param (
[System.Windows.Window]$Owner,
[System.Windows.Controls.ComboBox]$UserSelectionCombo,
[System.Windows.Controls.TextBox]$OtherUsernameTextBox,
[array]$DeploymentSettings
)
$lookup = @{}
foreach ($setting in $DeploymentSettings) {
$lookup[$setting.Name] = $setting.Value
}
if ($lookup.ContainsKey('UserSelectionIndex')) {
$UserSelectionCombo.SelectedIndex = [int]$lookup['UserSelectionIndex']
}
if ($lookup.ContainsKey('OtherUsername') -and $UserSelectionCombo.SelectedIndex -eq 1) {
$OtherUsernameTextBox.Text = $lookup['OtherUsername']
}
$appRemovalScopeCombo = $Owner.FindName('AppRemovalScopeCombo')
if ($lookup.ContainsKey('AppRemovalScopeIndex') -and $appRemovalScopeCombo) {
$appRemovalScopeCombo.SelectedIndex = [int]$lookup['AppRemovalScopeIndex']
}
$restorePointCheckBox = $Owner.FindName('RestorePointCheckBox')
if ($lookup.ContainsKey('CreateRestorePoint') -and $restorePointCheckBox) {
$restorePointCheckBox.IsChecked = [bool]$lookup['CreateRestorePoint']
}
$restartExplorerCheckBox = $Owner.FindName('RestartExplorerCheckBox')
if ($lookup.ContainsKey('RestartExplorer') -and $restartExplorerCheckBox) {
$restartExplorerCheckBox.IsChecked = [bool]$lookup['RestartExplorer']
}
}
function Export-Configuration {
param (
[System.Windows.Window]$Owner,
[bool]$UsesDarkMode,
[System.Windows.Controls.Panel]$AppsPanel,
[hashtable]$UiControlMappings,
[System.Windows.Controls.ComboBox]$UserSelectionCombo,
[System.Windows.Controls.TextBox]$OtherUsernameTextBox
)
# Precompute exportable data so empty categories can be disabled in the picker.
$selectedApps = Get-SelectedApplications -AppsPanel $AppsPanel
$tweakSettings = Get-SelectedTweakSettings -Owner $Owner -UiControlMappings $UiControlMappings
$disabledCategories = @()
if ($selectedApps.Count -eq 0) { $disabledCategories += 'Applications' }
if ($tweakSettings.Count -eq 0) { $disabledCategories += 'System Tweaks' }
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Export Configuration' -Prompt 'Select which settings to include in the export:' -DisabledCategories $disabledCategories
if (-not $categories) { return }
$config = @{ Version = '1.0' }
if ($categories -contains 'Applications') {
$config['Apps'] = @($selectedApps)
}
if ($categories -contains 'System Tweaks') {
$config['Tweaks'] = @($tweakSettings)
}
if ($categories -contains 'Deployment Settings') {
$config['Deployment'] = @(Get-DeploymentSettings -Owner $Owner -UserSelectionCombo $UserSelectionCombo -OtherUsernameTextBox $OtherUsernameTextBox)
}
# Show native save-file dialog
$saveDialog = New-Object Microsoft.Win32.SaveFileDialog
$saveDialog.Title = 'Export Configuration'
$saveDialog.Filter = 'JSON files (*.json)|*.json|All files (*.*)|*.*'
$saveDialog.DefaultExt = '.json'
$saveDialog.FileName = "Win11Debloat-Config-$(Get-Date -Format 'yyyyMMdd').json"
if ($saveDialog.ShowDialog($Owner) -ne $true) { return }
if (SaveToFile -Config $config -FilePath $saveDialog.FileName) {
Show-MessageBox -Message "Configuration exported successfully." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null
}
else {
Show-MessageBox -Message "Failed to export configuration" -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null
}
}
function Import-Configuration {
param (
[System.Windows.Window]$Owner,
[bool]$UsesDarkMode,
[System.Windows.Controls.Panel]$AppsPanel,
[hashtable]$UiControlMappings,
[System.Windows.Controls.ComboBox]$UserSelectionCombo,
[System.Windows.Controls.TextBox]$OtherUsernameTextBox,
[scriptblock]$OnAppsImported,
[scriptblock]$OnImportCompleted
)
# Show native open-file dialog
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
$openDialog.Title = 'Import Configuration'
$openDialog.Filter = 'JSON files (*.json)|*.json|All files (*.*)|*.*'
$openDialog.DefaultExt = '.json'
if ($openDialog.ShowDialog($Owner) -ne $true) { return }
$config = LoadJsonFile -filePath $openDialog.FileName -expectedVersion '1.0'
if (-not $config) {
Show-MessageBox -Message "Failed to read configuration file" -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null
return
}
if (-not $config.Version) {
Show-MessageBox -Message "Invalid configuration file format." -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null
return
}
$availableCategories = Get-AvailableImportExportCategories -Config $config
if ($availableCategories.Count -eq 0) {
Show-MessageBox -Message "The configuration file contains no importable data." -Title 'Import Configuration' -Button 'OK' -Icon 'Information' | Out-Null
return
}
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Import Configuration' -Prompt 'Select which settings to import:' -Categories $availableCategories
if (-not $categories) { return }
if ($categories -contains 'Applications' -and $config.Apps) {
$appIds = @(
$config.Apps |
Where-Object { $_ -is [string] } |
ForEach-Object { $_.Trim() } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
)
Apply-ImportedApplications -AppsPanel $AppsPanel -AppIds $appIds
if ($OnAppsImported) {
& $OnAppsImported
}
}
if ($categories -contains 'System Tweaks' -and $config.Tweaks) {
Apply-ImportedTweakSettings -Owner $Owner -UiControlMappings $UiControlMappings -TweakSettings @($config.Tweaks)
}
if ($categories -contains 'Deployment Settings' -and $config.Deployment) {
Apply-ImportedDeploymentSettings -Owner $Owner -UserSelectionCombo $UserSelectionCombo -OtherUsernameTextBox $OtherUsernameTextBox -DeploymentSettings @($config.Deployment)
}
Show-MessageBox -Message "Configuration imported successfully." -Title 'Import Configuration' -Button 'OK' -Icon 'Information' | Out-Null
if ($OnImportCompleted) {
& $OnImportCompleted $categories
}
}

View File

@@ -27,6 +27,8 @@ function Show-MainWindow {
$menuReportBug = $window.FindName('MenuReportBug')
$menuLogs = $window.FindName('MenuLogs')
$menuAbout = $window.FindName('MenuAbout')
$importConfigBtn = $window.FindName('ImportConfigBtn')
$exportConfigBtn = $window.FindName('ExportConfigBtn')
# Title bar event handlers
$titleBar.Add_MouseLeftButtonDown({
@@ -67,6 +69,22 @@ function Show-MainWindow {
Show-AboutDialog -Owner $window
})
# --- Import/Export Configuration ---
$exportConfigBtn.Add_Click({
Export-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox
})
$importConfigBtn.Add_Click({
Import-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -OnAppsImported { UpdateAppSelectionStatus; UpdatePresetStates } -OnImportCompleted {
$tabControl.SelectedIndex = 3
UpdateNavigationButtons
$window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Loaded, [action]{
Show-Bubble -TargetControl $reviewChangesBtn -Message 'View the selected changes here'
}) | Out-Null
}
})
$closeBtn.Add_Click({
$window.Close()
})
@@ -241,7 +259,7 @@ function Show-MainWindow {
$check = ($this.IsChecked -eq $true)
if ($this.IsChecked -eq $null) { $this.IsChecked = $false; $check = $false }
$presetIds = $this.PresetAppIds
ApplyPresetToApps -MatchFilter { param($c) $presetIds -contains $c.Tag }.GetNewClosure() -Check $check
ApplyPresetToApps -MatchFilter { param($c) (@($c.AppIds) | Where-Object { $presetIds -contains $_ }).Count -gt 0 }.GetNewClosure() -Check $check
})
}
@@ -255,6 +273,11 @@ function Show-MainWindow {
# Holds apps data preloaded before ShowDialog() so the first load skips the background job
$script:PreloadedAppData = $null
# Prevent app import until the apps list has finished initial population.
if ($importConfigBtn) {
$importConfigBtn.IsEnabled = $false
}
# Set script-level variable for GUI window reference
$script:GuiWindow = $window
@@ -292,12 +315,32 @@ function Show-MainWindow {
}
}
# Rebuilds $script:AppSearchMatches by scanning appsPanel children in their current order,
# collecting any that are still highlighted. Preserves the active match across reorderings.
function RebuildAppSearchIndex {
param($activeMatch = $null)
$newMatches = @()
$newActiveIndex = -1
$i = 0
foreach ($child in $appsPanel.Children) {
if ($child -is [System.Windows.Controls.CheckBox] -and $child.Background -ne [System.Windows.Media.Brushes]::Transparent) {
$newMatches += $child
if ($null -ne $activeMatch -and [System.Object]::ReferenceEquals($child, $activeMatch)) {
$newActiveIndex = $i
}
$i++
}
}
$script:AppSearchMatches = $newMatches
$script:AppSearchMatchIndex = if ($newActiveIndex -ge 0) { $newActiveIndex } elseif ($newMatches.Count -gt 0) { 0 } else { -1 }
}
function SortApps {
$children = @($appsPanel.Children)
$key = switch ($script:SortColumn) {
'Name' { { $_.AppName } }
'Description' { { $_.AppDescription } }
'AppId' { { $_.Tag } }
'AppId' { { $_.AppIdDisplay } }
}
$sorted = $children | Sort-Object $key -Descending:(-not $script:SortAscending)
$appsPanel.Children.Clear()
@@ -305,6 +348,14 @@ function Show-MainWindow {
$appsPanel.Children.Add($checkbox) | Out-Null
}
UpdateSortArrows
# Rebuild search match list in new sorted order so keyboard navigation stays correct
if ($script:AppSearchMatches.Count -gt 0) {
$activeMatch = if ($script:AppSearchMatchIndex -ge 0 -and $script:AppSearchMatchIndex -lt $script:AppSearchMatches.Count) {
$script:AppSearchMatches[$script:AppSearchMatchIndex]
} else { $null }
RebuildAppSearchIndex -activeMatch $activeMatch
}
}
function SetSortColumn($column) {
@@ -351,14 +402,6 @@ function Show-MainWindow {
function UpdatePresetStates {
$script:UpdatingPresets = $true
try {
# Build a set of currently checked app tags for fast lookup
$checkedTags = @{}
foreach ($child in $appsPanel.Children) {
if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) {
$checkedTags[$child.Tag] = $true
}
}
# Helper: count matching and checked apps, set checkbox state
function SetPresetState($checkbox, [scriptblock]$MatchFilter) {
$total = 0; $checked = 0
@@ -366,7 +409,7 @@ function Show-MainWindow {
if ($child -is [System.Windows.Controls.CheckBox]) {
if (& $MatchFilter $child) {
$total++
if ($checkedTags.ContainsKey($child.Tag)) { $checked++ }
if ($child.IsChecked) { $checked++ }
}
}
}
@@ -388,12 +431,12 @@ function Show-MainWindow {
SetPresetState $presetDefaultApps { param($c) $c.SelectedByDefault -eq $true }
foreach ($jsonCb in $script:JsonPresetCheckboxes) {
$localIds = $jsonCb.PresetAppIds
SetPresetState $jsonCb { param($c) $localIds -contains $c.Tag }.GetNewClosure()
SetPresetState $jsonCb { param($c) (@($c.AppIds) | Where-Object { $localIds -contains $_ }).Count -gt 0 }.GetNewClosure()
}
# Last used preset: only update if it's visible (has saved apps)
if ($presetLastUsed.Visibility -ne 'Collapsed' -and $script:SavedAppIds) {
SetPresetState $presetLastUsed { param($c) $script:SavedAppIds -contains $c.Tag }
SetPresetState $presetLastUsed { param($c) (@($c.AppIds) | Where-Object { $script:SavedAppIds -contains $_ }).Count -gt 0 }
}
}
finally {
@@ -403,7 +446,7 @@ function Show-MainWindow {
# Dynamically builds Tweaks UI from Features.json
function BuildDynamicTweaks {
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0"
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion $script:FeaturesConfigVersion
if (-not $featuresJson) {
Show-MessageBox -Message "Unable to load Features.json file!" -Title "Error" -Button 'OK' -Icon 'Error' | Out-Null
@@ -666,7 +709,7 @@ function Show-MainWindow {
if ($feature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($feature.FeatureId -match '^Enable') { $opt = 'Enable' }
$items = @('No Change', $opt)
$comboName = ("Feature_{0}_Combo" -f $feature.FeatureId) -replace '[^a-zA-Z0-9_]',''
$combo = CreateLabeledCombo -parent $panel -labelText ($feature.Action + ' ' + $feature.Label) -comboName $comboName -items $items
$combo = CreateLabeledCombo -parent $panel -labelText $feature.Action -comboName $comboName -items $items
# attach tooltip from Features.json if present
if ($feature.ToolTip) {
$tipBlock = New-Object System.Windows.Controls.TextBlock
@@ -678,7 +721,7 @@ function Show-MainWindow {
try { $lblBorderObj = $window.FindName("$comboName`_LabelBorder") } catch {}
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
}
$script:UiControlMappings[$comboName] = @{ Type='feature'; FeatureId = $feature.FeatureId; Action = $feature.Action; Label = $feature.Label }
$script:UiControlMappings[$comboName] = @{ Type='feature'; FeatureId = $feature.FeatureId; Action = $feature.Action }
}
}
}
@@ -686,7 +729,7 @@ function Show-MainWindow {
# 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
$script:FeatureLabelLookup[$f.FeatureId] = $f.Action
}
}
@@ -718,6 +761,9 @@ function Show-MainWindow {
if ($appsToAdd.Count -eq 0) {
$window.FindName('DeploymentApplyBtn').IsEnabled = $true
if ($importConfigBtn) {
$importConfigBtn.IsEnabled = $true
}
return
}
@@ -732,9 +778,9 @@ function Show-MainWindow {
$app = $appsToAdd[$i]
$checkbox = New-Object System.Windows.Controls.CheckBox
$automationName = if ($app.FriendlyName) { $app.FriendlyName } elseif ($app.AppId) { $app.AppId } else { $null }
$automationName = if ($app.FriendlyName) { $app.FriendlyName } elseif ($app.AppIdDisplay) { $app.AppIdDisplay } else { $null }
if ($automationName) { $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $automationName) }
$checkbox.Tag = $app.AppId
$checkbox.Tag = $app.AppIdDisplay
$checkbox.IsChecked = $app.IsChecked
$checkbox.Style = $window.Resources['AppsPanelCheckBoxStyle']
@@ -770,9 +816,9 @@ function Show-MainWindow {
[System.Windows.Controls.Grid]::SetColumn($tbDesc, 2)
$tbId = New-Object System.Windows.Controls.TextBlock
$tbId.Text = $app.AppId
$tbId.Style = $window.Resources['AppIdTextStyle']
$tbId.ToolTip = $app.AppId
$tbId.Text = $app.AppIdDisplay
$tbId.Style = $window.Resources["AppIdTextStyle"]
$tbId.ToolTip = $app.AppIdDisplay
[System.Windows.Controls.Grid]::SetColumn($tbId, 3)
$row.Children.Add($dot) | Out-Null
@@ -784,6 +830,8 @@ function Show-MainWindow {
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppName' -Value $app.FriendlyName
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppDescription' -Value $app.Description
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'SelectedByDefault' -Value $app.SelectedByDefault
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppIds' -Value @($app.AppId)
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppIdDisplay' -Value $app.AppIdDisplay
$checkbox.Add_Checked({ UpdateAppSelectionStatus })
$checkbox.Add_Unchecked({ UpdateAppSelectionStatus })
@@ -808,6 +856,9 @@ function Show-MainWindow {
# Re-enable Apply button now that the full, correctly-checked app list is ready
$window.FindName('DeploymentApplyBtn').IsEnabled = $true
if ($importConfigBtn) {
$importConfigBtn.IsEnabled = $true
}
}
# Loads apps into the UI
@@ -816,6 +867,10 @@ function Show-MainWindow {
if ($script:IsLoadingApps) { return }
$script:IsLoadingApps = $true
if ($importConfigBtn) {
$importConfigBtn.IsEnabled = $false
}
# Show loading indicator and clear existing apps
$loadingAppsIndicator.Visibility = 'Visible'
$appsPanel.Children.Clear()
@@ -891,6 +946,11 @@ function Show-MainWindow {
}
})
# Close the preset menu when the main window loses focus (e.g., user switches to another app).
$window.Add_Deactivated({
if ($presetsPopup.IsOpen) { $presetsPopup.IsOpen = $false }
})
# Toggle popup on button click
$presetsBtn.Add_Click({
$presetsPopup.IsOpen = -not $presetsPopup.IsOpen
@@ -1250,7 +1310,7 @@ function Show-MainWindow {
$appRemovalScopeCombo.SelectedIndex = 0
}
1 {
$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."
$userSelectionDescription.Text = "Changes will be applied to a different user profile on this system."
$otherUserPanel.Visibility = 'Visible'
$usernameValidationMessage.Text = ""
# Hide "Current user only" option, show "Target user only" option
@@ -1320,13 +1380,13 @@ function Show-MainWindow {
$successBrush = $window.Resources['ValidationSuccessColor']
if ($username.Length -eq 0) {
$usernameValidationMessage.Text = "[X] Please enter a username"
$usernameValidationMessage.Text = "Please enter a username"
$usernameValidationMessage.Foreground = $errorBrush
return $false
}
if ($username -eq $env:USERNAME) {
$usernameValidationMessage.Text = "[X] Cannot enter your own username, use 'Current User' option instead"
$usernameValidationMessage.Text = "Cannot enter your own username, use 'Current User' option instead"
$usernameValidationMessage.Foreground = $errorBrush
return $false
}
@@ -1334,12 +1394,18 @@ function Show-MainWindow {
$userExists = CheckIfUserExists -Username $username
if ($userExists) {
$usernameValidationMessage.Text = "[OK] User found: $username"
if (TestIfUserIsLoggedIn -Username $username) {
$usernameValidationMessage.Text = "User '$username' is currently logged in. Please sign out that user first."
$usernameValidationMessage.Foreground = $errorBrush
return $false
}
$usernameValidationMessage.Text = "User found: $username"
$usernameValidationMessage.Foreground = $successBrush
return $true
}
$usernameValidationMessage.Text = "[X] User not found, please enter a valid username"
$usernameValidationMessage.Text = "User not found, please enter a valid username"
$usernameValidationMessage.Foreground = $errorBrush
return $false
}
@@ -1364,13 +1430,13 @@ function Show-MainWindow {
if ($userSelectionCombo.SelectedIndex -ne 2) {
$appRemovalScopeCombo.IsEnabled = $true
}
$appRemovalScopeSection.Opacity = 1.0
$appRemovalScopeSection.Visibility = 'Visible'
UpdateAppRemovalScopeDescription
}
else {
# Disable app removal scope selection when no apps selected
$appRemovalScopeCombo.IsEnabled = $false
$appRemovalScopeSection.Opacity = 0.5
$appRemovalScopeSection.Visibility = 'Collapsed'
$appRemovalScopeDescription.Text = "No apps selected for removal."
}
@@ -1400,8 +1466,7 @@ function Show-MainWindow {
}
elseif ($mapping.Type -eq 'feature') {
$label = $script:FeatureLabelLookup[$mapping.FeatureId]
if (-not $label) { $label = $mapping.Action + ' ' + $mapping.Label }
$changesList += $label
if ($label) { $changesList += $label }
}
}
}
@@ -1445,6 +1510,44 @@ function Show-MainWindow {
UpdateNavigationButtons
})
# Handle Home Revert link button
$homeRevertLinkBtn = $window.FindName('HomeRevertLinkBtn')
if ($homeRevertLinkBtn) {
if (-not (Test-Path $script:SavedSettingsFilePath)) {
$homeRevertLinkBtn.Visibility = 'Collapsed'
}
$homeRevertLinkBtn.Add_Click({
$savedSettings = LoadJsonFile -filePath $script:SavedSettingsFilePath -expectedVersion "1.0" -optionalFile
if (-not $savedSettings -or -not $savedSettings.Settings) {
return
}
$revertSelection = Show-RevertSettingsModal -Owner $window -LastUsedSettings $savedSettings
$selectedFeatureIds = @($revertSelection.SelectedFeatureIds)
$shouldRestartExplorer = ($revertSelection.RestartExplorer -eq $true)
if (-not $selectedFeatureIds -or $selectedFeatureIds.Count -eq 0) {
return
}
AddParameter 'Undo'
foreach ($featureId in $selectedFeatureIds) {
if ($script:Features.ContainsKey($featureId)) {
$feature = $script:Features[$featureId]
if ($feature.RegistryUndoKey -and $feature.UndoAction) {
AddParameter $featureId
}
}
}
Show-ApplyModal -Owner $window -RestartExplorer $shouldRestartExplorer
$window.Close()
})
}
# Handle Home Default Mode button - apply defaults and navigate directly to overview
$homeDefaultModeBtn = $window.FindName('HomeDefaultModeBtn')
$homeDefaultModeBtn.Add_Click({
@@ -1482,7 +1585,13 @@ function Show-MainWindow {
$deploymentApplyBtn = $window.FindName('DeploymentApplyBtn')
$deploymentApplyBtn.Add_Click({
if (-not (ValidateOtherUsername)) {
Show-MessageBox -Message "Please enter a valid username." -Title "Invalid Username" -Button 'OK' -Icon 'Warning' | Out-Null
$validationMessage = if (-not [string]::IsNullOrWhiteSpace($usernameValidationMessage.Text)) {
$usernameValidationMessage.Text
}
else {
"Please enter a valid username."
}
Show-MessageBox -Message $validationMessage -Title "Invalid Username" -Button 'OK' -Icon 'Warning' | Out-Null
return
}
@@ -1492,9 +1601,10 @@ function Show-MainWindow {
$selectedApps = @()
foreach ($child in $appsPanel.Children) {
if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) {
$selectedApps += $child.Tag
$selectedApps += @($child.AppIds)
}
}
$selectedApps = @($selectedApps | Where-Object { $_ } | Select-Object -Unique)
if ($selectedApps.Count -gt 0) {
# Check if Microsoft Store is selected
@@ -1715,7 +1825,7 @@ function Show-MainWindow {
if ($script:UpdatingPresets) { return }
$check = ($this.IsChecked -eq $true)
if ($this.IsChecked -eq $null) { $this.IsChecked = $false; $check = $false }
ApplyPresetToApps -MatchFilter { param($c) $script:SavedAppIds -contains $c.Tag } -Check $check
ApplyPresetToApps -MatchFilter { param($c) (@($c.AppIds) | Where-Object { $script:SavedAppIds -contains $_ }).Count -gt 0 } -Check $check
})
}
else {

View File

@@ -152,14 +152,17 @@ function Show-MessageBox {
})
# Show dialog and return result from Tag
$msgWindow.ShowDialog() | Out-Null
# Hide overlay after dialog closes (only if this dialog was the one that showed it)
if ($overlay -and -not $overlayWasAlreadyVisible) {
try {
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' })
try {
$msgWindow.ShowDialog() | Out-Null
}
finally {
# Hide overlay after dialog closes (only if this dialog was the one that showed it)
if ($overlay -and -not $overlayWasAlreadyVisible) {
try {
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' })
}
catch { }
}
catch { }
}
return $msgWindow.Tag

View File

@@ -0,0 +1,198 @@
function Show-RevertSettingsModal {
param (
[Parameter(Mandatory=$false)]
[System.Windows.Window]$Owner = $null,
[Parameter(Mandatory=$true)]
$LastUsedSettings
)
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
$usesDarkMode = GetSystemUsesDarkMode
# Determine owner window
$ownerWindow = if ($Owner) { $Owner } else { $script:GuiWindow }
# Show overlay if owner window exists
$overlay = $null
$overlayWasAlreadyVisible = $false
if ($ownerWindow) {
try {
$overlay = $ownerWindow.FindName('ModalOverlay')
if ($overlay) {
$overlayWasAlreadyVisible = ($overlay.Visibility -eq 'Visible')
if (-not $overlayWasAlreadyVisible) {
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Visible' })
}
}
}
catch { }
}
# Load XAML from file
$xaml = Get-Content -Path $script:RevertSettingsWindowSchema -Raw
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
try {
$revertWindow = [System.Windows.Markup.XamlReader]::Load($reader)
}
finally {
$reader.Close()
}
if ($ownerWindow) {
try {
$revertWindow.Owner = $ownerWindow
}
catch { }
}
SetWindowThemeResources -window $revertWindow -usesDarkMode $usesDarkMode
$itemsPanel = $revertWindow.FindName('RevertItemsPanel')
$countText = $revertWindow.FindName('RevertSelectionCount')
$selectAllBtn = $revertWindow.FindName('RevertSelectAllBtn')
$clearBtn = $revertWindow.FindName('RevertClearBtn')
$applyBtn = $revertWindow.FindName('RevertApplyBtn')
$cancelBtn = $revertWindow.FindName('RevertCancelBtn')
$restartExplorerCheckbox = $revertWindow.FindName('RevertRestartExplorerCheckBox')
$featureCheckboxStyle = $revertWindow.FindResource('FeatureCheckboxStyle')
$restartExplorerCheckbox.Style = $featureCheckboxStyle
$entryCheckboxes = @()
foreach ($setting in $LastUsedSettings.Settings) {
if ($setting.Value -ne $true) { continue }
if ($setting.Name -eq 'Apps' -or $setting.Name -eq 'RemoveApps' -or $setting.Name -eq 'CreateRestorePoint') { continue }
$feature = $null
if ($script:Features.ContainsKey($setting.Name)) {
$feature = $script:Features[$setting.Name]
}
$undoFeature = GetUndoFeatureForParam -paramKey $setting.Name
$label = $setting.Name
if ($feature -and $feature.Action) {
$label = $feature.Action
}
$undoLabel = if ($undoFeature -and $undoFeature.UndoAction) {
$undoFeature.UndoAction
} else {
'No revert action available'
}
$canUndo = ($null -ne $undoFeature)
$itemBorder = New-Object System.Windows.Controls.Border
$itemBorder.Style = $revertWindow.FindResource('RevertItemBorderStyle')
$row = New-Object System.Windows.Controls.StackPanel
$row.Style = $revertWindow.FindResource('RevertItemRowStyle')
$checkbox = New-Object System.Windows.Controls.CheckBox
$checkbox.Content = $label
$checkbox.Tag = $setting.Name
$checkbox.Style = $featureCheckboxStyle
$checkbox.IsEnabled = $canUndo
$undoText = New-Object System.Windows.Controls.TextBlock
$undoText.Text = if ($canUndo) { "Revert to: $undoLabel" } else { 'Revert not supported for this setting' }
$undoText.Style = $revertWindow.FindResource('RevertItemUndoTextStyle')
$undoText.Opacity = if ($canUndo) { 0.75 } else { 0.4 }
$row.Children.Add($checkbox) | Out-Null
$row.Children.Add($undoText) | Out-Null
$itemBorder.Child = $row
$itemsPanel.Children.Add($itemBorder) | Out-Null
$entryCheckboxes += $checkbox
}
# Remove the divider from the last entry for cleaner list termination.
if ($itemsPanel.Children.Count -gt 0) {
$lastItem = $itemsPanel.Children[$itemsPanel.Children.Count - 1]
if ($lastItem -is [System.Windows.Controls.Border]) {
$lastItem.BorderThickness = [System.Windows.Thickness]::new(0)
}
}
if ($entryCheckboxes.Count -eq 0) {
$emptyText = New-Object System.Windows.Controls.TextBlock
$emptyText.Text = 'No previously applied tweaks can be reverted'
$emptyText.Style = $revertWindow.FindResource('RevertEmptyTextStyle')
$itemsPanel.Children.Add($emptyText) | Out-Null
$selectAllBtn.IsEnabled = $false
$clearBtn.IsEnabled = $false
}
$updateState = {
$selectedCount = 0
foreach ($cb in $entryCheckboxes) {
if ($cb.IsEnabled -and $cb.IsChecked -eq $true) {
$selectedCount++
}
}
$countText.Text = "$selectedCount settings selected"
$applyBtn.IsEnabled = ($selectedCount -gt 0)
}
foreach ($cb in $entryCheckboxes) {
$cb.Add_Checked($updateState)
$cb.Add_Unchecked($updateState)
}
$selectAllBtn.Add_Click({
foreach ($cb in $entryCheckboxes) {
if ($cb.IsEnabled) {
$cb.IsChecked = $true
}
}
})
$clearBtn.Add_Click({
foreach ($cb in $entryCheckboxes) {
if ($cb.IsEnabled) {
$cb.IsChecked = $false
}
}
})
$cancelHandler = {
$revertWindow.Close()
}
$cancelBtn.Add_Click($cancelHandler)
$applyBtn.Add_Click({
$selected = @()
foreach ($cb in $entryCheckboxes) {
if ($cb.IsEnabled -and $cb.IsChecked -eq $true -and $cb.Tag) {
$selected += $cb.Tag
}
}
$revertWindow.Tag = [PSCustomObject]@{
SelectedFeatureIds = $selected
RestartExplorer = ($restartExplorerCheckbox -and $restartExplorerCheckbox.IsChecked -eq $true)
}
$revertWindow.Close()
})
$revertWindow.ShowDialog() | Out-Null
if ($overlay -and -not $overlayWasAlreadyVisible) {
try {
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' })
}
catch { }
}
if ($revertWindow.Tag) {
return $revertWindow.Tag
}
return [PSCustomObject]@{
SelectedFeatureIds = @()
RestartExplorer = $false
}
}

View File

@@ -1,6 +1,7 @@
param (
[switch]$CLI,
[switch]$Silent,
[switch]$Undo,
[switch]$Verbose,
[switch]$Sysprep,
[string]$LogPath,
@@ -11,6 +12,7 @@ param (
[switch]$RunDefaults,
[switch]$RunDefaultsLite,
[switch]$RunSavedSettings,
[string]$Config,
[string]$Apps,
[string]$AppRemovalTarget,
[switch]$RemoveApps,

View File

@@ -0,0 +1,17 @@
# Returns the feature metadata for a parameter when it supports undo; otherwise returns $null.
function GetUndoFeatureForParam {
param (
[string]$paramKey
)
if (-not $script:Features -or -not $script:Features.ContainsKey($paramKey)) {
return $null
}
$feature = $script:Features[$paramKey]
if (-not ($feature.RegistryUndoKey -and ($feature.UndoText -or $feature.UndoAction))) {
return $null
}
return $feature
}

View File

@@ -0,0 +1,127 @@
function ImportConfigToParams {
param (
[Parameter(Mandatory = $true)]
[string]$ConfigPath,
[int]$CurrentBuild,
[string]$ExpectedVersion = '1.0'
)
$resolvedConfigPath = $null
try {
$resolvedConfigPath = (Resolve-Path -LiteralPath $ConfigPath -ErrorAction Stop).Path
}
catch {
throw "Unable to find config file at path: $ConfigPath"
}
if (-not (Test-Path -LiteralPath $resolvedConfigPath -PathType Leaf)) {
throw "Provided config path is not a file: $resolvedConfigPath"
}
if ([System.IO.Path]::GetExtension($resolvedConfigPath) -ne '.json') {
throw "Provided config file must be a .json file: $resolvedConfigPath"
}
$configJson = LoadJsonFile -filePath $resolvedConfigPath -expectedVersion $ExpectedVersion
if ($null -eq $configJson) {
throw "Failed to read config file: $resolvedConfigPath"
}
$importedItems = 0
if ($configJson.Apps) {
$appIds = @(
$configJson.Apps |
Where-Object { $_ -is [string] } |
ForEach-Object { $_.Trim() } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
)
if ($appIds.Count -gt 0) {
AddParameter 'RemoveApps'
AddParameter 'Apps' ($appIds -join ',')
$importedItems++
}
}
if ($configJson.Tweaks) {
foreach ($setting in @($configJson.Tweaks)) {
if (-not $setting -or -not $setting.Name -or $setting.Value -ne $true) {
continue
}
$feature = $script:Features[$setting.Name]
if (-not $feature) {
continue
}
if (($feature.MinVersion -and $CurrentBuild -lt $feature.MinVersion) -or ($feature.MaxVersion -and $CurrentBuild -gt $feature.MaxVersion) -or ($feature.FeatureId -eq 'DisableModernStandbyNetworking' -and (-not $script:ModernStandbySupported))) {
continue
}
AddParameter $setting.Name $true
$importedItems++
}
}
if ($configJson.Deployment) {
$deploymentLookup = @{}
foreach ($setting in @($configJson.Deployment)) {
if ($setting -and $setting.Name) {
$deploymentLookup[$setting.Name] = $setting.Value
}
}
if ($deploymentLookup.ContainsKey('CreateRestorePoint') -and [bool]$deploymentLookup['CreateRestorePoint']) {
AddParameter 'CreateRestorePoint'
$importedItems++
}
if ($deploymentLookup.ContainsKey('RestartExplorer') -and -not [bool]$deploymentLookup['RestartExplorer']) {
AddParameter 'NoRestartExplorer'
$importedItems++
}
if ($deploymentLookup.ContainsKey('UserSelectionIndex')) {
switch ([int]$deploymentLookup['UserSelectionIndex']) {
1 {
$otherUserName = if ($deploymentLookup.ContainsKey('OtherUsername')) { "$($deploymentLookup['OtherUsername'])".Trim() } else { '' }
if (-not [string]::IsNullOrWhiteSpace($otherUserName)) {
AddParameter 'User' $otherUserName
$importedItems++
}
}
2 {
AddParameter 'Sysprep'
$importedItems++
}
}
}
if ($deploymentLookup.ContainsKey('AppRemovalScopeIndex') -and $script:Params.ContainsKey('RemoveApps')) {
switch ([int]$deploymentLookup['AppRemovalScopeIndex']) {
0 {
AddParameter 'AppRemovalTarget' 'AllUsers'
$importedItems++
}
1 {
AddParameter 'AppRemovalTarget' 'CurrentUser'
$importedItems++
}
2 {
$targetUser = if ($deploymentLookup.ContainsKey('OtherUsername')) { "$($deploymentLookup['OtherUsername'])".Trim() } else { '' }
if (-not [string]::IsNullOrWhiteSpace($targetUser)) {
AddParameter 'AppRemovalTarget' $targetUser
$importedItems++
}
}
}
}
}
if ($importedItems -eq 0) {
throw "The config file contains no importable data: $resolvedConfigPath"
}
return $resolvedConfigPath
}

View File

@@ -0,0 +1,42 @@
function TestIfUserIsLoggedIn {
param(
[Parameter(Mandatory)]
[string]$Username
)
try {
$quserOutput = @(& quser 2>$null)
if ($LASTEXITCODE -ne 0 -or -not $quserOutput) {
return $false
}
foreach ($line in ($quserOutput | Select-Object -Skip 1)) {
if ([string]::IsNullOrWhiteSpace($line)) { continue }
# Remove current-session marker and split columns.
$normalizedLine = $line.TrimStart('>', ' ')
$parts = $normalizedLine -split '\s+'
if ($parts.Count -eq 0) { continue }
$sessionUser = $parts[0]
if ([string]::IsNullOrWhiteSpace($sessionUser)) { continue }
# Normalize possible DOMAIN\user or user@domain formats.
if ($sessionUser.Contains('\')) {
$sessionUser = ($sessionUser -split '\\')[-1]
}
if ($sessionUser.Contains('@')) {
$sessionUser = ($sessionUser -split '@')[0]
}
if ($sessionUser.Equals($Username, [System.StringComparison]::OrdinalIgnoreCase)) {
return $true
}
}
}
catch {
return $false
}
return $false
}

View File

@@ -4,6 +4,7 @@
param (
[switch]$CLI,
[switch]$Silent,
[switch]$Undo,
[switch]$Sysprep,
[string]$LogPath,
[string]$User,
@@ -13,6 +14,7 @@ param (
[switch]$RunDefaults,
[switch]$RunDefaultsLite,
[switch]$RunSavedSettings,
[string]$Config,
[string]$Apps,
[string]$AppRemovalTarget,
[switch]$RemoveApps,
@@ -104,6 +106,7 @@ param (
# Define script-level variables & paths
$script:Version = "2026.03.15"
$script:FeaturesConfigVersion = "2.0"
$script:AppsListFilePath = "$PSScriptRoot/Config/Apps.json"
$script:DefaultSettingsFilePath = "$PSScriptRoot/Config/DefaultSettings.json"
$script:FeaturesFilePath = "$PSScriptRoot/Config/Features.json"
@@ -117,11 +120,13 @@ $script:MainWindowSchema = "$PSScriptRoot/Schemas/MainWindow.xaml"
$script:MessageBoxSchema = "$PSScriptRoot/Schemas/MessageBoxWindow.xaml"
$script:AboutWindowSchema = "$PSScriptRoot/Schemas/AboutWindow.xaml"
$script:ApplyChangesWindowSchema = "$PSScriptRoot/Schemas/ApplyChangesWindow.xaml"
$script:RevertSettingsWindowSchema = "$PSScriptRoot/Schemas/RevertSettingsWindow.xaml"
$script:SharedStylesSchema = "$PSScriptRoot/Schemas/SharedStyles.xaml"
$script:BubbleHintSchema = "$PSScriptRoot/Schemas/BubbleHint.xaml"
$script:ImportExportConfigSchema = "$PSScriptRoot/Schemas/ImportExportConfigWindow.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', 'Undo', 'Sysprep', 'User', 'NoRestartExplorer', 'RunDefaults', 'RunDefaultsLite', 'RunSavedSettings', 'Config', 'RunAppsListGenerator', 'CLI', 'AppRemovalTarget'
# Script-level variables for GUI elements
$script:GuiWindow = $null
@@ -168,9 +173,23 @@ else {
Start-Transcript -Path $script:DefaultLogPath -Append -IncludeInvocationHeader -Force | Out-Null
}
# Check if script has all required files
if (-not ((Test-Path $script:DefaultSettingsFilePath) -and (Test-Path $script:AppsListFilePath) -and (Test-Path $script:RegfilesPath) -and (Test-Path $script:AssetsPath) -and (Test-Path $script:AppSelectionSchema) -and (Test-Path $script:ApplyChangesWindowSchema) -and (Test-Path $script:SharedStylesSchema) -and (Test-Path $script:BubbleHintSchema) -and (Test-Path $script:FeaturesFilePath))) {
Write-Error "Win11Debloat is unable to find required files, please ensure all script files are present"
# Check if script has all required files/directories.
$optionalPathVariables = @('SavedSettingsFilePath', 'CustomAppsListFilePath', 'DefaultLogPath')
$requiredPathVariables = @(Get-Variable -Scope Script | Where-Object {
$_.Name -match '(FilePath|Schema|ScriptPath|RegfilesPath|AssetsPath)$' -and ($optionalPathVariables -notcontains $_.Name)
} | Select-Object -ExpandProperty Name)
$missingRequiredPaths = @()
foreach ($variableName in $requiredPathVariables) {
$pathValue = Get-Variable -Name $variableName -Scope Script -ValueOnly
if ([String]::IsNullOrWhiteSpace($pathValue) -or -not (Test-Path $pathValue)) {
$missingRequiredPaths += "$variableName => $pathValue"
}
}
if ($missingRequiredPaths.Count -gt 0) {
Write-Error "Win11Debloat is unable to find required files, please ensure all script files are present. Missing: $($missingRequiredPaths -join '; ')"
Write-Output ""
Write-Output "Press any key to exit..."
$null = [System.Console]::ReadKey()
@@ -181,6 +200,15 @@ if (-not ((Test-Path $script:DefaultSettingsFilePath) -and (Test-Path $script:Ap
$script:Features = @{}
try {
$featuresData = Get-Content -Path $script:FeaturesFilePath -Raw | ConvertFrom-Json
if (-not $featuresData.Version -or $featuresData.Version -ne $script:FeaturesConfigVersion) {
Write-Error "Features.json version mismatch (expected $($script:FeaturesConfigVersion), found $($featuresData.Version))"
Write-Output ""
Write-Output "Press any key to exit..."
$null = [System.Console]::ReadKey()
Exit
}
foreach ($feature in $featuresData.Features) {
$script:Features[$feature.FeatureId] = $feature
}
@@ -249,6 +277,7 @@ if (-not $script:WingetInstalled -and -not $Silent) {
# File I/O functions
. "$PSScriptRoot/Scripts/FileIO/LoadJsonFile.ps1"
. "$PSScriptRoot/Scripts/FileIO/SaveToFile.ps1"
. "$PSScriptRoot/Scripts/FileIO/SaveSettings.ps1"
. "$PSScriptRoot/Scripts/FileIO/LoadSettings.ps1"
. "$PSScriptRoot/Scripts/FileIO/SaveCustomAppsListToFile.ps1"
@@ -263,7 +292,9 @@ if (-not $script:WingetInstalled -and -not $Silent) {
. "$PSScriptRoot/Scripts/GUI/AttachShiftClickBehavior.ps1"
. "$PSScriptRoot/Scripts/GUI/ApplySettingsToUiControls.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-MessageBox.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-ConfigWindow.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-ApplyModal.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-RevertSettingsModal.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-AppSelectionWindow.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-MainWindow.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-AboutDialog.ps1"
@@ -275,9 +306,12 @@ if (-not $script:WingetInstalled -and -not $Silent) {
. "$PSScriptRoot/Scripts/Helpers/CheckModernStandbySupport.ps1"
. "$PSScriptRoot/Scripts/Helpers/GenerateAppsList.ps1"
. "$PSScriptRoot/Scripts/Helpers/GetFriendlyTargetUserName.ps1"
. "$PSScriptRoot/Scripts/Helpers/ImportConfigToParams.ps1"
. "$PSScriptRoot/Scripts/Helpers/GetTargetUserForAppRemoval.ps1"
. "$PSScriptRoot/Scripts/Helpers/GetUndoFeatureForParam.ps1"
. "$PSScriptRoot/Scripts/Helpers/GetUserDirectory.ps1"
. "$PSScriptRoot/Scripts/Helpers/GetUserName.ps1"
. "$PSScriptRoot/Scripts/Helpers/TestIfUserIsLoggedIn.ps1"
# Threading functions
. "$PSScriptRoot/Scripts/Threading/DoEvents.ps1"
@@ -315,6 +349,27 @@ foreach ($Param in $script:ControlParams) {
}
}
# Guard: Undo mode requires at least one actionable and cannot be combined with deployment-targeted parameters
if ($script:Params.ContainsKey('Undo')) {
$deploymentTargetParams = @('Sysprep', 'User', 'AppRemovalTarget')
$selectedDeploymentParams = @($deploymentTargetParams | Where-Object { $script:Params.ContainsKey($_) })
if ($selectedDeploymentParams.Count -gt 0) {
Write-Error "The -Undo parameter cannot be combined with deployment target parameters: -$($selectedDeploymentParams -join ', -')."
AwaitKeyToExit
}
$loadsSettingsFromPreset = $RunDefaults -or $RunDefaultsLite -or $RunSavedSettings
$undoTargets = @($script:Params.Keys | Where-Object {
($script:ControlParams -notcontains $_) -and $_ -ne 'Apps' -and $_ -ne 'CreateRestorePoint'
})
if ($undoTargets.Count -eq 0 -and -not $loadsSettingsFromPreset) {
Write-Error "The -Undo parameter requires at least one setting/feature parameter to revert."
AwaitKeyToExit
}
}
# Hide progress bars for app removal, as they block Win11Debloat's output
if (-not ($script:Params.ContainsKey("Verbose"))) {
$ProgressPreference = 'SilentlyContinue'
@@ -351,6 +406,9 @@ if ((Test-Path $script:SavedSettingsFilePath) -and ([String]::IsNullOrWhiteSpace
Remove-Item -Path $script:SavedSettingsFilePath -recurse
}
# Default to CLI mode for deployment-targeted parameters.
$launchInCLI = $CLI -or $script:Params.ContainsKey("User") -or $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("AppRemovalTarget")
# Only run the app selection form if the 'RunAppsListGenerator' parameter was passed to the script
if ($RunAppsListGenerator) {
PrintHeader "Custom Apps List Generator"
@@ -370,7 +428,7 @@ if ($RunAppsListGenerator) {
}
# Change script execution based on provided parameters or user input
if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSavedSettings -or ($controlParamsCount -eq $script:Params.Count)) {
if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSavedSettings -or $Config -or ($controlParamsCount -eq $script:Params.Count)) {
if ($RunDefaults -or $RunDefaultsLite) {
ShowCLIDefaultModeOptions
}
@@ -383,8 +441,23 @@ if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSa
ShowCLILastUsedSettings
}
elseif ($Config) {
try {
ImportConfigToParams -ConfigPath $Config -CurrentBuild $WinVersion -ExpectedVersion '1.0'
}
catch {
Write-Error "$_"
AwaitKeyToExit
}
if (-not $Silent) {
PrintHeader 'Custom Mode'
PrintPendingChanges
PrintHeader 'Custom Mode'
}
}
else {
if ($CLI) {
if ($launchInCLI) {
$Mode = ShowCLIMenuOptions
}
else {
@@ -395,7 +468,7 @@ if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSa
Exit
}
catch {
Write-Warning "Unable to load WPF GUI (not supported in this environment), falling back to CLI mode"
Write-Warning "Something went wrong while loading the graphical interface, falling back to CLI mode: $_"
if (-not $Silent) {
Write-Host ""
Write-Host "Press any key to continue..."
@@ -436,9 +509,17 @@ if (($controlParamsCount -eq $script:Params.Keys.Count) -or ($script:Params.Keys
AwaitKeyToExit
}
# Execute all selected/provided parameters using the consolidated function
# (This also handles restore point creation if requested)
ExecuteAllChanges
try {
# Execute all selected/provided parameters using the consolidated function
# (This also handles restore point creation if requested)
ExecuteAllChanges
}
catch {
Write-Error "An error occurred while applying changes: $_"
AwaitKeyToExit
}
RestartExplorer