mirror of
https://github.com/Raphire/Win11Debloat.git
synced 2026-06-10 18:46:28 +00:00
Compare commits
35 Commits
2026.04.26
...
icon-test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12f3ce401b | ||
|
|
924772c3a0 | ||
|
|
68248b4a04 | ||
|
|
1ed967b9d3 | ||
|
|
4332eaa833 | ||
|
|
9deeb295e7 | ||
|
|
f6ed6ac487 | ||
|
|
b920536be2 | ||
|
|
7273f29fea | ||
|
|
7381c29da2 | ||
|
|
3bed9cafbc | ||
|
|
6dbaac0513 | ||
|
|
6e63b34dbb | ||
|
|
3f763b01ab | ||
|
|
4109588e0f | ||
|
|
37872b2030 | ||
|
|
abfc5db2c3 | ||
|
|
1d828d6a78 | ||
|
|
4d9da4749b | ||
|
|
5cf9ac4082 | ||
|
|
924c192ca5 | ||
|
|
2a5cb986c9 | ||
|
|
66982ada28 | ||
|
|
489af33a8b | ||
|
|
51aa288dfd | ||
|
|
24a6f1bcf8 | ||
|
|
8ac664e45f | ||
|
|
85aa67b5d2 | ||
|
|
c8b4563954 | ||
|
|
22f3144c0f | ||
|
|
2c360961e3 | ||
|
|
11a324365d | ||
|
|
5daa922148 | ||
|
|
1826d6d8be | ||
|
|
c15309bcf6 |
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -109,7 +109,7 @@ Avoid these common mistakes when contributing:
|
|||||||
|
|
||||||
Placing files in the wrong directory will cause the script to fail when trying to apply or undo changes.
|
Placing files in the wrong directory will cause the script to fail when trying to apply or undo changes.
|
||||||
|
|
||||||
6. **Not Testing Undo Functionality**: Always test that your undo registry file properly reverts all changes. A feature that can't be undone will frustrate users.
|
6. **Not Testing Undo Functionality**: Always test that your undo registry file properly reverts all changes.
|
||||||
|
|
||||||
7. **Not Testing User/Sysprep Functionality**: Always test that your feature works when applied to another user or to the Windows default user with Sysprep. Sysprep changes can be tested by creating new users after running the script.
|
7. **Not Testing User/Sysprep Functionality**: Always test that your feature works when applied to another user or to the Windows default user with Sysprep. Sysprep changes can be tested by creating new users after running the script.
|
||||||
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ LastUsedSettings.json
|
|||||||
CustomAppsList
|
CustomAppsList
|
||||||
Logs/*
|
Logs/*
|
||||||
Win11Debloat.log
|
Win11Debloat.log
|
||||||
|
Backups/*
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -11,18 +11,7 @@
|
|||||||
Topmost="False"
|
Topmost="False"
|
||||||
ShowInTaskbar="False">
|
ShowInTaskbar="False">
|
||||||
|
|
||||||
<Border BorderBrush="{DynamicResource BorderColor}"
|
<Border Style="{DynamicResource ModalCardBorderStyle}">
|
||||||
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 Margin="0">
|
<Grid Margin="0">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
@@ -32,13 +21,9 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- Title Bar -->
|
<!-- Title Bar -->
|
||||||
<Grid Grid.Row="0" x:Name="TitleBar" Height="48" Background="Transparent">
|
<Grid Grid.Row="0" x:Name="TitleBar" Style="{DynamicResource ModalTitleBarStyle}">
|
||||||
<TextBlock Text="About Win11Debloat"
|
<TextBlock Text="About Win11Debloat"
|
||||||
Foreground="{DynamicResource FgColor}"
|
Style="{DynamicResource ModalTitleTextStyle}"/>
|
||||||
FontSize="18"
|
|
||||||
FontWeight="SemiBold"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Margin="20,0,0,0"/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Message Content -->
|
<!-- Message Content -->
|
||||||
@@ -50,7 +35,7 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- Message Text -->
|
<!-- Message Text -->
|
||||||
<Grid Grid.Row="0" Margin="24,12,24,20">
|
<Grid Grid.Row="0" Margin="20,4,20,20">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
@@ -64,41 +49,29 @@
|
|||||||
<!-- Version -->
|
<!-- Version -->
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0"
|
<TextBlock Grid.Row="0" Grid.Column="0"
|
||||||
Text="Version:"
|
Text="Version:"
|
||||||
FontSize="14"
|
Style="{DynamicResource ModalInfoLabelTextStyle}"/>
|
||||||
Foreground="{DynamicResource FgColor}"
|
|
||||||
FontWeight="SemiBold"
|
|
||||||
Margin="0,0,16,8"/>
|
|
||||||
<TextBlock x:Name="VersionText"
|
<TextBlock x:Name="VersionText"
|
||||||
Grid.Row="0" Grid.Column="1"
|
Grid.Row="0" Grid.Column="1"
|
||||||
Text="0.0.0"
|
Text="0.0.0"
|
||||||
FontSize="14"
|
Style="{DynamicResource ModalInfoValueTextStyle}"/>
|
||||||
Foreground="{DynamicResource FgColor}"
|
|
||||||
Margin="0,0,0,8"/>
|
|
||||||
|
|
||||||
<!-- Author -->
|
<!-- Author -->
|
||||||
<TextBlock Grid.Row="1" Grid.Column="0"
|
<TextBlock Grid.Row="1" Grid.Column="0"
|
||||||
Text="Author:"
|
Text="Author:"
|
||||||
FontSize="14"
|
Style="{DynamicResource ModalInfoLabelTextStyle}"/>
|
||||||
Foreground="{DynamicResource FgColor}"
|
|
||||||
FontWeight="SemiBold"
|
|
||||||
Margin="0,0,16,8"/>
|
|
||||||
<TextBlock Grid.Row="1" Grid.Column="1"
|
<TextBlock Grid.Row="1" Grid.Column="1"
|
||||||
Text="Raphire"
|
Text="Raphire"
|
||||||
FontSize="14"
|
Style="{DynamicResource ModalInfoValueTextStyle}"/>
|
||||||
Foreground="{DynamicResource FgColor}"
|
|
||||||
Margin="0,0,0,8"/>
|
|
||||||
|
|
||||||
<!-- Project Link -->
|
<!-- Project Link -->
|
||||||
<TextBlock Grid.Row="2" Grid.Column="0"
|
<TextBlock Grid.Row="2" Grid.Column="0"
|
||||||
Text="Project:"
|
Text="Project:"
|
||||||
FontSize="14"
|
Style="{DynamicResource ModalInfoLabelTextStyle}"
|
||||||
Foreground="{DynamicResource FgColor}"
|
|
||||||
FontWeight="SemiBold"
|
|
||||||
Margin="0,0,16,0"/>
|
Margin="0,0,16,0"/>
|
||||||
<TextBlock x:Name="ProjectLink"
|
<TextBlock x:Name="ProjectLink"
|
||||||
Grid.Row="2" Grid.Column="1"
|
Grid.Row="2" Grid.Column="1"
|
||||||
Text="https://github.com/Raphire/Win11Debloat"
|
Text="https://github.com/Raphire/Win11Debloat"
|
||||||
FontSize="14"
|
FontSize="13"
|
||||||
Style="{DynamicResource HyperlinkStyle}"
|
Style="{DynamicResource HyperlinkStyle}"
|
||||||
Margin="0,0,0,0"/>
|
Margin="0,0,0,0"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -107,10 +80,10 @@
|
|||||||
<Border Grid.Row="1"
|
<Border Grid.Row="1"
|
||||||
Height="1"
|
Height="1"
|
||||||
Background="{DynamicResource BorderColor}"
|
Background="{DynamicResource BorderColor}"
|
||||||
Margin="10,0"/>
|
Margin="20,0"/>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<StackPanel Grid.Row="2" Margin="24,20">
|
<StackPanel Grid.Row="2" Margin="20,18,20,20">
|
||||||
<!-- Donation Message -->
|
<!-- Donation Message -->
|
||||||
<TextBlock Text="Win11Debloat is a passion project that I maintain in my free time. If you've found this tool useful, please consider making a small donation to support its development. I really appreciate it!"
|
<TextBlock Text="Win11Debloat is a passion project that I maintain in my free time. If you've found this tool useful, please consider making a small donation to support its development. I really appreciate it!"
|
||||||
FontSize="14"
|
FontSize="14"
|
||||||
@@ -150,19 +123,11 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Button Panel -->
|
<!-- Button Panel -->
|
||||||
<Border Grid.Row="2"
|
<Border Grid.Row="2" Style="{DynamicResource ModalFooterBorderStyle}">
|
||||||
Background="{DynamicResource BgColor}"
|
<StackPanel x:Name="ButtonPanel" Style="{DynamicResource ModalFooterButtonsRightStyle}">
|
||||||
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="CloseButton"
|
<Button x:Name="CloseButton"
|
||||||
Content="Close"
|
Content="Close"
|
||||||
Height="32" MinWidth="80" Margin="4,0"
|
Style="{DynamicResource ModalSecondaryActionButtonStyle}"/>
|
||||||
Style="{DynamicResource SecondaryButtonStyle}"/>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -11,18 +11,7 @@
|
|||||||
Topmost="False"
|
Topmost="False"
|
||||||
ShowInTaskbar="False">
|
ShowInTaskbar="False">
|
||||||
|
|
||||||
<Border BorderBrush="{DynamicResource BorderColor}"
|
<Border Style="{DynamicResource ModalCardBorderStyle}">
|
||||||
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>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
@@ -100,11 +89,7 @@
|
|||||||
MaxWidth="430"/>
|
MaxWidth="430"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Border Background="{DynamicResource BgColor}"
|
<Border Style="{DynamicResource ModalFooterBorderStyle}">
|
||||||
BorderBrush="{DynamicResource BorderColor}"
|
|
||||||
BorderThickness="0,1,0,0"
|
|
||||||
Padding="16,12"
|
|
||||||
CornerRadius="0,0,8,8">
|
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
|
|
||||||
<!-- Progress bar -->
|
<!-- Progress bar -->
|
||||||
@@ -172,29 +157,22 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Button Panel -->
|
<!-- Button Panel -->
|
||||||
<Border Background="{DynamicResource BgColor}"
|
<Border Style="{DynamicResource ModalFooterBorderStyle}">
|
||||||
BorderBrush="{DynamicResource BorderColor}"
|
<UniformGrid x:Name="ButtonPanel" Rows="1">
|
||||||
BorderThickness="0,1,0,0"
|
<Button x:Name="ApplyKofiBtn"
|
||||||
Padding="16,12"
|
Style="{DynamicResource ModalSecondaryStretchedButtonStyle}"
|
||||||
CornerRadius="0,0,8,8">
|
|
||||||
<StackPanel x:Name="ButtonPanel"
|
|
||||||
Orientation="Horizontal"
|
|
||||||
HorizontalAlignment="Center">
|
|
||||||
<Button x:Name="ApplyKofiBtn" Width="210" Height="32"
|
|
||||||
Style="{DynamicResource SecondaryButtonStyle}"
|
|
||||||
Margin="0,0,12,0"
|
|
||||||
AutomationProperties.Name="Support the creator">
|
AutomationProperties.Name="Support the creator">
|
||||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,0,8,-1"/>
|
<TextBlock Text="" 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"/>
|
<TextBlock Text="Support the creator" VerticalAlignment="Center" FontSize="14" Margin="0,0,0,1"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
<Button x:Name="ApplyCloseBtn" Width="100" Height="32"
|
<Button x:Name="ApplyCloseBtn"
|
||||||
Style="{DynamicResource PrimaryButtonStyle}"
|
Style="{DynamicResource ModalPrimaryStretchedButtonStyle}"
|
||||||
AutomationProperties.Name="Close">
|
AutomationProperties.Name="Close">
|
||||||
<TextBlock Text="Close" VerticalAlignment="Center" FontSize="14" Margin="0,0,0,1"/>
|
<TextBlock Text="Close" VerticalAlignment="Center" FontSize="14" Margin="0,0,0,1"/>
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</UniformGrid>
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -12,18 +12,7 @@
|
|||||||
Topmost="False"
|
Topmost="False"
|
||||||
ShowInTaskbar="False">
|
ShowInTaskbar="False">
|
||||||
|
|
||||||
<Border BorderBrush="{DynamicResource BorderColor}"
|
<Border Style="{DynamicResource ModalCardBorderStyle}">
|
||||||
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>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
@@ -33,18 +22,14 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- Title Bar -->
|
<!-- Title Bar -->
|
||||||
<Grid Grid.Row="0" x:Name="TitleBar" Height="40" Background="Transparent">
|
<Grid Grid.Row="0" x:Name="TitleBar" Style="{DynamicResource ModalTitleBarStyle}">
|
||||||
<TextBlock x:Name="TitleText"
|
<TextBlock x:Name="TitleText"
|
||||||
Foreground="{DynamicResource FgColor}"
|
Style="{DynamicResource ModalTitleTextStyle}"/>
|
||||||
FontSize="16"
|
|
||||||
FontWeight="SemiBold"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Margin="16,0,0,0"/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Padding="0,0,8,0">
|
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Padding="0,0,8,0">
|
||||||
<StackPanel x:Name="ContentPanel" Margin="20,12,20,9">
|
<StackPanel x:Name="ContentPanel" Margin="20,4,20,8">
|
||||||
<TextBlock x:Name="PromptText"
|
<TextBlock x:Name="PromptText"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
FontSize="14"
|
FontSize="14"
|
||||||
@@ -57,22 +42,15 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
<!-- Button Footer -->
|
<!-- Button Footer -->
|
||||||
<Border Grid.Row="2"
|
<Border Grid.Row="2" Style="{DynamicResource ModalFooterBorderStyle}">
|
||||||
Background="{DynamicResource BgColor}"
|
<UniformGrid Rows="1">
|
||||||
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"
|
<Button x:Name="OkButton"
|
||||||
Content="OK"
|
Content="OK"
|
||||||
Height="32" MinWidth="80" Margin="4,0"
|
Style="{DynamicResource ModalPrimaryStretchedButtonStyle}"/>
|
||||||
Style="{DynamicResource PrimaryButtonStyle}"/>
|
|
||||||
<Button x:Name="CancelButton"
|
<Button x:Name="CancelButton"
|
||||||
Content="Cancel"
|
Content="Cancel"
|
||||||
Height="32" MinWidth="80" Margin="4,0"
|
Style="{DynamicResource ModalSecondaryStretchedButtonStyle}"/>
|
||||||
Style="{DynamicResource SecondaryButtonStyle}"/>
|
</UniformGrid>
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
|
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
|
||||||
Title="Win11Debloat"
|
Title="Win11Debloat"
|
||||||
MinWidth="1130" MinHeight="600"
|
MinWidth="860" MinHeight="600"
|
||||||
ResizeMode="CanResize"
|
ResizeMode="CanResize"
|
||||||
SnapsToDevicePixels="True"
|
SnapsToDevicePixels="True"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
@@ -28,177 +28,6 @@
|
|||||||
</Trigger>
|
</Trigger>
|
||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
</Style>
|
</Style>
|
||||||
<!-- ComboBox Style -->
|
|
||||||
<Style TargetType="ComboBox">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource ComboBgColor}"/>
|
|
||||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
|
||||||
<Setter Property="BorderThickness" Value="1"/>
|
|
||||||
<Setter Property="Margin" Value="0,4,0,12"/>
|
|
||||||
<Setter Property="MinHeight" Value="33"/>
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate TargetType="ComboBox">
|
|
||||||
<Grid>
|
|
||||||
<!-- Left accent line -->
|
|
||||||
<Border x:Name="ClosedAccentLine" Width="3" Height="18" HorizontalAlignment="Left" VerticalAlignment="Stretch" Background="{DynamicResource ButtonBg}" CornerRadius="1.5" Panel.ZIndex="2"/>
|
|
||||||
<ToggleButton x:Name="ToggleButton" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Focusable="False" IsChecked="{Binding Path=IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" ClickMode="Press">
|
|
||||||
<ToggleButton.Style>
|
|
||||||
<Style TargetType="ToggleButton">
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate TargetType="ToggleButton">
|
|
||||||
<Border x:Name="Border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="4">
|
|
||||||
<TextBlock x:Name="Arrow"
|
|
||||||
Text=""
|
|
||||||
FontFamily="Segoe Fluent Icons"
|
|
||||||
FontSize="10"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Margin="0,0,8,0"
|
|
||||||
Foreground="{DynamicResource FgColor}"
|
|
||||||
RenderTransformOrigin="0.5,0.5">
|
|
||||||
<TextBlock.RenderTransform>
|
|
||||||
<RotateTransform x:Name="ArrowRotation" Angle="0"/>
|
|
||||||
</TextBlock.RenderTransform>
|
|
||||||
</TextBlock>
|
|
||||||
</Border>
|
|
||||||
<ControlTemplate.Triggers>
|
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
|
||||||
<Setter TargetName="Border" Property="Background" Value="{DynamicResource ComboHoverColor}"/>
|
|
||||||
</Trigger>
|
|
||||||
<Trigger Property="IsChecked" Value="True">
|
|
||||||
<Trigger.EnterActions>
|
|
||||||
<BeginStoryboard>
|
|
||||||
<Storyboard>
|
|
||||||
<DoubleAnimation Storyboard.TargetName="Arrow" Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)" To="180" Duration="0:0:0.2">
|
|
||||||
<DoubleAnimation.EasingFunction>
|
|
||||||
<CubicEase EasingMode="EaseOut"/>
|
|
||||||
</DoubleAnimation.EasingFunction>
|
|
||||||
</DoubleAnimation>
|
|
||||||
</Storyboard>
|
|
||||||
</BeginStoryboard>
|
|
||||||
</Trigger.EnterActions>
|
|
||||||
<Trigger.ExitActions>
|
|
||||||
<BeginStoryboard>
|
|
||||||
<Storyboard>
|
|
||||||
<DoubleAnimation Storyboard.TargetName="Arrow" Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)" To="0" Duration="0:0:0.2">
|
|
||||||
<DoubleAnimation.EasingFunction>
|
|
||||||
<CubicEase EasingMode="EaseOut"/>
|
|
||||||
</DoubleAnimation.EasingFunction>
|
|
||||||
</DoubleAnimation>
|
|
||||||
</Storyboard>
|
|
||||||
</BeginStoryboard>
|
|
||||||
</Trigger.ExitActions>
|
|
||||||
</Trigger>
|
|
||||||
</ControlTemplate.Triggers>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
</ToggleButton.Style>
|
|
||||||
</ToggleButton>
|
|
||||||
<ContentPresenter x:Name="ContentPresenter"
|
|
||||||
IsHitTestVisible="False"
|
|
||||||
Margin="10,0,20,0"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Content="{TemplateBinding SelectionBoxItem}"
|
|
||||||
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
|
|
||||||
ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"/>
|
|
||||||
<Popup x:Name="Popup"
|
|
||||||
Placement="Bottom"
|
|
||||||
IsOpen="{TemplateBinding IsDropDownOpen}"
|
|
||||||
AllowsTransparency="True"
|
|
||||||
Focusable="False"
|
|
||||||
PopupAnimation="Fade"
|
|
||||||
StaysOpen="False"
|
|
||||||
PlacementTarget="{Binding ElementName=ToggleButton}"
|
|
||||||
VerticalOffset="1"
|
|
||||||
HorizontalOffset="0">
|
|
||||||
<Grid x:Name="DropDown" MinWidth="{TemplateBinding ActualWidth}" MaxHeight="{TemplateBinding MaxDropDownHeight}" Margin="12">
|
|
||||||
<Border x:Name="DropDownBorder"
|
|
||||||
Background="{DynamicResource ComboItemBgColor}"
|
|
||||||
BorderBrush="{DynamicResource BorderColor}"
|
|
||||||
BorderThickness="1"
|
|
||||||
CornerRadius="4"
|
|
||||||
Padding="5,4,5,1">
|
|
||||||
<Border.Effect>
|
|
||||||
<DropShadowEffect BlurRadius="12" Opacity="0.25" ShadowDepth="4"/>
|
|
||||||
</Border.Effect>
|
|
||||||
<ScrollViewer Margin="0,2,0,0"
|
|
||||||
VerticalScrollBarVisibility="Auto"
|
|
||||||
HorizontalScrollBarVisibility="Disabled">
|
|
||||||
<ItemsPresenter Margin="0,0,0,1"/>
|
|
||||||
</ScrollViewer>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</Popup>
|
|
||||||
</Grid>
|
|
||||||
<ControlTemplate.Triggers>
|
|
||||||
<Trigger Property="SelectedIndex" Value="0">
|
|
||||||
<Setter TargetName="ClosedAccentLine" Property="Visibility" Value="Collapsed"/>
|
|
||||||
</Trigger>
|
|
||||||
<Trigger Property="SelectedIndex" Value="-1">
|
|
||||||
<Setter TargetName="ClosedAccentLine" Property="Visibility" Value="Collapsed"/>
|
|
||||||
</Trigger>
|
|
||||||
<Trigger Property="IsEnabled" Value="False">
|
|
||||||
<Setter TargetName="ClosedAccentLine" Property="Visibility" Value="Collapsed"/>
|
|
||||||
<Setter Property="Background" Value="{DynamicResource ButtonDisabled}"/>
|
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
|
||||||
<Setter Property="Foreground" Value="{DynamicResource ButtonTextDisabled}"/>
|
|
||||||
<Setter Property="Opacity" Value="0.6"/>
|
|
||||||
</Trigger>
|
|
||||||
</ControlTemplate.Triggers>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- ComboBoxItem Style -->
|
|
||||||
<Style TargetType="ComboBoxItem">
|
|
||||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
|
||||||
<Setter Property="Background" Value="Transparent"/>
|
|
||||||
<Setter Property="Padding" Value="10,8"/>
|
|
||||||
<Setter Property="Margin" Value="0,0,0,4"/>
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate TargetType="ComboBoxItem">
|
|
||||||
<Grid>
|
|
||||||
<Border x:Name="ItemBorder"
|
|
||||||
Background="{TemplateBinding Background}"
|
|
||||||
BorderBrush="{TemplateBinding BorderBrush}"
|
|
||||||
BorderThickness="{TemplateBinding BorderThickness}"
|
|
||||||
Padding="{TemplateBinding Padding}"
|
|
||||||
CornerRadius="4">
|
|
||||||
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
|
||||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
|
|
||||||
</Border>
|
|
||||||
<!-- Left accent line -->
|
|
||||||
<Border x:Name="AccentLine"
|
|
||||||
Width="3"
|
|
||||||
Height="15"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
VerticalAlignment="Stretch"
|
|
||||||
Background="{DynamicResource ButtonBg}"
|
|
||||||
CornerRadius="1.5"
|
|
||||||
Margin="0"
|
|
||||||
Visibility="Collapsed"/>
|
|
||||||
</Grid>
|
|
||||||
<ControlTemplate.Triggers>
|
|
||||||
<Trigger Property="IsSelected" Value="True">
|
|
||||||
<Setter TargetName="AccentLine" Property="Visibility" Value="Visible"/>
|
|
||||||
<Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource ComboItemSelectedColor}"/>
|
|
||||||
</Trigger>
|
|
||||||
<Trigger Property="IsHighlighted" Value="True">
|
|
||||||
<Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource ComboItemHoverColor}"/>
|
|
||||||
</Trigger>
|
|
||||||
</ControlTemplate.Triggers>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- TextBlock Label Style -->
|
<!-- TextBlock Label Style -->
|
||||||
<Style x:Key="LabelStyle" TargetType="TextBlock">
|
<Style x:Key="LabelStyle" TargetType="TextBlock">
|
||||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||||
@@ -582,6 +411,11 @@
|
|||||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
|
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
|
||||||
</MenuItem.Icon>
|
</MenuItem.Icon>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem x:Name="RestoreBackupBtn" Header="Restore backup" AutomationProperties.Name="Restore registry backup">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem x:Name="MenuDocumentation" Header="Documentation" AutomationProperties.Name="Documentation">
|
<MenuItem x:Name="MenuDocumentation" Header="Documentation" AutomationProperties.Name="Documentation">
|
||||||
<MenuItem.Icon>
|
<MenuItem.Icon>
|
||||||
@@ -898,6 +732,8 @@
|
|||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="10"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<StackPanel Grid.Column="0" Orientation="Horizontal">
|
<StackPanel Grid.Column="0" Orientation="Horizontal">
|
||||||
@@ -964,7 +800,9 @@
|
|||||||
</Popup>
|
</Popup>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Border x:Name="TweakSearchBorder" Grid.Column="2">
|
<CheckBox x:Name="ShowCurrentlyAppliedTweaksCheckBox" Grid.Column="2" Content="Automatically check currently applied tweaks" IsChecked="True" Foreground="{DynamicResource FgColor}" VerticalAlignment="Center" AutomationProperties.Name="Check applied tweaks" ToolTip="Check all tweaks currently active on this system for the current user. Unchecking clears all selections."/>
|
||||||
|
|
||||||
|
<Border x:Name="TweakSearchBorder" Grid.Column="4">
|
||||||
<Border.Style>
|
<Border.Style>
|
||||||
<Style TargetType="Border" BasedOn="{StaticResource SearchBoxBorderStyle}">
|
<Style TargetType="Border" BasedOn="{StaticResource SearchBoxBorderStyle}">
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
|
|||||||
@@ -44,8 +44,7 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Message Content -->
|
<!-- Message Content -->
|
||||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" MaxHeight="500" Padding="0" Margin="20,12,1,20">
|
<Grid Grid.Row="1" Margin="20,12,1,20">
|
||||||
<Grid Margin="0,0,20,0">
|
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
@@ -58,20 +57,21 @@
|
|||||||
FontSize="24"
|
FontSize="24"
|
||||||
Foreground="{DynamicResource FgColor}"
|
Foreground="{DynamicResource FgColor}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Margin="4,2,14,0"
|
Margin="4,0,14,0"
|
||||||
Visibility="Collapsed"/>
|
Visibility="Collapsed"/>
|
||||||
|
|
||||||
<!-- Message Text -->
|
<!-- Message Text -->
|
||||||
|
<ScrollViewer Grid.Column="1" VerticalScrollBarVisibility="Auto" MaxHeight="300" Padding="0">
|
||||||
<TextBlock x:Name="MessageText"
|
<TextBlock x:Name="MessageText"
|
||||||
Grid.Column="1"
|
|
||||||
Text="Message content goes here"
|
Text="Message content goes here"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
FontSize="14"
|
FontSize="14"
|
||||||
LineHeight="20"
|
LineHeight="20"
|
||||||
Foreground="{DynamicResource FgColor}"
|
Foreground="{DynamicResource FgColor}"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"
|
||||||
</Grid>
|
Margin="0,0,20,0"/>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<!-- Button Panel -->
|
<!-- Button Panel -->
|
||||||
<Border Grid.Row="2"
|
<Border Grid.Row="2"
|
||||||
|
|||||||
427
Schemas/RestoreBackupWindow.xaml
Normal file
427
Schemas/RestoreBackupWindow.xaml
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="Restore Backup"
|
||||||
|
Width="500"
|
||||||
|
SizeToContent="Height"
|
||||||
|
MaxHeight="560"
|
||||||
|
ResizeMode="NoResize"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
WindowStyle="None"
|
||||||
|
AllowsTransparency="True"
|
||||||
|
Background="Transparent"
|
||||||
|
Topmost="False"
|
||||||
|
ShowInTaskbar="False">
|
||||||
|
|
||||||
|
<Window.Resources>
|
||||||
|
<Style x:Key="RestoreOptionTileStyle" TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBg}"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="12,10"/>
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="8"
|
||||||
|
Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0,0,0,1"/>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SecondaryButtonDisabled}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource SecondaryButtonTextDisabled}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHover}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressed}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<Border Style="{DynamicResource ModalCardBorderStyle}">
|
||||||
|
<Grid Margin="0">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid Grid.Row="0" x:Name="TitleBar" Style="{DynamicResource ModalTitleBarStyle}">
|
||||||
|
<TextBlock x:Name="TitleText"
|
||||||
|
Text="Restore Backup"
|
||||||
|
Style="{DynamicResource ModalTitleTextStyle}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Button x:Name="CloseBtn" Grid.Row="0"
|
||||||
|
HorizontalAlignment="Right" VerticalAlignment="Top"
|
||||||
|
Width="36" Height="32"
|
||||||
|
BorderThickness="0"
|
||||||
|
Cursor="Hand"
|
||||||
|
ToolTip="Close"
|
||||||
|
AutomationProperties.Name="Close">
|
||||||
|
<Button.Template>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border Background="{TemplateBinding Background}" BorderThickness="0" CornerRadius="0,8,0,0">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Button.Template>
|
||||||
|
<Button.Style>
|
||||||
|
<Style TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource CloseHover}"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Button.Style>
|
||||||
|
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="10"/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Margin="0">
|
||||||
|
<Grid Margin="20,4,20,18">
|
||||||
|
|
||||||
|
<TabControl x:Name="RestoreModeTabs"
|
||||||
|
SelectedIndex="0"
|
||||||
|
Background="Transparent"
|
||||||
|
BorderThickness="0"
|
||||||
|
Padding="0">
|
||||||
|
<TabControl.ItemContainerStyle>
|
||||||
|
<Style TargetType="TabItem">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed"/>
|
||||||
|
<Setter Property="Height" Value="0"/>
|
||||||
|
</Style>
|
||||||
|
</TabControl.ItemContainerStyle>
|
||||||
|
<TabItem x:Name="SelectTypeTab" Header="SelectType">
|
||||||
|
<Grid x:Name="SelectTypePanel">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
FontSize="14"
|
||||||
|
LineHeight="20"
|
||||||
|
Foreground="{DynamicResource FgColor}"
|
||||||
|
Text="Choose what changes you want to restore."/>
|
||||||
|
|
||||||
|
<Button x:Name="ChooseRegistryBtn"
|
||||||
|
Grid.Row="1"
|
||||||
|
Margin="0,14,0,0"
|
||||||
|
Style="{StaticResource RestoreOptionTileStyle}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0"
|
||||||
|
Text=""
|
||||||
|
FontFamily="Segoe Fluent Icons"
|
||||||
|
FontSize="24"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="14,0,14,0"/>
|
||||||
|
<StackPanel Grid.Column="1"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0,0,12,0">
|
||||||
|
<TextBlock Text="Restore Registry Backup"
|
||||||
|
FontSize="16"
|
||||||
|
FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="Restore system registry configuration from a backup"
|
||||||
|
FontSize="12"
|
||||||
|
Opacity="0.75"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Margin="0,2,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button x:Name="ChooseStartMenuBtn"
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="0,10,0,0"
|
||||||
|
Style="{StaticResource RestoreOptionTileStyle}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0"
|
||||||
|
Text=""
|
||||||
|
FontFamily="Segoe Fluent Icons"
|
||||||
|
FontSize="24"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="14,0,14,0"/>
|
||||||
|
<StackPanel Grid.Column="1"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0,0,12,0">
|
||||||
|
<TextBlock Text="Restore Start Menu Backup"
|
||||||
|
FontSize="16"
|
||||||
|
FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="Restore the Start Menu pinned apps layout from a backup"
|
||||||
|
FontSize="12"
|
||||||
|
Opacity="0.75"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Margin="0,2,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem x:Name="RegistryTab" Header="Registry">
|
||||||
|
<Grid x:Name="RegistryPanel">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid x:Name="IntroInfoPanel" Grid.Row="0">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
FontSize="14"
|
||||||
|
LineHeight="20"
|
||||||
|
Foreground="{DynamicResource FgColor}"
|
||||||
|
Text="This will restore any system registry changes made by Win11Debloat to their previous state. You can review the changes after selecting a backup file. Apps will need to be reinstalled manually."/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1"
|
||||||
|
Margin="0,12,0,2"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
FontSize="13"
|
||||||
|
Foreground="{DynamicResource FgColor}"
|
||||||
|
Opacity="0.75"
|
||||||
|
Text="Warning: Only use backup files generated by Win11Debloat."/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid x:Name="OverviewPanel" Grid.Row="1" Margin="0" Visibility="Collapsed">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid Grid.Row="0" Margin="0,0,0,16">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="File:" Style="{DynamicResource ModalInfoLabelTextStyle}"/>
|
||||||
|
<TextBlock x:Name="BackupFileText" Grid.Row="0" Grid.Column="1" Text="Not selected" Style="{DynamicResource ModalInfoValueTextStyle}" TextWrapping="Wrap" Margin="0,0,0,8"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="Created:" Style="{DynamicResource ModalInfoLabelTextStyle}"/>
|
||||||
|
<TextBlock x:Name="BackupCreatedText" Grid.Row="1" Grid.Column="1" Text="N/A" Style="{DynamicResource ModalInfoValueTextStyle}"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="Target:" Style="{DynamicResource ModalInfoLabelTextStyle}" Margin="0,0,16,0"/>
|
||||||
|
<TextBlock x:Name="BackupTargetText" Grid.Row="2" Grid.Column="1" Text="N/A" Style="{DynamicResource ModalInfoValueTextStyle}" Margin="0"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Border Grid.Row="1" Height="1" Background="{DynamicResource BorderColor}" Margin="0,0,0,12"/>
|
||||||
|
|
||||||
|
<Grid x:Name="OverviewFeaturesSection" Grid.Row="2">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
Text="The following changes will be reverted:"
|
||||||
|
FontSize="13"
|
||||||
|
Foreground="{DynamicResource FgColor}"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Margin="0,0,0,8"/>
|
||||||
|
|
||||||
|
<ItemsControl x:Name="FeaturesItemsControl" Grid.Row="1">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel Orientation="Vertical" IsItemsHost="True"/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding DisplayText}" FontSize="12" Foreground="{DynamicResource FgColor}" TextWrapping="Wrap" Margin="0,0,0,6"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock x:Name="OverviewSummaryText"
|
||||||
|
Grid.Row="2"
|
||||||
|
FontSize="13"
|
||||||
|
Foreground="{DynamicResource FgColor}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Visibility="Collapsed"
|
||||||
|
Text="This will restore the Start Menu pinned apps layout for the current user."/>
|
||||||
|
|
||||||
|
<Border x:Name="ReappliedSeparator" Grid.Row="3" Height="1" Background="{DynamicResource BorderColor}" Margin="0,12,0,12" Visibility="Collapsed"/>
|
||||||
|
|
||||||
|
<Grid x:Name="ReappliedPanel" Grid.Row="4" Visibility="Collapsed">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
Text="The following changes will be re-applied:"
|
||||||
|
FontSize="13"
|
||||||
|
Foreground="{DynamicResource FgColor}"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Margin="0,0,0,8"/>
|
||||||
|
|
||||||
|
<ItemsControl x:Name="ReappliedFeaturesItemsControl" Grid.Row="1">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel Orientation="Vertical" IsItemsHost="True"/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding DisplayText}" FontSize="12" Foreground="{DynamicResource FgColor}" TextWrapping="Wrap" Margin="0,0,0,6"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Border x:Name="NonRevertibleSeparator" Grid.Row="5" Height="1" Background="{DynamicResource BorderColor}" Margin="0,12,0,12" Visibility="Collapsed"/>
|
||||||
|
|
||||||
|
<Grid x:Name="NonRevertiblePanel" Grid.Row="6" Visibility="Collapsed">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
Text="The following changes won't be reverted:"
|
||||||
|
FontSize="13"
|
||||||
|
Foreground="{DynamicResource FgColor}"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Margin="0,0,0,8"/>
|
||||||
|
|
||||||
|
<ItemsControl x:Name="NonRevertibleFeaturesItemsControl" Grid.Row="1">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel Orientation="Vertical" IsItemsHost="True"/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding DisplayText}" FontSize="12" Foreground="{DynamicResource FgColor}" Opacity="0.85" TextWrapping="Wrap" Margin="0,0,0,6"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
|
||||||
|
<TextBlock x:Name="NonRevertibleWikiLink"
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="0,8,0,0"
|
||||||
|
Text="Visit the wiki for more information"
|
||||||
|
FontSize="13"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Style="{DynamicResource HyperlinkStyle}"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem x:Name="StartMenuTab" Header="StartMenu">
|
||||||
|
<Grid x:Name="StartMenuPanel">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid x:Name="StartMenuIntroPanel" Grid.Row="0" Visibility="Visible">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
FontSize="14"
|
||||||
|
LineHeight="20"
|
||||||
|
Margin="0,0,0,12"
|
||||||
|
Foreground="{DynamicResource FgColor}"
|
||||||
|
Text="This will restore the Start Menu pinned apps layout for the selected user(s) using a backup. Win11Debloat can automatically find the backup created by the script."/>
|
||||||
|
|
||||||
|
<ComboBox x:Name="StartMenuScopeCombo"
|
||||||
|
Grid.Row="1"
|
||||||
|
SelectedIndex="0"
|
||||||
|
Margin="0,0,0,2"
|
||||||
|
MinWidth="408"
|
||||||
|
HorizontalAlignment="Left">
|
||||||
|
<ComboBoxItem Content="Current user" Tag="CurrentUser"/>
|
||||||
|
<ComboBoxItem Content="All users" Tag="AllUsers"/>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<CheckBox x:Name="StartMenuAutoBackupCheck"
|
||||||
|
Grid.Row="2"
|
||||||
|
Content="Automatically find Start Menu backup"
|
||||||
|
IsChecked="True"
|
||||||
|
FontSize="12"
|
||||||
|
Margin="0,10,0,0"
|
||||||
|
Foreground="{DynamicResource FgColor}"
|
||||||
|
Opacity="0.85"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</TabItem>
|
||||||
|
</TabControl>
|
||||||
|
</Grid>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<Border Grid.Row="3" Style="{DynamicResource ModalFooterBorderStyle}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Button x:Name="PrimaryActionBtn"
|
||||||
|
Grid.Column="0"
|
||||||
|
Content="Next"
|
||||||
|
Visibility="Collapsed"
|
||||||
|
Style="{DynamicResource ModalPrimaryStretchedButtonStyle}"/>
|
||||||
|
|
||||||
|
<Button x:Name="BackBtn"
|
||||||
|
Grid.Column="1"
|
||||||
|
Content="Back"
|
||||||
|
Visibility="Collapsed"
|
||||||
|
Style="{DynamicResource ModalSecondaryStretchedButtonStyle}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Window>
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
BorderThickness="{TemplateBinding BorderThickness}"
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
CornerRadius="4"
|
CornerRadius="4"
|
||||||
Padding="{TemplateBinding Padding}">
|
Padding="{TemplateBinding Padding}">
|
||||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,0,0,1"/>
|
||||||
</Border>
|
</Border>
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
@@ -125,6 +125,255 @@
|
|||||||
<Setter Property="TextAlignment" Value="Center"/>
|
<Setter Property="TextAlignment" Value="Center"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- Shared modal window shell styles -->
|
||||||
|
<Style x:Key="ModalCardBorderStyle" TargetType="Border">
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="CornerRadius" Value="8"/>
|
||||||
|
<Setter Property="Background" Value="{DynamicResource CardBgColor}"/>
|
||||||
|
<Setter Property="Margin" Value="25"/>
|
||||||
|
<Setter Property="Effect">
|
||||||
|
<Setter.Value>
|
||||||
|
<DropShadowEffect Color="Black"
|
||||||
|
Opacity="0.15"
|
||||||
|
BlurRadius="20"
|
||||||
|
ShadowDepth="0"
|
||||||
|
Direction="0"/>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ModalTitleBarStyle" TargetType="Grid">
|
||||||
|
<Setter Property="Height" Value="48"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ModalTitleTextStyle" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||||
|
<Setter Property="FontSize" Value="16"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="Margin" Value="20,0,0,0"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ModalFooterBorderStyle" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource BgColor}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0,1,0,0"/>
|
||||||
|
<Setter Property="Padding" Value="16,12"/>
|
||||||
|
<Setter Property="CornerRadius" Value="0,0,8,8"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ModalFooterButtonsRightStyle" TargetType="StackPanel">
|
||||||
|
<Setter Property="Orientation" Value="Horizontal"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Right"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ModalPrimaryActionButtonStyle" TargetType="Button" BasedOn="{StaticResource PrimaryButtonStyle}">
|
||||||
|
<Setter Property="Height" Value="32"/>
|
||||||
|
<Setter Property="MinWidth" Value="80"/>
|
||||||
|
<Setter Property="Margin" Value="4,0"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ModalSecondaryActionButtonStyle" TargetType="Button" BasedOn="{StaticResource SecondaryButtonStyle}">
|
||||||
|
<Setter Property="Height" Value="32"/>
|
||||||
|
<Setter Property="MinWidth" Value="80"/>
|
||||||
|
<Setter Property="Margin" Value="4,0"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ModalPrimaryStretchedButtonStyle" TargetType="Button" BasedOn="{StaticResource PrimaryButtonStyle}">
|
||||||
|
<Setter Property="Height" Value="32"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||||
|
<Setter Property="Margin" Value="4,0"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ModalSecondaryStretchedButtonStyle" TargetType="Button" BasedOn="{StaticResource SecondaryButtonStyle}">
|
||||||
|
<Setter Property="Height" Value="32"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||||
|
<Setter Property="Margin" Value="4,0"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ModalInfoLabelTextStyle" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,16,8"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ModalInfoValueTextStyle" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,8"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Shared ComboBox style used across windows -->
|
||||||
|
<Style TargetType="ComboBoxItem">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="Padding" Value="10,8"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,4"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ComboBoxItem">
|
||||||
|
<Grid>
|
||||||
|
<Border x:Name="ItemBorder"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
Padding="{TemplateBinding Padding}"
|
||||||
|
CornerRadius="4">
|
||||||
|
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
|
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
|
||||||
|
</Border>
|
||||||
|
<Border x:Name="AccentLine"
|
||||||
|
Width="3"
|
||||||
|
Height="15"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Background="{DynamicResource ButtonBg}"
|
||||||
|
CornerRadius="1.5"
|
||||||
|
Margin="0"
|
||||||
|
Visibility="Collapsed"/>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter TargetName="AccentLine" Property="Visibility" Value="Visible"/>
|
||||||
|
<Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource ComboItemSelectedColor}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsHighlighted" Value="True">
|
||||||
|
<Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource ComboItemHoverColor}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="ComboBox">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ComboBgColor}"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Margin" Value="0,4,0,12"/>
|
||||||
|
<Setter Property="MinHeight" Value="33"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ComboBox">
|
||||||
|
<Grid>
|
||||||
|
<Border x:Name="ClosedAccentLine" Width="3" Height="18" HorizontalAlignment="Left" VerticalAlignment="Stretch" Background="{DynamicResource ButtonBg}" CornerRadius="1.5" Panel.ZIndex="2"/>
|
||||||
|
<ToggleButton x:Name="ToggleButton" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Focusable="False" IsChecked="{Binding Path=IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" ClickMode="Press">
|
||||||
|
<ToggleButton.Style>
|
||||||
|
<Style TargetType="ToggleButton">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ToggleButton">
|
||||||
|
<Border x:Name="Border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="4">
|
||||||
|
<TextBlock x:Name="Arrow"
|
||||||
|
Text=""
|
||||||
|
FontFamily="Segoe Fluent Icons"
|
||||||
|
FontSize="10"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0,0,8,0"
|
||||||
|
Foreground="{DynamicResource FgColor}"
|
||||||
|
RenderTransformOrigin="0.5,0.5">
|
||||||
|
<TextBlock.RenderTransform>
|
||||||
|
<RotateTransform Angle="0"/>
|
||||||
|
</TextBlock.RenderTransform>
|
||||||
|
</TextBlock>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Border" Property="Background" Value="{DynamicResource ComboHoverColor}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Trigger.EnterActions>
|
||||||
|
<BeginStoryboard>
|
||||||
|
<Storyboard>
|
||||||
|
<DoubleAnimation Storyboard.TargetName="Arrow" Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)" To="180" Duration="0:0:0.2">
|
||||||
|
<DoubleAnimation.EasingFunction>
|
||||||
|
<CubicEase EasingMode="EaseOut"/>
|
||||||
|
</DoubleAnimation.EasingFunction>
|
||||||
|
</DoubleAnimation>
|
||||||
|
</Storyboard>
|
||||||
|
</BeginStoryboard>
|
||||||
|
</Trigger.EnterActions>
|
||||||
|
<Trigger.ExitActions>
|
||||||
|
<BeginStoryboard>
|
||||||
|
<Storyboard>
|
||||||
|
<DoubleAnimation Storyboard.TargetName="Arrow" Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)" To="0" Duration="0:0:0.2">
|
||||||
|
<DoubleAnimation.EasingFunction>
|
||||||
|
<CubicEase EasingMode="EaseOut"/>
|
||||||
|
</DoubleAnimation.EasingFunction>
|
||||||
|
</DoubleAnimation>
|
||||||
|
</Storyboard>
|
||||||
|
</BeginStoryboard>
|
||||||
|
</Trigger.ExitActions>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</ToggleButton.Style>
|
||||||
|
</ToggleButton>
|
||||||
|
<ContentPresenter x:Name="ContentPresenter"
|
||||||
|
IsHitTestVisible="False"
|
||||||
|
Margin="10,0,20,0"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Content="{TemplateBinding SelectionBoxItem}"
|
||||||
|
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
|
||||||
|
ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"/>
|
||||||
|
<Popup x:Name="Popup"
|
||||||
|
Placement="Bottom"
|
||||||
|
IsOpen="{TemplateBinding IsDropDownOpen}"
|
||||||
|
AllowsTransparency="True"
|
||||||
|
Focusable="False"
|
||||||
|
PopupAnimation="Fade"
|
||||||
|
StaysOpen="False"
|
||||||
|
PlacementTarget="{Binding ElementName=ToggleButton}"
|
||||||
|
VerticalOffset="1"
|
||||||
|
HorizontalOffset="0">
|
||||||
|
<Grid x:Name="DropDown" MinWidth="{TemplateBinding ActualWidth}" MaxHeight="{TemplateBinding MaxDropDownHeight}" Margin="12">
|
||||||
|
<Border x:Name="DropDownBorder"
|
||||||
|
Background="{DynamicResource ComboItemBgColor}"
|
||||||
|
BorderBrush="{DynamicResource BorderColor}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="4"
|
||||||
|
Padding="5,4,5,1">
|
||||||
|
<Border.Effect>
|
||||||
|
<DropShadowEffect BlurRadius="12" Opacity="0.25" ShadowDepth="4"/>
|
||||||
|
</Border.Effect>
|
||||||
|
<ScrollViewer Margin="0,2,0,0"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
HorizontalScrollBarVisibility="Disabled">
|
||||||
|
<ItemsPresenter Margin="0,0,0,1"/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Popup>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="SelectedIndex" Value="0">
|
||||||
|
<Setter TargetName="ClosedAccentLine" Property="Visibility" Value="Collapsed"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="SelectedIndex" Value="-1">
|
||||||
|
<Setter TargetName="ClosedAccentLine" Property="Visibility" Value="Collapsed"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter TargetName="ClosedAccentLine" Property="Visibility" Value="Collapsed"/>
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ButtonDisabled}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonTextDisabled}"/>
|
||||||
|
<Setter Property="Opacity" Value="0.6"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
<!-- Base CheckBox style used across windows -->
|
<!-- Base CheckBox style used across windows -->
|
||||||
<Style TargetType="CheckBox">
|
<Style TargetType="CheckBox">
|
||||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||||
|
|||||||
@@ -46,9 +46,8 @@ function PrintPendingChanges {
|
|||||||
}
|
}
|
||||||
default {
|
default {
|
||||||
if ($script:Features -and $script:Features.ContainsKey($parameterName)) {
|
if ($script:Features -and $script:Features.ContainsKey($parameterName)) {
|
||||||
$action = $script:Features[$parameterName].Action
|
|
||||||
$message = $script:Features[$parameterName].Label
|
$message = $script:Features[$parameterName].Label
|
||||||
Write-Output "- $action $message"
|
Write-Output "- $message"
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
# Fallback: show the parameter name if no feature description is available
|
# Fallback: show the parameter name if no feature description is available
|
||||||
|
|||||||
21
Scripts/Features/BackupRegistryFeatureSelection.ps1
Normal file
21
Scripts/Features/BackupRegistryFeatureSelection.ps1
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
function Get-FeatureId {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Feature
|
||||||
|
)
|
||||||
|
|
||||||
|
$featureId = [string]$Feature.FeatureId
|
||||||
|
if ([string]::IsNullOrWhiteSpace($featureId)) {
|
||||||
|
throw 'Selected feature is missing required FeatureId.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return $featureId
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RegistryBackedFeatures {
|
||||||
|
param(
|
||||||
|
[object[]]$Features = @()
|
||||||
|
)
|
||||||
|
|
||||||
|
return @($Features | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey) })
|
||||||
|
}
|
||||||
319
Scripts/Features/BackupRegistrySnapshotCapture.ps1
Normal file
319
Scripts/Features/BackupRegistrySnapshotCapture.ps1
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
function Get-RegistryBackupCapturePlans {
|
||||||
|
param(
|
||||||
|
[object[]]$SelectedRegistryFeatures = @(),
|
||||||
|
[object[]]$UndoRegistryFeatures = @(),
|
||||||
|
[switch]$UseSysprepRegFiles
|
||||||
|
)
|
||||||
|
|
||||||
|
$planMap = @{}
|
||||||
|
|
||||||
|
foreach ($feature in $SelectedRegistryFeatures) {
|
||||||
|
$regFilePath = Get-RegistryFilePathForFeature -RegistryKey $feature.RegistryKey -UseSysprepRegFiles:$UseSysprepRegFiles
|
||||||
|
if (-not (Test-Path $regFilePath)) {
|
||||||
|
throw "Unable to find registry file for backup: $($feature.RegistryKey) ($regFilePath)"
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($operation in @(Get-RegFileOperations -regFilePath $regFilePath)) {
|
||||||
|
if (-not $operation.KeyPath) { continue }
|
||||||
|
Add-RegistryPlanOperation -PlanMap $planMap -Operation $operation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($feature in $UndoRegistryFeatures) {
|
||||||
|
$regFilePath = Resolve-RegistryBackupUndoFilePath -Feature $feature
|
||||||
|
if ([string]::IsNullOrWhiteSpace($regFilePath)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $regFilePath)) {
|
||||||
|
$undoKeyDescription = if (-not [string]::IsNullOrWhiteSpace([string]$feature.RegistryUndoKey)) {
|
||||||
|
[string]$feature.RegistryUndoKey
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[string]$feature.RegistryKey
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "Unable to find registry undo file for backup: $undoKeyDescription ($regFilePath)"
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($operation in @(Get-RegFileOperations -regFilePath $regFilePath)) {
|
||||||
|
if (-not $operation.KeyPath) { continue }
|
||||||
|
Add-RegistryPlanOperation -PlanMap $planMap -Operation $operation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return @(
|
||||||
|
foreach ($entry in $planMap.Values) {
|
||||||
|
[PSCustomObject]@{
|
||||||
|
Path = $entry.Path
|
||||||
|
IncludeSubKeys = [bool]$entry.IncludeSubKeys
|
||||||
|
CaptureAllValues = [bool]$entry.CaptureAllValues
|
||||||
|
ValueNames = @($entry.ValueNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Add-RegistryPlanOperation {
|
||||||
|
param(
|
||||||
|
[hashtable]$PlanMap,
|
||||||
|
[PSCustomObject]$Operation
|
||||||
|
)
|
||||||
|
|
||||||
|
$mapKey = $Operation.KeyPath.ToLowerInvariant()
|
||||||
|
if (-not $PlanMap.ContainsKey($mapKey)) {
|
||||||
|
$PlanMap[$mapKey] = [PSCustomObject]@{
|
||||||
|
Path = $Operation.KeyPath
|
||||||
|
IncludeSubKeys = $false
|
||||||
|
CaptureAllValues = $false
|
||||||
|
ValueNames = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$plan = $PlanMap[$mapKey]
|
||||||
|
switch ($Operation.OperationType) {
|
||||||
|
'DeleteKey' {
|
||||||
|
$plan.IncludeSubKeys = $true
|
||||||
|
$plan.CaptureAllValues = $true
|
||||||
|
}
|
||||||
|
'SetValue' {
|
||||||
|
if (-not $plan.CaptureAllValues) {
|
||||||
|
$null = $plan.ValueNames.Add([string]$Operation.ValueName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'DeleteValue' {
|
||||||
|
if (-not $plan.CaptureAllValues) {
|
||||||
|
$null = $plan.ValueNames.Add([string]$Operation.ValueName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-RegistryBackupUndoFilePath {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Feature
|
||||||
|
)
|
||||||
|
|
||||||
|
$undoRegistryKey = [string]$Feature.RegistryUndoKey
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($undoRegistryKey)) {
|
||||||
|
$resolvedUndoPath = Resolve-UndoRegFilePath -FileName $undoRegistryKey
|
||||||
|
return Join-Path $script:RegfilesPath $resolvedUndoPath
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedRegistryKey = [string]$Feature.RegistryKey
|
||||||
|
if ([string]::IsNullOrWhiteSpace($resolvedRegistryKey)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([System.IO.Path]::IsPathRooted($resolvedRegistryKey)) {
|
||||||
|
return $resolvedRegistryKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return Join-Path $script:RegfilesPath $resolvedRegistryKey
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RegistrySnapshotsForBackup {
|
||||||
|
param(
|
||||||
|
[object[]]$CapturePlans = @()
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($CapturePlans.Count -eq 0) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
$snapshotScript = {
|
||||||
|
param($plans)
|
||||||
|
|
||||||
|
$snapshots = @()
|
||||||
|
foreach ($plan in $plans) {
|
||||||
|
$snapshots += Get-RegistryKeySnapshot -KeyPath $plan.Path -CaptureAllValues:$plan.CaptureAllValues -ValueNames @($plan.ValueNames) -IncludeSubKeys:$plan.IncludeSubKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
return @($snapshots)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($script:Params.ContainsKey('Sysprep') -or $script:Params.ContainsKey('User')) {
|
||||||
|
return Invoke-WithLoadedBackupHive -ScriptBlock $snapshotScript -ArgumentObject @($CapturePlans)
|
||||||
|
}
|
||||||
|
|
||||||
|
return & $snapshotScript $CapturePlans
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-WithLoadedBackupHive {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[scriptblock]$ScriptBlock,
|
||||||
|
$ArgumentObject = $null
|
||||||
|
)
|
||||||
|
|
||||||
|
$hiveDatPath = if ($script:Params.ContainsKey('Sysprep')) {
|
||||||
|
GetUserDirectory -userName 'Default' -fileName 'NTUSER.DAT'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
GetUserDirectory -userName $script:Params.Item('User') -fileName 'NTUSER.DAT'
|
||||||
|
}
|
||||||
|
|
||||||
|
$global:LASTEXITCODE = 0
|
||||||
|
reg load 'HKU\Default' "$hiveDatPath" | Out-Null
|
||||||
|
$loadExitCode = $LASTEXITCODE
|
||||||
|
if ($loadExitCode -ne 0) {
|
||||||
|
throw "Failed to load user hive for registry backup at '$hiveDatPath' (exit code: $loadExitCode)"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return & $ScriptBlock $ArgumentObject
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$global:LASTEXITCODE = 0
|
||||||
|
reg unload 'HKU\Default' | Out-Null
|
||||||
|
$unloadExitCode = $LASTEXITCODE
|
||||||
|
if ($unloadExitCode -ne 0) {
|
||||||
|
throw "Failed to unload registry hive 'HKU\Default' (exit code: $unloadExitCode)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RegistryKeySnapshot {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$KeyPath,
|
||||||
|
[bool]$CaptureAllValues = $false,
|
||||||
|
[string[]]$ValueNames = @(),
|
||||||
|
[bool]$IncludeSubKeys = $false
|
||||||
|
)
|
||||||
|
|
||||||
|
$registryParts = Split-RegistryPath -path $KeyPath
|
||||||
|
if (-not $registryParts) {
|
||||||
|
throw "Unsupported registry path in backup: $KeyPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$rootKey = Get-RegistryRootKey -hiveName $registryParts.Hive
|
||||||
|
if (-not $rootKey) {
|
||||||
|
throw "Unsupported registry hive in backup: $($registryParts.Hive)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$subKeyPath = $registryParts.SubKey
|
||||||
|
$key = $rootKey.OpenSubKey($subKeyPath, $false)
|
||||||
|
if ($null -eq $key) {
|
||||||
|
return @{
|
||||||
|
Path = $KeyPath
|
||||||
|
Exists = $false
|
||||||
|
Values = @()
|
||||||
|
SubKeys = @()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return (Convert-RegistryKeyToSnapshot -RegistryKey $key -FullPath $KeyPath -CaptureAllValues:$CaptureAllValues -ValueNames $ValueNames -IncludeSubKeys:$IncludeSubKeys)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$key.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Convert-RegistryKeyToSnapshot {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[Microsoft.Win32.RegistryKey]$RegistryKey,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$FullPath,
|
||||||
|
[bool]$CaptureAllValues = $false,
|
||||||
|
[string[]]$ValueNames = @(),
|
||||||
|
[bool]$IncludeSubKeys = $false
|
||||||
|
)
|
||||||
|
|
||||||
|
$values = @()
|
||||||
|
if ($CaptureAllValues) {
|
||||||
|
foreach ($valueName in @($RegistryKey.GetValueNames())) {
|
||||||
|
$values += @(Convert-RegistryValueToSnapshot -RegistryKey $RegistryKey -ValueName $valueName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
foreach ($valueName in @($ValueNames | Sort-Object -Unique)) {
|
||||||
|
$exists = ($RegistryKey.GetValueNames() -contains $valueName)
|
||||||
|
if ($exists) {
|
||||||
|
$values += @(Convert-RegistryValueToSnapshot -RegistryKey $RegistryKey -ValueName $valueName)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$values += @{
|
||||||
|
Name = $valueName
|
||||||
|
Exists = $false
|
||||||
|
Kind = $null
|
||||||
|
Data = $null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$subKeys = @()
|
||||||
|
if ($IncludeSubKeys) {
|
||||||
|
foreach ($subKeyName in @($RegistryKey.GetSubKeyNames())) {
|
||||||
|
$childKey = $RegistryKey.OpenSubKey($subKeyName, $false)
|
||||||
|
if ($null -eq $childKey) { continue }
|
||||||
|
|
||||||
|
try {
|
||||||
|
$childPath = if ([string]::IsNullOrWhiteSpace($FullPath)) { $subKeyName } else { "$FullPath\$subKeyName" }
|
||||||
|
$subKeys += @(Convert-RegistryKeyToSnapshot -RegistryKey $childKey -FullPath $childPath -CaptureAllValues:$true -IncludeSubKeys:$true)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$childKey.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return @{
|
||||||
|
Path = $FullPath
|
||||||
|
Exists = $true
|
||||||
|
Values = $values
|
||||||
|
SubKeys = $subKeys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Convert-RegistryValueToSnapshot {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[Microsoft.Win32.RegistryKey]$RegistryKey,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[string]$ValueName
|
||||||
|
)
|
||||||
|
|
||||||
|
$valueKind = $RegistryKey.GetValueKind($ValueName)
|
||||||
|
$value = $RegistryKey.GetValue($ValueName, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
|
||||||
|
try {
|
||||||
|
$normalizedValue = switch ($valueKind) {
|
||||||
|
([Microsoft.Win32.RegistryValueKind]::Binary) { @($value | ForEach-Object { [int]$_ }) }
|
||||||
|
([Microsoft.Win32.RegistryValueKind]::MultiString) { @($value) }
|
||||||
|
([Microsoft.Win32.RegistryValueKind]::DWord) { [BitConverter]::ToUInt32([BitConverter]::GetBytes([int32]$value), 0) }
|
||||||
|
([Microsoft.Win32.RegistryValueKind]::QWord) { [BitConverter]::ToUInt64([BitConverter]::GetBytes([int64]$value), 0) }
|
||||||
|
default { if ($null -ne $value) { [string]$value } else { $null } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$valueType = if ($null -ne $value) { $value.GetType().FullName } else { '<null>' }
|
||||||
|
$valueForLog = if ($null -eq $value) { '<null>' } elseif ($value -is [array]) { ($value -join ',') } else { [string]$value }
|
||||||
|
throw "Failed to normalize registry value for backup. Key='$($RegistryKey.Name)' Name='$ValueName' Kind='$valueKind' RawType='$valueType' RawValue='$valueForLog'. InnerError: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return @{
|
||||||
|
Name = $ValueName
|
||||||
|
Exists = $true
|
||||||
|
Kind = $valueKind.ToString()
|
||||||
|
Data = $normalizedValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RegistryBackupTargetDescription {
|
||||||
|
if ($script:Params.ContainsKey('Sysprep')) {
|
||||||
|
return 'DefaultUserProfile'
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedUserName = [string](GetUserName)
|
||||||
|
|
||||||
|
if ($script:Params.ContainsKey('User')) {
|
||||||
|
return "User:$resolvedUserName"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "CurrentUser:$resolvedUserName"
|
||||||
|
}
|
||||||
110
Scripts/Features/BackupRegistryState.ps1
Normal file
110
Scripts/Features/BackupRegistryState.ps1
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
function New-RegistrySettingsBackup {
|
||||||
|
param(
|
||||||
|
[string[]]$ActionableKeys,
|
||||||
|
[object[]]$ExtraFeatures = @()
|
||||||
|
)
|
||||||
|
|
||||||
|
$ActionableKeys = @($ActionableKeys)
|
||||||
|
$selectedFeatures = @(Get-SelectedFeatures -ActionableKeys $ActionableKeys)
|
||||||
|
$undoFeatures = @($ExtraFeatures | Where-Object { $_ -ne $null })
|
||||||
|
$allFeatures = @($selectedFeatures) + @($undoFeatures)
|
||||||
|
if (@($allFeatures | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey) }).Count -eq 0) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$timestamp = Get-Date
|
||||||
|
$backupDirectory = $script:RegistryBackupsPath
|
||||||
|
if (-not (Test-Path $backupDirectory)) {
|
||||||
|
New-Item -ItemType Directory -Path $backupDirectory -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$backupFileName = 'Win11Debloat-RegistryBackup-{0}.json' -f $timestamp.ToString('yyyyMMdd_HHmmss')
|
||||||
|
$backupFilePath = Join-Path $backupDirectory $backupFileName
|
||||||
|
|
||||||
|
$backupConfig = Get-RegistryBackupPayload -SelectedFeatures $selectedFeatures -UndoFeatures $undoFeatures -CreatedAt $timestamp
|
||||||
|
if (-not (SaveToFile -Config $backupConfig -FilePath $backupFilePath -MaxDepth 25)) {
|
||||||
|
throw "Failed to save registry backup to '$backupFilePath'"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Backup successfully created: $backupFilePath"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
return $backupFilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SelectedFeatures {
|
||||||
|
param(
|
||||||
|
[string[]]$ActionableKeys
|
||||||
|
)
|
||||||
|
|
||||||
|
$selectedFeatures = New-Object System.Collections.Generic.List[object]
|
||||||
|
$selectedFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
|
||||||
|
foreach ($paramKey in $ActionableKeys) {
|
||||||
|
if (-not $script:Features.ContainsKey($paramKey)) { continue }
|
||||||
|
|
||||||
|
$feature = $script:Features[$paramKey]
|
||||||
|
if (-not $feature) { continue }
|
||||||
|
|
||||||
|
$featureId = Get-FeatureId -Feature $feature
|
||||||
|
|
||||||
|
if ($selectedFeatureIds.Add($featureId)) {
|
||||||
|
$selectedFeatures.Add($feature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return @($selectedFeatures.ToArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RegistryBackupPayload {
|
||||||
|
param(
|
||||||
|
[object[]]$SelectedFeatures = @(),
|
||||||
|
[object[]]$UndoFeatures = @(),
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[datetime]$CreatedAt
|
||||||
|
)
|
||||||
|
|
||||||
|
$selectedFeatureIds = New-Object System.Collections.Generic.List[string]
|
||||||
|
$seenSelectedFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
foreach ($feature in $SelectedFeatures) {
|
||||||
|
$featureId = Get-FeatureId -Feature $feature
|
||||||
|
|
||||||
|
if ($seenSelectedFeatureIds.Add($featureId)) {
|
||||||
|
$selectedFeatureIds.Add($featureId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedUndoFeatureIds = New-Object System.Collections.Generic.List[string]
|
||||||
|
$seenUndoFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
foreach ($feature in $UndoFeatures) {
|
||||||
|
$featureId = Get-FeatureId -Feature $feature
|
||||||
|
|
||||||
|
if ($seenUndoFeatureIds.Add($featureId)) {
|
||||||
|
$selectedUndoFeatureIds.Add($featureId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedRegistryFeatures = @(Get-RegistryBackedFeatures -Features $SelectedFeatures)
|
||||||
|
$undoRegistryFeatures = @($UndoFeatures | Where-Object {
|
||||||
|
-not [string]::IsNullOrWhiteSpace([string]$_.RegistryUndoKey) -or -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey)
|
||||||
|
})
|
||||||
|
$capturePlans = @(Get-RegistryBackupCapturePlans -SelectedRegistryFeatures $selectedRegistryFeatures -UndoRegistryFeatures $undoRegistryFeatures)
|
||||||
|
$registryKeys = @(Get-RegistrySnapshotsForBackup -CapturePlans $capturePlans)
|
||||||
|
|
||||||
|
$backupPayload = @{
|
||||||
|
Version = '1.0'
|
||||||
|
BackupType = 'RegistryState'
|
||||||
|
CreatedAt = $CreatedAt.ToString('o')
|
||||||
|
CreatedBy = 'Win11Debloat'
|
||||||
|
Target = (Get-RegistryBackupTargetDescription)
|
||||||
|
ComputerName = $env:COMPUTERNAME
|
||||||
|
SelectedFeatures = @($selectedFeatureIds)
|
||||||
|
RegistryKeys = @($registryKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($selectedUndoFeatureIds.Count -gt 0) {
|
||||||
|
$backupPayload['SelectedUndoFeatures'] = @($selectedUndoFeatureIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $backupPayload
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ function CreateSystemRestorePoint {
|
|||||||
# In GUI mode, skip the prompt and just try to enable it
|
# In GUI mode, skip the prompt and just try to enable it
|
||||||
if ($script:GuiWindow -or $Silent -or $( Read-Host -Prompt "System restore is disabled, would you like to enable it and create a restore point? (y/n)") -eq 'y') {
|
if ($script:GuiWindow -or $Silent -or $( Read-Host -Prompt "System restore is disabled, would you like to enable it and create a restore point? (y/n)") -eq 'y') {
|
||||||
try {
|
try {
|
||||||
$enableResult = Invoke-NonBlocking -TimeoutSeconds 20 -ScriptBlock {
|
$enableResult = Invoke-NonBlocking -TimeoutSeconds 90 -ScriptBlock {
|
||||||
try {
|
try {
|
||||||
Enable-ComputerRestore -Drive "$env:SystemDrive"
|
Enable-ComputerRestore -Drive "$env:SystemDrive"
|
||||||
return $null
|
return $null
|
||||||
@@ -33,7 +33,7 @@ function CreateSystemRestorePoint {
|
|||||||
|
|
||||||
if (-not $failed) {
|
if (-not $failed) {
|
||||||
try {
|
try {
|
||||||
$result = Invoke-NonBlocking -TimeoutSeconds 20 -ScriptBlock {
|
$result = Invoke-NonBlocking -TimeoutSeconds 90 -ScriptBlock {
|
||||||
try {
|
try {
|
||||||
$recentRestorePoints = Get-ComputerRestorePoint | Where-Object { (Get-Date) - [System.Management.ManagementDateTimeConverter]::ToDateTime($_.CreationTime) -le (New-TimeSpan -Hours 24) }
|
$recentRestorePoints = Get-ComputerRestorePoint | Where-Object { (Get-Date) - [System.Management.ManagementDateTimeConverter]::ToDateTime($_.CreationTime) -le (New-TimeSpan -Hours 24) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
function DisableStoreSearchSuggestionsForAllUsers {
|
function DisableStoreSearchSuggestionsForAllUsers {
|
||||||
# Get path to Store app database for all users
|
# Get path to Store app database for all users
|
||||||
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages"
|
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages"
|
||||||
$usersStoreDbPaths = get-childitem -path $userPathString
|
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
# Go through all users and disable start search suggestions
|
# Go through all users and disable start search suggestions
|
||||||
ForEach ($storeDbPath in $usersStoreDbPaths) {
|
ForEach ($storeDbPath in $usersStoreDbPaths) {
|
||||||
@@ -50,3 +50,129 @@ function DisableStoreSearchSuggestions {
|
|||||||
|
|
||||||
Write-Host "Disabled Microsoft Store search suggestions for user $userName"
|
Write-Host "Disabled Microsoft Store search suggestions for user $userName"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function EnableStoreSearchSuggestionsForAllUsers {
|
||||||
|
# Get path to Store app database for all users
|
||||||
|
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages"
|
||||||
|
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
# Go through all users and re-enable start search suggestions
|
||||||
|
ForEach ($storeDbPath in $usersStoreDbPaths) {
|
||||||
|
EnableStoreSearchSuggestions ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Also re-enable for the default user profile
|
||||||
|
$defaultStoreDbPath = GetUserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false
|
||||||
|
EnableStoreSearchSuggestions $defaultStoreDbPath
|
||||||
|
}
|
||||||
|
|
||||||
|
function EnableStoreSearchSuggestions {
|
||||||
|
param (
|
||||||
|
$StoreAppsDatabase = "$env:LocalAppData\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Change path to correct user if a user was specified
|
||||||
|
if ($script:Params.ContainsKey("User")) {
|
||||||
|
$StoreAppsDatabase = GetUserDirectory -userName "$(GetUserName)" -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$userName = [regex]::Match($StoreAppsDatabase, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value
|
||||||
|
if (-not $userName) { $userName = '<unknown>' }
|
||||||
|
|
||||||
|
if (-not (Test-Path -Path $StoreAppsDatabase)) {
|
||||||
|
Write-Host "Store app database not found for user $userName, nothing to undo"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure we can modify/delete the file even if restrictive ACLs were set.
|
||||||
|
$global:LASTEXITCODE = 0
|
||||||
|
takeown /F "$StoreAppsDatabase" /A | Out-Null
|
||||||
|
icacls "$StoreAppsDatabase" /grant *S-1-5-32-544:F /C | Out-Null
|
||||||
|
|
||||||
|
$everyoneSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0') # 'EVERYONE' group
|
||||||
|
|
||||||
|
try {
|
||||||
|
$acl = Get-Acl -Path $StoreAppsDatabase
|
||||||
|
$denyRules = @(
|
||||||
|
$acl.Access | Where-Object {
|
||||||
|
$_.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Deny -and
|
||||||
|
(($_.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne 0) -and
|
||||||
|
(try { $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $everyoneSid } catch { $false })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($denyRule in $denyRules) {
|
||||||
|
$null = $acl.RemoveAccessRuleSpecific($denyRule)
|
||||||
|
}
|
||||||
|
|
||||||
|
Set-Acl -Path $StoreAppsDatabase -AclObject $acl | Out-Null
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Failed to normalize ACL for store database '$StoreAppsDatabase': $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Remove-Item -Path $StoreAppsDatabase -Force -ErrorAction Stop
|
||||||
|
Write-Host "Re-enabled Microsoft Store search suggestions for user $userName"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
throw "Failed to remove '$StoreAppsDatabase' while undoing Microsoft Store search suggestions for user $userName. $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-StoreSearchSuggestionsDisabled {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$StoreAppsDatabase
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-Path -Path $StoreAppsDatabase)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$acl = Get-Acl -Path $StoreAppsDatabase
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$everyoneSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0')
|
||||||
|
|
||||||
|
foreach ($accessRule in @($acl.Access)) {
|
||||||
|
if ($accessRule.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Deny -and
|
||||||
|
(($accessRule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne 0) -and
|
||||||
|
(try { $accessRule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $everyoneSid } catch { $false })) {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-StoreSearchSuggestionsDisabledForAllUsers {
|
||||||
|
$paths = @()
|
||||||
|
|
||||||
|
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages"
|
||||||
|
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
|
||||||
|
foreach ($storeDbPath in $usersStoreDbPaths) {
|
||||||
|
$paths += ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaultStoreDbPath = GetUserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false
|
||||||
|
if ($defaultStoreDbPath) {
|
||||||
|
$paths += $defaultStoreDbPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($paths.Count -eq 0) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($path in $paths) {
|
||||||
|
if (-not (Test-StoreSearchSuggestionsDisabled -StoreAppsDatabase $path)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $true
|
||||||
|
}
|
||||||
@@ -14,3 +14,36 @@ function EnableWindowsFeature {
|
|||||||
Write-Host ($dismResult | Out-String).Trim()
|
Write-Host ($dismResult | Out-String).Trim()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Disables a Windows optional feature and pipes its output to the console
|
||||||
|
function DisableWindowsFeature {
|
||||||
|
param (
|
||||||
|
[string]$FeatureName
|
||||||
|
)
|
||||||
|
|
||||||
|
$result = Invoke-NonBlocking -ScriptBlock {
|
||||||
|
param($name)
|
||||||
|
Disable-WindowsOptionalFeature -Online -FeatureName $name -NoRestart
|
||||||
|
} -ArgumentList $FeatureName
|
||||||
|
|
||||||
|
$dismResult = @($result) | Where-Object { $_ -is [Microsoft.Dism.Commands.ImageObject] }
|
||||||
|
if ($dismResult) {
|
||||||
|
Write-Host ($dismResult | Out-String).Trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-WindowsOptionalFeatureEnabled {
|
||||||
|
param (
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$FeatureName
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
$feature = Get-WindowsOptionalFeature -Online -FeatureName $FeatureName -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($feature.State -eq 'Enabled')
|
||||||
|
}
|
||||||
@@ -1,3 +1,55 @@
|
|||||||
|
# Resolves the path of an undo reg file relative to $script:RegfilesPath.
|
||||||
|
# Checks the Undo/ subfolder first, then falls back to the root Regfiles/ folder.
|
||||||
|
function Resolve-UndoRegFilePath {
|
||||||
|
param ([string]$FileName)
|
||||||
|
$undoSubPath = Join-Path 'Undo' $FileName
|
||||||
|
if (Test-Path (Join-Path $script:RegfilesPath $undoSubPath)) {
|
||||||
|
return $undoSubPath
|
||||||
|
}
|
||||||
|
return $FileName
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-UndoFeatureAction {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$FeatureId
|
||||||
|
)
|
||||||
|
|
||||||
|
switch ($FeatureId) {
|
||||||
|
'DisableStoreSearchSuggestions' {
|
||||||
|
if ($script:Params.ContainsKey('Sysprep')) {
|
||||||
|
Write-Host "> Re-enabling Microsoft Store search suggestions in the start menu for all users..."
|
||||||
|
EnableStoreSearchSuggestionsForAllUsers
|
||||||
|
Write-Host ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "> Re-enabling Microsoft Store search suggestions for user $(GetUserName)..."
|
||||||
|
EnableStoreSearchSuggestions
|
||||||
|
Write-Host ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
'EnableWindowsSandbox' {
|
||||||
|
Write-Host "> Disabling Windows Sandbox..."
|
||||||
|
DisableWindowsFeature 'Containers-DisposableClientVM'
|
||||||
|
Write-Host ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
'EnableWindowsSubsystemForLinux' {
|
||||||
|
Write-Host "> Disabling Windows Subsystem for Linux..."
|
||||||
|
DisableWindowsFeature 'Microsoft-Windows-Subsystem-Linux'
|
||||||
|
DisableWindowsFeature 'VirtualMachinePlatform'
|
||||||
|
Write-Host ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
Write-Host "> No undo action defined for $FeatureId, skipping..." -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Executes a single parameter/feature based on its key
|
# Executes a single parameter/feature based on its key
|
||||||
# Parameters:
|
# Parameters:
|
||||||
# $paramKey - The parameter name to execute
|
# $paramKey - The parameter name to execute
|
||||||
@@ -26,10 +78,6 @@ function ExecuteParameter {
|
|||||||
# Also remove the app package for Copilot
|
# Also remove the app package for Copilot
|
||||||
RemoveApps 'Microsoft.Copilot'
|
RemoveApps 'Microsoft.Copilot'
|
||||||
}
|
}
|
||||||
'DisableWidgets' {
|
|
||||||
# Also remove the app packages for Widgets
|
|
||||||
RemoveApps 'Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -86,6 +134,13 @@ function ExecuteParameter {
|
|||||||
RemoveApps $appsList
|
RemoveApps $appsList
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
'DisableWidgets' {
|
||||||
|
Write-Host "> Disabling widgets on the taskbar & lock screen..."
|
||||||
|
# Stop widgets related processes before removing the app packages to prevent potential issues
|
||||||
|
Get-Process *Widget* | Stop-Process
|
||||||
|
|
||||||
|
RemoveApps 'Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime'
|
||||||
|
}
|
||||||
"EnableWindowsSandbox" {
|
"EnableWindowsSandbox" {
|
||||||
Write-Host "> Enabling Windows Sandbox..."
|
Write-Host "> Enabling Windows Sandbox..."
|
||||||
EnableWindowsFeature "Containers-DisposableClientVM"
|
EnableWindowsFeature "Containers-DisposableClientVM"
|
||||||
@@ -138,6 +193,8 @@ function ExecuteParameter {
|
|||||||
|
|
||||||
# Executes all selected parameters/features
|
# Executes all selected parameters/features
|
||||||
function ExecuteAllChanges {
|
function ExecuteAllChanges {
|
||||||
|
$script:RegistryImportFailures = 0
|
||||||
|
|
||||||
# Build list of actionable parameters (skip control params and data-only params)
|
# Build list of actionable parameters (skip control params and data-only params)
|
||||||
$actionableKeys = @()
|
$actionableKeys = @()
|
||||||
foreach ($paramKey in $script:Params.Keys) {
|
foreach ($paramKey in $script:Params.Keys) {
|
||||||
@@ -147,17 +204,57 @@ function ExecuteAllChanges {
|
|||||||
$actionableKeys += $paramKey
|
$actionableKeys += $paramKey
|
||||||
}
|
}
|
||||||
|
|
||||||
$totalSteps = $actionableKeys.Count
|
$hasRegistryBackedFeature = $false
|
||||||
|
foreach ($paramKey in $actionableKeys) {
|
||||||
|
if (-not $script:Features.ContainsKey($paramKey)) { continue }
|
||||||
|
|
||||||
|
$feature = $script:Features[$paramKey]
|
||||||
|
if ($feature -and -not [string]::IsNullOrWhiteSpace([string]$feature.RegistryKey)) {
|
||||||
|
$hasRegistryBackedFeature = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Undo operations that write registry values also require a backup
|
||||||
|
if (-not $hasRegistryBackedFeature) {
|
||||||
|
foreach ($featureId in $script:UndoParams.Keys) {
|
||||||
|
$f = if ($script:Features.ContainsKey($featureId)) { $script:Features[$featureId] } else { $null }
|
||||||
|
if ($f -and $f.RegistryUndoKey) { $hasRegistryBackedFeature = $true; break }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalSteps = $actionableKeys.Count + $script:UndoParams.Count
|
||||||
|
if ($hasRegistryBackedFeature) { $totalSteps++ }
|
||||||
if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ }
|
if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ }
|
||||||
$currentStep = 0
|
$currentStep = 0
|
||||||
|
|
||||||
|
if ($hasRegistryBackedFeature) {
|
||||||
|
$currentStep++
|
||||||
|
if ($script:ApplyProgressCallback) {
|
||||||
|
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating registry backup..."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "> Creating registry backup..."
|
||||||
|
try {
|
||||||
|
$undoSyntheticFeatures = @($script:UndoParams.Keys | ForEach-Object {
|
||||||
|
$f = if ($script:Features.ContainsKey($_)) { $script:Features[$_] } else { $null }
|
||||||
|
if ($f -and $f.RegistryUndoKey) {
|
||||||
|
[PSCustomObject]@{ FeatureId = $_; RegistryKey = (Resolve-UndoRegFilePath $f.RegistryUndoKey) }
|
||||||
|
}
|
||||||
|
} | Where-Object { $_ })
|
||||||
|
New-RegistrySettingsBackup -ActionableKeys $actionableKeys -ExtraFeatures $undoSyntheticFeatures | Out-Null
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
throw "Registry backup failed before applying changes. $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Create restore point if requested (CLI only - GUI handles this separately)
|
# Create restore point if requested (CLI only - GUI handles this separately)
|
||||||
if ($script:Params.ContainsKey("CreateRestorePoint")) {
|
if ($script:Params.ContainsKey("CreateRestorePoint")) {
|
||||||
$currentStep++
|
$currentStep++
|
||||||
if ($script:ApplyProgressCallback) {
|
if ($script:ApplyProgressCallback) {
|
||||||
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating system restore point"
|
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating system restore point, this may take a moment..."
|
||||||
}
|
}
|
||||||
Write-Host "> Attempting to create a system restore point..."
|
Write-Host "> Creating a system restore point..."
|
||||||
CreateSystemRestorePoint
|
CreateSystemRestorePoint
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
}
|
}
|
||||||
@@ -178,14 +275,10 @@ function ExecuteAllChanges {
|
|||||||
# Prefer explicit ApplyText when provided
|
# Prefer explicit ApplyText when provided
|
||||||
$stepName = $feature.ApplyText
|
$stepName = $feature.ApplyText
|
||||||
} elseif ($feature.Label) {
|
} elseif ($feature.Label) {
|
||||||
# Fallback: construct a name from Action and Label, or just Label
|
# Fallback: use label from Features.json
|
||||||
if ($feature.Action) {
|
|
||||||
$stepName = "$($feature.Action) $($feature.Label)"
|
|
||||||
} else {
|
|
||||||
$stepName = $feature.Label
|
$stepName = $feature.Label
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ($script:ApplyProgressCallback) {
|
if ($script:ApplyProgressCallback) {
|
||||||
& $script:ApplyProgressCallback $currentStep $totalSteps $stepName
|
& $script:ApplyProgressCallback $currentStep $totalSteps $stepName
|
||||||
@@ -193,4 +286,29 @@ function ExecuteAllChanges {
|
|||||||
|
|
||||||
ExecuteParameter -paramKey $paramKey
|
ExecuteParameter -paramKey $paramKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Execute all undo operations
|
||||||
|
foreach ($featureId in $script:UndoParams.Keys) {
|
||||||
|
if ($script:CancelRequested) { return }
|
||||||
|
|
||||||
|
$undoLabel = if ($script:FeatureLabelLookup) { $script:FeatureLabelLookup[$featureId] } else { $null }
|
||||||
|
if (-not $undoLabel) { $undoLabel = $featureId }
|
||||||
|
|
||||||
|
$currentStep++
|
||||||
|
if ($script:ApplyProgressCallback) {
|
||||||
|
& $script:ApplyProgressCallback $currentStep $totalSteps "Undoing: $undoLabel"
|
||||||
|
}
|
||||||
|
|
||||||
|
$f = if ($script:Features.ContainsKey($featureId)) { $script:Features[$featureId] } else { $null }
|
||||||
|
if ($f -and $f.RegistryUndoKey) {
|
||||||
|
ImportRegistryFile "> Undoing: $undoLabel" (Resolve-UndoRegFilePath $f.RegistryUndoKey)
|
||||||
|
} else {
|
||||||
|
Invoke-UndoFeatureAction -FeatureId $featureId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($script:RegistryImportFailures -gt 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "$($script:RegistryImportFailures) registry import change(s) failed. See output above for details." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
}
|
}
|
||||||
175
Scripts/Features/GetCurrentTweakState.ps1
Normal file
175
Scripts/Features/GetCurrentTweakState.ps1
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# Tests whether the registry operations in a feature's .reg file currently match the live registry.
|
||||||
|
# Returns $true if ALL operations in the apply reg file match current system state.
|
||||||
|
# Returns $false if the feature has no RegistryKey, the file is missing, or any operation mismatches.
|
||||||
|
function Get-ExpectedRegistryValueKind {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Operation
|
||||||
|
)
|
||||||
|
|
||||||
|
switch ([string]$Operation.ValueType) {
|
||||||
|
'DWord' { return [Microsoft.Win32.RegistryValueKind]::DWord }
|
||||||
|
'QWord' { return [Microsoft.Win32.RegistryValueKind]::QWord }
|
||||||
|
'String' { return [Microsoft.Win32.RegistryValueKind]::String }
|
||||||
|
'Binary' { return [Microsoft.Win32.RegistryValueKind]::Binary }
|
||||||
|
'Hex2' { return [Microsoft.Win32.RegistryValueKind]::ExpandString }
|
||||||
|
'Hex7' { return [Microsoft.Win32.RegistryValueKind]::MultiString }
|
||||||
|
default { return $null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-FeatureApplied {
|
||||||
|
param (
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$FeatureId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $script:Features.ContainsKey($FeatureId)) { return $false }
|
||||||
|
$feature = $script:Features[$FeatureId]
|
||||||
|
|
||||||
|
switch ($FeatureId) {
|
||||||
|
'DisableWidgets' {
|
||||||
|
# Widgets packages cannot be reinstalled automatically, so we treat their
|
||||||
|
# absence as the applied state (checked) and presence as not-yet-applied.
|
||||||
|
$widgetAppIds = @(
|
||||||
|
'Microsoft.StartExperiencesApp',
|
||||||
|
'MicrosoftWindows.Client.WebExperience',
|
||||||
|
'Microsoft.WidgetsPlatformRuntime'
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($appId in $widgetAppIds) {
|
||||||
|
if (Get-AppxPackage -Name $appId -AllUsers -ErrorAction SilentlyContinue) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
'DisableStoreSearchSuggestions' {
|
||||||
|
if ($script:Params.ContainsKey('Sysprep')) {
|
||||||
|
return (Test-StoreSearchSuggestionsDisabledForAllUsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
$storeDbPath = "$env:LocalAppData\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
|
||||||
|
if ($script:Params.ContainsKey('User')) {
|
||||||
|
$storeDbPath = GetUserDirectory -userName "$(GetUserName)" -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Test-StoreSearchSuggestionsDisabled -StoreAppsDatabase $storeDbPath)
|
||||||
|
}
|
||||||
|
'EnableWindowsSandbox' {
|
||||||
|
return (Test-WindowsOptionalFeatureEnabled -FeatureName 'Containers-DisposableClientVM')
|
||||||
|
}
|
||||||
|
'EnableWindowsSubsystemForLinux' {
|
||||||
|
$wslEnabled = Test-WindowsOptionalFeatureEnabled -FeatureName 'Microsoft-Windows-Subsystem-Linux'
|
||||||
|
$vmpEnabled = Test-WindowsOptionalFeatureEnabled -FeatureName 'VirtualMachinePlatform'
|
||||||
|
return ($wslEnabled -and $vmpEnabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $feature.RegistryKey) { return $false }
|
||||||
|
|
||||||
|
$regFilePath = Join-Path $script:RegfilesPath $feature.RegistryKey
|
||||||
|
if (-not (Test-Path $regFilePath)) { return $false }
|
||||||
|
|
||||||
|
try {
|
||||||
|
$operations = @(Get-RegFileOperations -regFilePath $regFilePath)
|
||||||
|
}
|
||||||
|
catch { return $false }
|
||||||
|
|
||||||
|
if ($operations.Count -eq 0) { return $false }
|
||||||
|
|
||||||
|
foreach ($op in $operations) {
|
||||||
|
$parts = Split-RegistryPath -path $op.KeyPath
|
||||||
|
if (-not $parts) { return $false }
|
||||||
|
|
||||||
|
$rootKey = Get-RegistryRootKey -hiveName $parts.Hive
|
||||||
|
if (-not $rootKey) { return $false }
|
||||||
|
|
||||||
|
$key = $null
|
||||||
|
try {
|
||||||
|
$key = $rootKey.OpenSubKey($parts.SubKey, $false)
|
||||||
|
|
||||||
|
switch ($op.OperationType) {
|
||||||
|
'DeleteKey' {
|
||||||
|
if ($null -ne $key) { return $false }
|
||||||
|
}
|
||||||
|
'DeleteValue' {
|
||||||
|
if ($null -ne $key) {
|
||||||
|
$names = @($key.GetValueNames())
|
||||||
|
if ($names -icontains $op.ValueName) { return $false }
|
||||||
|
}
|
||||||
|
# key missing = value also gone = operation matches
|
||||||
|
}
|
||||||
|
'SetValue' {
|
||||||
|
if ($null -eq $key) { return $false }
|
||||||
|
$names = @($key.GetValueNames())
|
||||||
|
if (-not ($names -icontains $op.ValueName)) { return $false }
|
||||||
|
|
||||||
|
$actualKind = $key.GetValueKind($op.ValueName)
|
||||||
|
$expectedKind = Get-ExpectedRegistryValueKind -Operation $op
|
||||||
|
if ($null -eq $expectedKind -or $actualKind -ne $expectedKind) { return $false }
|
||||||
|
$actualRaw = $key.GetValue($op.ValueName, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
|
||||||
|
|
||||||
|
$actual = switch ($actualKind) {
|
||||||
|
([Microsoft.Win32.RegistryValueKind]::DWord) {
|
||||||
|
[BitConverter]::ToUInt32([BitConverter]::GetBytes([int32]$actualRaw), 0)
|
||||||
|
}
|
||||||
|
([Microsoft.Win32.RegistryValueKind]::QWord) {
|
||||||
|
[BitConverter]::ToUInt64([BitConverter]::GetBytes([int64]$actualRaw), 0)
|
||||||
|
}
|
||||||
|
([Microsoft.Win32.RegistryValueKind]::Binary) {
|
||||||
|
@($actualRaw | ForEach-Object { [int]$_ })
|
||||||
|
}
|
||||||
|
([Microsoft.Win32.RegistryValueKind]::MultiString) {
|
||||||
|
@($actualRaw)
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
if ($null -ne $actualRaw) { [string]$actualRaw } else { $null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$expected = $op.ValueData
|
||||||
|
|
||||||
|
$match = if (($actual -is [array]) -and ($expected -is [array])) {
|
||||||
|
(Compare-Object $actual $expected).Count -eq 0
|
||||||
|
} else {
|
||||||
|
$actual -eq $expected
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $match) { return $false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { return $false }
|
||||||
|
finally {
|
||||||
|
if ($null -ne $key) { $key.Close() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns the 1-based index of the UiGroup option whose features all match current system state,
|
||||||
|
# or 0 if no option fully matches (meaning the current state is unknown / "No Change").
|
||||||
|
function Get-CurrentGroupActiveIndex {
|
||||||
|
param (
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[object]$Group
|
||||||
|
)
|
||||||
|
|
||||||
|
$i = 1
|
||||||
|
foreach ($val in $Group.Values) {
|
||||||
|
$allApplied = $true
|
||||||
|
foreach ($fid in $val.FeatureIds) {
|
||||||
|
if (-not (Test-FeatureApplied -FeatureId $fid)) {
|
||||||
|
$allApplied = $false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($allApplied) { return $i }
|
||||||
|
$i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -8,33 +8,38 @@ function ImportRegistryFile {
|
|||||||
Write-Host $message
|
Write-Host $message
|
||||||
|
|
||||||
$usesOfflineHive = $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User")
|
$usesOfflineHive = $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User")
|
||||||
$regFilePath = if ($usesOfflineHive) {
|
$regFilePath = Get-RegistryFilePathForFeature -RegistryKey $path
|
||||||
"$script:RegfilesPath\Sysprep\$path"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
"$script:RegfilesPath\$path"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $regFilePath)) {
|
if (-not (Test-Path $regFilePath)) {
|
||||||
$errorMessage = "Unable to find registry file: $path ($regFilePath)"
|
$errorMessage = "Unable to find registry file: $path ($regFilePath)"
|
||||||
|
$script:RegistryImportFailures++
|
||||||
Write-Host "Error: $errorMessage" -ForegroundColor Red
|
Write-Host "Error: $errorMessage" -ForegroundColor Red
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
throw $errorMessage
|
throw $errorMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
# Reset exit code before running reg.exe for reliable success detection
|
$regResult = $null
|
||||||
$global:LASTEXITCODE = 0
|
$offlineHiveLoaded = $false
|
||||||
|
|
||||||
|
try {
|
||||||
if ($usesOfflineHive) {
|
if ($usesOfflineHive) {
|
||||||
# Sysprep targets Default user, User targets the specified user
|
# Sysprep targets Default user, User targets the specified user
|
||||||
$hiveDatPath = if ($script:Params.ContainsKey("Sysprep")) {
|
$targetUserName = if ($script:Params.ContainsKey("Sysprep")) { "Default" } else { $script:Params.Item("User") }
|
||||||
GetUserDirectory -userName "Default" -fileName "NTUSER.DAT"
|
$hiveDatPath = GetUserDirectory -userName $targetUserName -fileName "NTUSER.DAT"
|
||||||
} else {
|
|
||||||
GetUserDirectory -userName $script:Params.Item("User") -fileName "NTUSER.DAT"
|
$global:LASTEXITCODE = 0
|
||||||
|
reg load "HKU\Default" $hiveDatPath | Out-Null
|
||||||
|
$loadExitCode = $LASTEXITCODE
|
||||||
|
|
||||||
|
if ($loadExitCode -ne 0) {
|
||||||
|
throw "Failed importing registry file '$path'. Offline hive load failed: Failed to load user hive at '$hiveDatPath' (exit code: $loadExitCode)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$offlineHiveLoaded = $true
|
||||||
}
|
}
|
||||||
|
|
||||||
$regResult = Invoke-NonBlocking -ScriptBlock {
|
$regResult = Invoke-NonBlocking -ScriptBlock {
|
||||||
param($hivePath, $targetRegFilePath)
|
param($targetRegFilePath)
|
||||||
$result = @{
|
$result = @{
|
||||||
Output = @()
|
Output = @()
|
||||||
ExitCode = 0
|
ExitCode = 0
|
||||||
@@ -43,13 +48,6 @@ function ImportRegistryFile {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$global:LASTEXITCODE = 0
|
$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
|
$output = reg import $targetRegFilePath 2>&1
|
||||||
$importExitCode = $LASTEXITCODE
|
$importExitCode = $LASTEXITCODE
|
||||||
|
|
||||||
@@ -66,27 +64,9 @@ function ImportRegistryFile {
|
|||||||
$result.Error = $_.Exception.Message
|
$result.Error = $_.Exception.Message
|
||||||
$result.ExitCode = if ($LASTEXITCODE -ne 0) { $LASTEXITCODE } else { 1 }
|
$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
|
return $result
|
||||||
} -ArgumentList @($hiveDatPath, $regFilePath)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$regResult = Invoke-NonBlocking -ScriptBlock {
|
|
||||||
param($targetRegFilePath)
|
|
||||||
$global:LASTEXITCODE = 0
|
|
||||||
$output = reg import $targetRegFilePath 2>&1
|
|
||||||
return @{ Output = @($output); ExitCode = $LASTEXITCODE; Error = $null }
|
|
||||||
} -ArgumentList $regFilePath
|
} -ArgumentList $regFilePath
|
||||||
}
|
|
||||||
|
|
||||||
$regOutput = @($regResult.Output)
|
$regOutput = @($regResult.Output)
|
||||||
$hasSuccess = ($regResult.ExitCode -eq 0) -and -not $regResult.Error
|
$hasSuccess = ($regResult.ExitCode -eq 0) -and -not $regResult.Error
|
||||||
@@ -107,11 +87,27 @@ function ImportRegistryFile {
|
|||||||
|
|
||||||
if (-not $hasSuccess) {
|
if (-not $hasSuccess) {
|
||||||
$details = if ($regResult.Error) { $regResult.Error } else { "Exit code: $($regResult.ExitCode)" }
|
$details = if ($regResult.Error) { $regResult.Error } else { "Exit code: $($regResult.ExitCode)" }
|
||||||
$errorMessage = "Failed importing registry file '$path'. $details"
|
Write-Warning "reg import failed for '$path'. Falling back to PowerShell registry writer. Details: $details"
|
||||||
Write-Host $errorMessage -ForegroundColor Red
|
Invoke-RegistryOperationsFromRegFile -RegFilePath $regFilePath
|
||||||
Write-Host ""
|
Write-Host "Fallback import succeeded for '$path'." -ForegroundColor Yellow
|
||||||
throw $errorMessage
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$script:RegistryImportFailures++
|
||||||
|
Write-Host $_.Exception.Message -ForegroundColor Red
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if ($offlineHiveLoaded) {
|
||||||
|
$global:LASTEXITCODE = 0
|
||||||
|
reg unload "HKU\Default" | Out-Null
|
||||||
|
$unloadExitCode = $LASTEXITCODE
|
||||||
|
|
||||||
|
if ($unloadExitCode -ne 0) {
|
||||||
|
Write-Warning "Failed to unload registry hive HKU\Default after importing '$path' (exit code: $unloadExitCode)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
447
Scripts/Features/RegistryBackupValidation.ps1
Normal file
447
Scripts/Features/RegistryBackupValidation.ps1
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
function Get-NormalizedSelectedFeatureIdsFromBackup {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Backup
|
||||||
|
)
|
||||||
|
|
||||||
|
$selectedFeatures = New-Object System.Collections.Generic.List[string]
|
||||||
|
$selectedFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
$errors = New-Object System.Collections.Generic.List[string]
|
||||||
|
$hasInvalidSelectedFeatureId = $false
|
||||||
|
|
||||||
|
if (-not $Backup.PSObject.Properties['SelectedFeatures']) {
|
||||||
|
$errors.Add('Missing property: SelectedFeatures')
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
SelectedFeatures = $selectedFeatures.ToArray()
|
||||||
|
Errors = $errors.ToArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($featureId in @($Backup.SelectedFeatures)) {
|
||||||
|
if ($featureId -isnot [string] -or [string]::IsNullOrWhiteSpace([string]$featureId)) {
|
||||||
|
$hasInvalidSelectedFeatureId = $true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedFeatureId = [string]$featureId
|
||||||
|
if ($selectedFeatureIds.Add($normalizedFeatureId)) {
|
||||||
|
$selectedFeatures.Add($normalizedFeatureId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasInvalidSelectedFeatureId) {
|
||||||
|
$errors.Add('SelectedFeatures must contain non-empty string feature IDs.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
SelectedFeatures = $selectedFeatures.ToArray()
|
||||||
|
Errors = $errors.ToArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-NormalizedSelectedUndoFeatureIdsFromBackup {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Backup
|
||||||
|
)
|
||||||
|
|
||||||
|
$selectedUndoFeatures = New-Object System.Collections.Generic.List[string]
|
||||||
|
$selectedUndoFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
$errors = New-Object System.Collections.Generic.List[string]
|
||||||
|
|
||||||
|
# SelectedUndoFeatures is optional - only process if present
|
||||||
|
if (-not $Backup.PSObject.Properties['SelectedUndoFeatures']) {
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
SelectedUndoFeatures = $selectedUndoFeatures.ToArray()
|
||||||
|
Errors = $errors.ToArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasInvalidSelectedUndoFeatureId = $false
|
||||||
|
foreach ($featureId in @($Backup.SelectedUndoFeatures)) {
|
||||||
|
if ($featureId -isnot [string] -or [string]::IsNullOrWhiteSpace([string]$featureId)) {
|
||||||
|
$hasInvalidSelectedUndoFeatureId = $true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedFeatureId = [string]$featureId
|
||||||
|
if ($selectedUndoFeatureIds.Add($normalizedFeatureId)) {
|
||||||
|
$selectedUndoFeatures.Add($normalizedFeatureId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasInvalidSelectedUndoFeatureId) {
|
||||||
|
$errors.Add('SelectedUndoFeatures must contain non-empty string feature IDs.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
SelectedUndoFeatures = $selectedUndoFeatures.ToArray()
|
||||||
|
Errors = $errors.ToArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Normalize-RegistryKeySnapshot {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Snapshot
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $Snapshot.PSObject.Properties['Path'] -or [string]::IsNullOrWhiteSpace([string]$Snapshot.Path)) {
|
||||||
|
throw 'Backup validation failed: Registry key snapshot is missing Path.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$exists = $false
|
||||||
|
if ($Snapshot.PSObject.Properties['Exists']) {
|
||||||
|
$exists = [bool]$Snapshot.Exists
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = @()
|
||||||
|
if ($Snapshot.PSObject.Properties['Values']) {
|
||||||
|
foreach ($valueSnapshot in @($Snapshot.Values)) {
|
||||||
|
$valueExists = $true
|
||||||
|
if ($valueSnapshot.PSObject.Properties['Exists']) {
|
||||||
|
$valueExists = [bool]$valueSnapshot.Exists
|
||||||
|
}
|
||||||
|
|
||||||
|
$values += [PSCustomObject]@{
|
||||||
|
Name = [string]$valueSnapshot.Name
|
||||||
|
Exists = $valueExists
|
||||||
|
Kind = if ($valueSnapshot.PSObject.Properties['Kind']) { [string]$valueSnapshot.Kind } else { $null }
|
||||||
|
Data = if ($valueSnapshot.PSObject.Properties['Data']) { $valueSnapshot.Data } else { $null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$subKeys = @()
|
||||||
|
if ($Snapshot.PSObject.Properties['SubKeys']) {
|
||||||
|
foreach ($subKeySnapshot in @($Snapshot.SubKeys)) {
|
||||||
|
$subKeys += @(Normalize-RegistryKeySnapshot -Snapshot $subKeySnapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Path = [string]$Snapshot.Path
|
||||||
|
Exists = $exists
|
||||||
|
Values = @($values)
|
||||||
|
SubKeys = @($subKeys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-RegistryBackupMatchesSelectedFeatures {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[AllowEmptyCollection()]
|
||||||
|
[string[]]$SelectedFeatureIds,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[AllowEmptyCollection()]
|
||||||
|
[string[]]$SelectedUndoFeatureIds,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Target,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[AllowEmptyCollection()]
|
||||||
|
[object[]]$RegistryKeys
|
||||||
|
)
|
||||||
|
|
||||||
|
$errors = New-Object System.Collections.Generic.List[string]
|
||||||
|
|
||||||
|
if (-not $script:Features -or $script:Features.Count -eq 0) {
|
||||||
|
$errors.Add('Unable to validate registry backup allowlist because feature definitions are not loaded.')
|
||||||
|
return $errors.ToArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedRegistryFeatures = @(Get-SelectedRegistryFeaturesForBackupValidation -SelectedFeatureIds @($SelectedFeatureIds) -IsUndoFeature:$false -Errors $errors)
|
||||||
|
$undoRegistryFeatures = @(Get-SelectedRegistryFeaturesForBackupValidation -SelectedFeatureIds @($SelectedUndoFeatureIds) -IsUndoFeature:$true -Errors $errors)
|
||||||
|
$useSysprepRegFiles = ($Target -eq 'DefaultUserProfile') -or ($Target -like 'User:*')
|
||||||
|
|
||||||
|
$capturePlans = @()
|
||||||
|
if ($errors.Count -eq 0 -and ($selectedRegistryFeatures.Count -gt 0 -or $undoRegistryFeatures.Count -gt 0)) {
|
||||||
|
$capturePlans = @(Get-RegistryBackupCapturePlans -SelectedRegistryFeatures @($selectedRegistryFeatures) -UndoRegistryFeatures @($undoRegistryFeatures) -UseSysprepRegFiles:$useSysprepRegFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
$planMap = New-RegistryBackupAllowListPlanMap -CapturePlans @($capturePlans)
|
||||||
|
|
||||||
|
if ($planMap.Count -eq 0 -and @($RegistryKeys).Count -gt 0) {
|
||||||
|
$errors.Add('Backup contains registry snapshots but no allowed registry paths were derived from the selected features.')
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rootSnapshot in @($RegistryKeys)) {
|
||||||
|
Test-RegistrySnapshotAgainstAllowList -Snapshot $rootSnapshot -PlanMap $planMap -Errors $errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors.ToArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SelectedRegistryFeaturesForBackupValidation {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[AllowEmptyCollection()]
|
||||||
|
[string[]]$SelectedFeatureIds,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[bool]$IsUndoFeature,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[AllowEmptyCollection()]
|
||||||
|
$Errors
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $Errors -or -not ($Errors -is [System.Collections.IList])) {
|
||||||
|
throw 'Get-SelectedRegistryFeaturesForBackupValidation requires Errors to be a mutable list collection.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedRegistryFeatures = New-Object System.Collections.Generic.List[object]
|
||||||
|
foreach ($featureId in @($SelectedFeatureIds)) {
|
||||||
|
if (-not $script:Features.ContainsKey($featureId)) {
|
||||||
|
$Errors.Add("Selected feature '$featureId' was not found in the current feature catalog.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$feature = $script:Features[$featureId]
|
||||||
|
if (-not $feature) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# For undo features, check RegistryUndoKey if present (real features)
|
||||||
|
# Otherwise check RegistryKey (for synthetic features from backup capture)
|
||||||
|
$registryKeyToUse = if ($IsUndoFeature) {
|
||||||
|
$key = [string]$feature.RegistryUndoKey
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($key)) {
|
||||||
|
$key
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[string]$feature.RegistryKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[string]$feature.RegistryKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($registryKeyToUse)) {
|
||||||
|
$selectedRegistryFeatures.Add($feature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $selectedRegistryFeatures.ToArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-RegistryBackupAllowListPlanMap {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[AllowEmptyCollection()]
|
||||||
|
[object[]]$CapturePlans
|
||||||
|
)
|
||||||
|
|
||||||
|
$planMap = @{}
|
||||||
|
foreach ($plan in @($CapturePlans)) {
|
||||||
|
$normalizedPath = Get-NormalizedRegistryPathKey -Path $plan.Path
|
||||||
|
if ([string]::IsNullOrWhiteSpace($normalizedPath)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$planMap[$normalizedPath] = [PSCustomObject]@{
|
||||||
|
Path = $plan.Path
|
||||||
|
NormalizedPath = $normalizedPath
|
||||||
|
IncludeSubKeys = [bool]$plan.IncludeSubKeys
|
||||||
|
CaptureAllValues = [bool]$plan.CaptureAllValues
|
||||||
|
ValueNames = ConvertTo-RegistryValueNameSet -ValueNames @($plan.ValueNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $planMap
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-RegistryValueNameSet {
|
||||||
|
param(
|
||||||
|
[AllowEmptyCollection()]
|
||||||
|
[string[]]$ValueNames
|
||||||
|
)
|
||||||
|
|
||||||
|
$valueNameSet = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
foreach ($valueName in @($ValueNames)) {
|
||||||
|
$null = $valueNameSet.Add([string]$valueName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $valueNameSet
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-RegistrySnapshotAgainstAllowList {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Snapshot,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$PlanMap,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[AllowEmptyCollection()]
|
||||||
|
[System.Collections.Generic.List[string]]$Errors
|
||||||
|
)
|
||||||
|
|
||||||
|
$snapshotPath = [string]$Snapshot.Path
|
||||||
|
$normalizedPath = Get-NormalizedRegistryPathKey -Path $snapshotPath
|
||||||
|
if ([string]::IsNullOrWhiteSpace($normalizedPath)) {
|
||||||
|
$Errors.Add("Backup contains unsupported registry path '$snapshotPath'.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$planMatch = Find-RegistryAllowListPlanMatch -NormalizedPath $normalizedPath -PlanMap $PlanMap
|
||||||
|
if ($null -eq $planMatch) {
|
||||||
|
$Errors.Add("Backup contains unexpected registry path '$snapshotPath' that is not allowed by SelectedFeatures.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($valueSnapshot in @($Snapshot.Values)) {
|
||||||
|
$valueName = Get-NormalizedRegistryValueName -ValueName $valueSnapshot.Name
|
||||||
|
$valueExists = [bool]$valueSnapshot.Exists
|
||||||
|
|
||||||
|
if (-not (Test-RegistryValueAllowedByPlan -PlanMatch $planMatch -ValueName $valueName)) {
|
||||||
|
$Errors.Add("Backup contains unexpected value '$valueName' under '$snapshotPath'.")
|
||||||
|
}
|
||||||
|
|
||||||
|
$kindName = if ($valueSnapshot.PSObject.Properties['Kind']) { [string]$valueSnapshot.Kind } else { '' }
|
||||||
|
$valueReference = Get-RegistryValueReferenceForError -SnapshotPath $snapshotPath -ValueName $valueName
|
||||||
|
if ($valueExists) {
|
||||||
|
if (-not (Test-RegistryValueKindNameSupported -KindName $kindName)) {
|
||||||
|
$Errors.Add("Backup contains unsupported registry value kind '$kindName' for '$valueReference'.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif (-not [string]::IsNullOrWhiteSpace($kindName)) {
|
||||||
|
$Errors.Add("Backup value '$valueReference' must not define Kind when Exists is false.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($subKeySnapshot in @($Snapshot.SubKeys)) {
|
||||||
|
Test-RegistrySnapshotAgainstAllowList -Snapshot $subKeySnapshot -PlanMap $PlanMap -Errors $Errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-RegistryValueAllowedByPlan {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$PlanMatch,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[AllowNull()]
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[string]$ValueName
|
||||||
|
)
|
||||||
|
|
||||||
|
$ValueName = Get-NormalizedRegistryValueName -ValueName $ValueName
|
||||||
|
|
||||||
|
if ($PlanMatch.CaptureAllValues -or $PlanMatch.IsDescendant) {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
return $PlanMatch.ValueNames.Contains($ValueName)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RegistryValueReferenceForError {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$SnapshotPath,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[AllowNull()]
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[string]$ValueName
|
||||||
|
)
|
||||||
|
|
||||||
|
$ValueName = Get-NormalizedRegistryValueName -ValueName $ValueName
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ValueName)) {
|
||||||
|
return "$SnapshotPath\\(Default)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "$SnapshotPath\\$ValueName"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-NormalizedRegistryValueName {
|
||||||
|
param(
|
||||||
|
[AllowNull()]
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[object]$ValueName
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $ValueName) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return [string]$ValueName
|
||||||
|
}
|
||||||
|
|
||||||
|
function Find-RegistryAllowListPlanMatch {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$NormalizedPath,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$PlanMap
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($PlanMap.ContainsKey($NormalizedPath)) {
|
||||||
|
$plan = $PlanMap[$NormalizedPath]
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
IsDescendant = $false
|
||||||
|
CaptureAllValues = [bool]$plan.CaptureAllValues
|
||||||
|
ValueNames = $plan.ValueNames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($plan in @($PlanMap.Values)) {
|
||||||
|
if (-not [bool]$plan.IncludeSubKeys) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$subKeyPrefix = "$($plan.NormalizedPath)\\"
|
||||||
|
if ($NormalizedPath.StartsWith($subKeyPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
IsDescendant = $true
|
||||||
|
CaptureAllValues = $true
|
||||||
|
ValueNames = $plan.ValueNames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-NormalizedRegistryPathKey {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Path
|
||||||
|
)
|
||||||
|
|
||||||
|
$parts = Split-RegistryPath -path $Path
|
||||||
|
if (-not $parts) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$hiveName = [string]$parts.Hive
|
||||||
|
if ([string]::IsNullOrWhiteSpace($hiveName)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedHive = $hiveName.ToUpperInvariant()
|
||||||
|
$subKey = [string]$parts.SubKey
|
||||||
|
if ([string]::IsNullOrWhiteSpace($subKey)) {
|
||||||
|
return $normalizedHive
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedSubKey = ($subKey -replace '/', '\\').Trim('\')
|
||||||
|
if ([string]::IsNullOrWhiteSpace($normalizedSubKey)) {
|
||||||
|
return $normalizedHive
|
||||||
|
}
|
||||||
|
|
||||||
|
return "$normalizedHive\\$normalizedSubKey"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-RegistryValueKindNameSupported {
|
||||||
|
param(
|
||||||
|
[string]$KindName
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($KindName)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$kind = [System.Enum]::Parse([Microsoft.Win32.RegistryValueKind], $KindName, $true)
|
||||||
|
return $kind -ne [Microsoft.Win32.RegistryValueKind]::Unknown
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
# Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/
|
# Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/
|
||||||
function ReplaceStartMenuForAllUsers {
|
function ReplaceStartMenuForAllUsers {
|
||||||
param (
|
param (
|
||||||
$startMenuTemplate = "$script:AssetsPath/Start/start2.bin"
|
$startMenuTemplate = "$script:AssetsPath\Start\start2.bin"
|
||||||
)
|
)
|
||||||
|
|
||||||
Write-Host "> Removing all pinned apps from the start menu for all users..."
|
Write-Host "> Removing all pinned apps from the start menu for all users..."
|
||||||
@@ -16,7 +16,7 @@ function ReplaceStartMenuForAllUsers {
|
|||||||
|
|
||||||
# Get path to start menu file for all users
|
# Get path to start menu file for all users
|
||||||
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
|
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
|
||||||
$usersStartMenuPaths = get-childitem -path $userPathString
|
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
# Go through all users and replace the start menu file
|
# Go through all users and replace the start menu file
|
||||||
ForEach ($startMenuPath in $usersStartMenuPaths) {
|
ForEach ($startMenuPath in $usersStartMenuPaths) {
|
||||||
@@ -43,13 +43,13 @@ function ReplaceStartMenuForAllUsers {
|
|||||||
# Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/
|
# Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/
|
||||||
function ReplaceStartMenu {
|
function ReplaceStartMenu {
|
||||||
param (
|
param (
|
||||||
$startMenuTemplate = "$script:AssetsPath/Start/start2.bin",
|
$startMenuTemplate = "$script:AssetsPath\Start\start2.bin",
|
||||||
$startMenuBinFile = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
|
$startMenuBinFile = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Change path to correct user if a user was specified
|
# Change path to correct user if a user was specified
|
||||||
if ($script:Params.ContainsKey("User")) {
|
if ($script:Params.ContainsKey("User")) {
|
||||||
$startMenuBinFile = GetUserDirectory -userName "$(GetUserName)" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -exitIfPathNotFound $false
|
$startMenuBinFile = GetStartMenuBinPathForUser -UserName (GetUserName)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if template bin file exists
|
# Check if template bin file exists
|
||||||
@@ -63,7 +63,7 @@ function ReplaceStartMenu {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
$userName = [regex]::Match($startMenuBinFile, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value
|
$userName = GetStartMenuUserNameFromPath -StartMenuBinFile $startMenuBinFile
|
||||||
|
|
||||||
$backupBinFile = $startMenuBinFile + ".bak"
|
$backupBinFile = $startMenuBinFile + ".bak"
|
||||||
|
|
||||||
@@ -81,3 +81,141 @@ function ReplaceStartMenu {
|
|||||||
|
|
||||||
Write-Host "Replaced start menu for user $userName"
|
Write-Host "Replaced start menu for user $userName"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function GetStartMenuBinPathForUser {
|
||||||
|
param(
|
||||||
|
[string]$UserName
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($UserName)) {
|
||||||
|
return "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
|
||||||
|
}
|
||||||
|
|
||||||
|
return (GetUserDirectory -userName $UserName -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -exitIfPathNotFound $false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function GetStartMenuUserNameFromPath {
|
||||||
|
param(
|
||||||
|
[string]$StartMenuBinFile
|
||||||
|
)
|
||||||
|
|
||||||
|
$resolvedUserName = [regex]::Match($StartMenuBinFile, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value
|
||||||
|
if ([string]::IsNullOrWhiteSpace($resolvedUserName)) {
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
return $resolvedUserName
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function RestoreStartMenuFromBackup {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$StartMenuBinFile,
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[string]$BackupFilePath
|
||||||
|
)
|
||||||
|
|
||||||
|
$userName = GetStartMenuUserNameFromPath -StartMenuBinFile $StartMenuBinFile
|
||||||
|
$backupBinFile = if ([string]::IsNullOrWhiteSpace($BackupFilePath)) {
|
||||||
|
$StartMenuBinFile + '.bak'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$BackupFilePath
|
||||||
|
}
|
||||||
|
$currentBinBackup = $StartMenuBinFile + '.restore.bak'
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $backupBinFile)) {
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
UserName = $userName
|
||||||
|
Result = $false
|
||||||
|
Message = "No start menu backup file found for user $userName."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (Test-Path -LiteralPath $StartMenuBinFile) {
|
||||||
|
Move-Item -Path $StartMenuBinFile -Destination $currentBinBackup -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-Item -Path $backupBinFile -Destination $StartMenuBinFile -Force
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
UserName = $userName
|
||||||
|
Result = $true
|
||||||
|
Message = "Restored start menu for user $userName."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
UserName = $userName
|
||||||
|
Result = $false
|
||||||
|
Message = "Failed to restore start menu for user $userName. $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function RestoreStartMenu {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[string]$BackupFilePath
|
||||||
|
)
|
||||||
|
|
||||||
|
$targetUserName = GetUserName
|
||||||
|
$startMenuBinFile = GetStartMenuBinPathForUser -UserName $targetUserName
|
||||||
|
|
||||||
|
Write-Host "Restoring start menu for user $targetUserName from backup..."
|
||||||
|
|
||||||
|
return RestoreStartMenuFromBackup -StartMenuBinFile $startMenuBinFile -BackupFilePath $BackupFilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
function RestoreStartMenuForAllUsers {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[string]$BackupFilePath
|
||||||
|
)
|
||||||
|
|
||||||
|
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
|
||||||
|
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
|
||||||
|
$results = @()
|
||||||
|
|
||||||
|
Write-Host "Restoring start menu for all users from backup..."
|
||||||
|
|
||||||
|
foreach ($startMenuPath in $usersStartMenuPaths) {
|
||||||
|
$startMenuBinFile = Join-Path $startMenuPath.FullName 'start2.bin'
|
||||||
|
$results += RestoreStartMenuFromBackup -StartMenuBinFile $startMenuBinFile -BackupFilePath $BackupFilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaultStartMenuPath = GetUserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState" -exitIfPathNotFound $false
|
||||||
|
|
||||||
|
if (Test-Path $defaultStartMenuPath) {
|
||||||
|
$defaultStartMenuBinFile = Join-Path $defaultStartMenuPath 'start2.bin'
|
||||||
|
if (Test-Path -LiteralPath $defaultStartMenuBinFile) {
|
||||||
|
try {
|
||||||
|
Remove-Item -LiteralPath $defaultStartMenuBinFile -Force
|
||||||
|
$results += [PSCustomObject]@{
|
||||||
|
UserName = 'Default'
|
||||||
|
Result = $true
|
||||||
|
Message = 'Removed start2.bin for the default user profile.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$results += [PSCustomObject]@{
|
||||||
|
UserName = 'Default'
|
||||||
|
Result = $false
|
||||||
|
Message = "Failed to remove start2.bin for the default user profile. $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($results.Count -eq 0) {
|
||||||
|
$results += [PSCustomObject]@{
|
||||||
|
UserName = 'unknown'
|
||||||
|
Result = $false
|
||||||
|
Message = 'No user start menu locations were found.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ function RestartExplorer {
|
|||||||
foreach ($paramKey in $script:Params.Keys) {
|
foreach ($paramKey in $script:Params.Keys) {
|
||||||
if ($script:Features.ContainsKey($paramKey) -and $script:Features[$paramKey].RequiresReboot -eq $true) {
|
if ($script:Features.ContainsKey($paramKey) -and $script:Features[$paramKey].RequiresReboot -eq $true) {
|
||||||
$feature = $script:Features[$paramKey]
|
$feature = $script:Features[$paramKey]
|
||||||
Write-Host "Warning: '$($feature.Action) $($feature.Label)' requires a reboot to take full effect" -ForegroundColor Yellow
|
Write-Host "Warning: '$($feature.Label)' requires a reboot to take full effect" -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
241
Scripts/Features/RestoreRegistryApplyState.ps1
Normal file
241
Scripts/Features/RestoreRegistryApplyState.ps1
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
function Invoke-WithLoadedRestoreHive {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Target,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[scriptblock]$ScriptBlock,
|
||||||
|
$ArgumentObject = $null
|
||||||
|
)
|
||||||
|
|
||||||
|
$hiveDatPath = if ($Target -eq 'DefaultUserProfile') {
|
||||||
|
GetUserDirectory -userName 'Default' -fileName 'NTUSER.DAT'
|
||||||
|
}
|
||||||
|
elseif ($Target -like 'User:*') {
|
||||||
|
$userName = $Target.Substring(5)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($userName)) {
|
||||||
|
throw 'Invalid backup target format for user restore.'
|
||||||
|
}
|
||||||
|
GetUserDirectory -userName $userName -fileName 'NTUSER.DAT'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw "Unsupported backup target '$Target'."
|
||||||
|
}
|
||||||
|
|
||||||
|
$global:LASTEXITCODE = 0
|
||||||
|
reg load 'HKU\Default' "$hiveDatPath" | Out-Null
|
||||||
|
$loadExitCode = $LASTEXITCODE
|
||||||
|
if ($loadExitCode -ne 0) {
|
||||||
|
throw "Failed to load target user hive '$hiveDatPath' (exit code: $loadExitCode)."
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
& $ScriptBlock $ArgumentObject
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$global:LASTEXITCODE = 0
|
||||||
|
reg unload 'HKU\Default' | Out-Null
|
||||||
|
$unloadExitCode = $LASTEXITCODE
|
||||||
|
if ($unloadExitCode -ne 0) {
|
||||||
|
throw "Failed to unload registry hive 'HKU\Default' (exit code: $unloadExitCode)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Restore-RegistryKeySnapshot {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Snapshot
|
||||||
|
)
|
||||||
|
|
||||||
|
$registryParts = Split-RegistryPath -path $Snapshot.Path
|
||||||
|
if (-not $registryParts) {
|
||||||
|
throw "Unsupported registry path in backup: $($Snapshot.Path)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$rootKey = Get-RegistryRootKey -hiveName $registryParts.Hive
|
||||||
|
if (-not $rootKey) {
|
||||||
|
throw "Unsupported registry hive in backup: $($registryParts.Hive)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$subKeyPath = $registryParts.SubKey
|
||||||
|
if ([string]::IsNullOrWhiteSpace($subKeyPath)) {
|
||||||
|
throw "Unsupported root-level registry path in backup: $($Snapshot.Path)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $Snapshot.Exists) {
|
||||||
|
Remove-RegistrySubKeyTreeIfExists -RootKey $rootKey -SubKeyPath $subKeyPath
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$forceFullTree = @($Snapshot.SubKeys).Count -gt 0
|
||||||
|
if ($forceFullTree) {
|
||||||
|
Remove-RegistrySubKeyTreeIfExists -RootKey $rootKey -SubKeyPath $subKeyPath
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = $rootKey.CreateSubKey($subKeyPath)
|
||||||
|
if ($null -eq $key) {
|
||||||
|
throw "Unable to create or open registry key '$($Snapshot.Path)'"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
foreach ($valueSnapshot in @($Snapshot.Values)) {
|
||||||
|
Restore-RegistryValueSnapshot -RegistryKey $key -Snapshot $valueSnapshot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$key.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($subKeySnapshot in @($Snapshot.SubKeys)) {
|
||||||
|
Restore-RegistryKeySnapshot -Snapshot $subKeySnapshot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Restore-RegistryValueSnapshot {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[Microsoft.Win32.RegistryKey]$RegistryKey,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Snapshot
|
||||||
|
)
|
||||||
|
|
||||||
|
$valueName = if ($null -ne $Snapshot.Name) { [string]$Snapshot.Name } else { '' }
|
||||||
|
|
||||||
|
if (-not [bool]$Snapshot.Exists) {
|
||||||
|
try {
|
||||||
|
$RegistryKey.DeleteValue($valueName, $false)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
throw "Failed deleting registry value '$valueName' in '$($RegistryKey.Name)': $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$valueKind = Convert-RegistryValueKindFromBackup -KindName $Snapshot.Kind
|
||||||
|
$normalizedData = Convert-RegistryValueDataFromBackup -Kind $valueKind -Data $Snapshot.Data
|
||||||
|
|
||||||
|
try {
|
||||||
|
$RegistryKey.SetValue($valueName, $normalizedData, $valueKind)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$retryBytes = Convert-BackupDataToByteArray -Data $Snapshot.Data
|
||||||
|
if ($null -ne $retryBytes) {
|
||||||
|
try {
|
||||||
|
$RegistryKey.SetValue($valueName, $retryBytes, [Microsoft.Win32.RegistryValueKind]::Binary)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# Fall through to original error message for context.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "Failed setting registry value '$valueName' in '$($RegistryKey.Name)': $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Convert-RegistryValueKindFromBackup {
|
||||||
|
param(
|
||||||
|
[string]$KindName
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($KindName)) {
|
||||||
|
return [Microsoft.Win32.RegistryValueKind]::String
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return [System.Enum]::Parse([Microsoft.Win32.RegistryValueKind], $KindName, $true)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
throw "Unsupported registry value kind in backup: $KindName"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Convert-RegistryValueDataFromBackup {
|
||||||
|
param(
|
||||||
|
[Microsoft.Win32.RegistryValueKind]$Kind,
|
||||||
|
$Data
|
||||||
|
)
|
||||||
|
|
||||||
|
switch ($Kind) {
|
||||||
|
([Microsoft.Win32.RegistryValueKind]::DWord) {
|
||||||
|
$unsigned = [uint32]$Data
|
||||||
|
return [BitConverter]::ToInt32([BitConverter]::GetBytes($unsigned), 0)
|
||||||
|
}
|
||||||
|
([Microsoft.Win32.RegistryValueKind]::QWord) {
|
||||||
|
$unsigned = [uint64]$Data
|
||||||
|
return [BitConverter]::ToInt64([BitConverter]::GetBytes($unsigned), 0)
|
||||||
|
}
|
||||||
|
([Microsoft.Win32.RegistryValueKind]::MultiString) { return @($Data | ForEach-Object { [string]$_ }) }
|
||||||
|
([Microsoft.Win32.RegistryValueKind]::Binary) {
|
||||||
|
$bytes = Convert-BackupDataToByteArray -Data $Data
|
||||||
|
if ($null -eq $bytes) {
|
||||||
|
return (New-Object byte[] 0)
|
||||||
|
}
|
||||||
|
return $bytes
|
||||||
|
}
|
||||||
|
([Microsoft.Win32.RegistryValueKind]::None) { return $null }
|
||||||
|
default {
|
||||||
|
if ($null -ne $Data) {
|
||||||
|
return [string]$Data
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Convert-BackupDataToByteArray {
|
||||||
|
param(
|
||||||
|
$Data
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $Data) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Data -is [byte[]]) {
|
||||||
|
return ,$Data
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = @($Data)
|
||||||
|
if ($items.Count -eq 0) {
|
||||||
|
return ,(New-Object byte[] 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($item in $items) {
|
||||||
|
if ($item -isnot [ValueType] -and $item -isnot [string]) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$parsed = 0
|
||||||
|
if (-not [int]::TryParse([string]$item, [ref]$parsed)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($parsed -lt 0 -or $parsed -gt 255) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$bytes = New-Object byte[] $items.Count
|
||||||
|
for ($i = 0; $i -lt $items.Count; $i++) {
|
||||||
|
$bytes[$i] = [byte][int]$items[$i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ,$bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
function Remove-RegistrySubKeyTreeIfExists {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[Microsoft.Win32.RegistryKey]$RootKey,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$SubKeyPath
|
||||||
|
)
|
||||||
|
|
||||||
|
$existing = $RootKey.OpenSubKey($SubKeyPath, $false)
|
||||||
|
if ($existing) {
|
||||||
|
$existing.Close()
|
||||||
|
$RootKey.DeleteSubKeyTree($SubKeyPath, $false)
|
||||||
|
}
|
||||||
|
}
|
||||||
156
Scripts/Features/RestoreRegistryBackup.ps1
Normal file
156
Scripts/Features/RestoreRegistryBackup.ps1
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
function Load-RegistryBackupFromFile {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$FilePath
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $FilePath)) {
|
||||||
|
throw "Backup file was not found: $FilePath"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$rawBackup = Get-Content -LiteralPath $FilePath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
throw "Failed to read backup file '$FilePath'. The file is not valid JSON."
|
||||||
|
}
|
||||||
|
|
||||||
|
return Normalize-RegistryBackup -Backup $rawBackup
|
||||||
|
}
|
||||||
|
|
||||||
|
function Normalize-RegistryBackup {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Backup
|
||||||
|
)
|
||||||
|
|
||||||
|
$errors = New-Object System.Collections.Generic.List[string]
|
||||||
|
|
||||||
|
if (-not $Backup.PSObject.Properties['Version']) {
|
||||||
|
$errors.Add('Missing property: Version')
|
||||||
|
}
|
||||||
|
elseif ([string]$Backup.Version -ne '1.0') {
|
||||||
|
$errors.Add("Unsupported backup version '$($Backup.Version)'.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $Backup.PSObject.Properties['BackupType']) {
|
||||||
|
$errors.Add('Missing property: BackupType')
|
||||||
|
}
|
||||||
|
elseif ([string]$Backup.BackupType -ne 'RegistryState') {
|
||||||
|
$errors.Add("Unsupported BackupType '$($Backup.BackupType)'.")
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedTarget = ''
|
||||||
|
if (-not $Backup.PSObject.Properties['Target'] -or [string]::IsNullOrWhiteSpace([string]$Backup.Target)) {
|
||||||
|
$errors.Add('Missing property: Target')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$normalizedTarget = [string]$Backup.Target
|
||||||
|
|
||||||
|
if ($normalizedTarget -eq 'DefaultUserProfile') {
|
||||||
|
# Valid target format.
|
||||||
|
}
|
||||||
|
elseif ($normalizedTarget -like 'User:*') {
|
||||||
|
$targetUserName = $normalizedTarget.Substring(5)
|
||||||
|
$targetValidation = Test-TargetUserName -UserName $targetUserName
|
||||||
|
if (-not $targetValidation.IsValid) {
|
||||||
|
$errors.Add("Invalid user '$normalizedTarget'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($normalizedTarget -like 'CurrentUser:*') {
|
||||||
|
$targetCurrentUserName = $normalizedTarget.Substring(12)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($targetCurrentUserName) -or ($targetCurrentUserName -ne $env:USERNAME)) {
|
||||||
|
$errors.Add("Backup was made for '$targetCurrentUserName', this does not match current user '$env:USERNAME'.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$errors.Add("Unsupported Target '$normalizedTarget'.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$registryKeys = @()
|
||||||
|
if (-not $Backup.PSObject.Properties['RegistryKeys']) {
|
||||||
|
$errors.Add('Missing property: RegistryKeys')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$registryKeys = @($Backup.RegistryKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedKeys = @()
|
||||||
|
foreach ($keySnapshot in $registryKeys) {
|
||||||
|
$normalizedKeys += @(Normalize-RegistryKeySnapshot -Snapshot $keySnapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedFeatureParseResult = Get-NormalizedSelectedFeatureIdsFromBackup -Backup $Backup
|
||||||
|
$selectedFeatures = @($selectedFeatureParseResult.SelectedFeatures)
|
||||||
|
foreach ($selectedFeatureParseError in @($selectedFeatureParseResult.Errors)) {
|
||||||
|
$errors.Add([string]$selectedFeatureParseError)
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedUndoFeatureParseResult = Get-NormalizedSelectedUndoFeatureIdsFromBackup -Backup $Backup
|
||||||
|
$selectedUndoFeatures = @($selectedUndoFeatureParseResult.SelectedUndoFeatures)
|
||||||
|
foreach ($selectedUndoFeatureParseError in @($selectedUndoFeatureParseResult.Errors)) {
|
||||||
|
$errors.Add([string]$selectedUndoFeatureParseError)
|
||||||
|
}
|
||||||
|
|
||||||
|
$allSelectedFeatures = @($selectedFeatures) + @($selectedUndoFeatures)
|
||||||
|
if ($allSelectedFeatures.Count -eq 0) {
|
||||||
|
$errors.Add('Backup must contain at least one feature ID in SelectedFeatures or SelectedUndoFeatures.')
|
||||||
|
}
|
||||||
|
$allowListValidationErrors = @(Test-RegistryBackupMatchesSelectedFeatures -SelectedFeatureIds @($selectedFeatures) -SelectedUndoFeatureIds @($selectedUndoFeatures) -Target $normalizedTarget -RegistryKeys @($normalizedKeys))
|
||||||
|
foreach ($allowListValidationError in $allowListValidationErrors) {
|
||||||
|
$errors.Add([string]$allowListValidationError)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($errors.Count -gt 0) {
|
||||||
|
Write-Error "Backup validation failed: $($errors -join ' ')"
|
||||||
|
if ($errors.Count -eq 1) {
|
||||||
|
throw ("Validation failed: $($errors[0])")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw ("Validation failed with $($errors.Count) errors. See console output for details.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Version = [string]$Backup.Version
|
||||||
|
BackupType = [string]$Backup.BackupType
|
||||||
|
CreatedAt = [string]$Backup.CreatedAt
|
||||||
|
CreatedBy = [string]$Backup.CreatedBy
|
||||||
|
ComputerName = [string]$Backup.ComputerName
|
||||||
|
Target = $normalizedTarget
|
||||||
|
SelectedFeatures = @($selectedFeatures)
|
||||||
|
SelectedUndoFeatures = @($selectedUndoFeatures)
|
||||||
|
RegistryKeys = @($normalizedKeys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Restore-RegistryBackupState {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Backup
|
||||||
|
)
|
||||||
|
|
||||||
|
$friendlyTarget = GetFriendlyRegistryBackupTarget -Target ([string]$Backup.Target)
|
||||||
|
|
||||||
|
$restoreAction = {
|
||||||
|
param($normalizedBackup)
|
||||||
|
|
||||||
|
Write-Host "Applying registry restore from $(@($normalizedBackup.RegistryKeys).Count) root snapshot(s)."
|
||||||
|
foreach ($rootSnapshot in @($normalizedBackup.RegistryKeys)) {
|
||||||
|
Restore-RegistryKeySnapshot -Snapshot $rootSnapshot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Starting restore for $friendlyTarget."
|
||||||
|
|
||||||
|
if ($Backup.Target -eq 'DefaultUserProfile' -or $Backup.Target -like 'User:*') {
|
||||||
|
Write-Host "Restore requires loading target user hive."
|
||||||
|
Invoke-WithLoadedRestoreHive -Target $Backup.Target -ScriptBlock $restoreAction -ArgumentObject $Backup
|
||||||
|
Write-Host "Restore completed for $friendlyTarget."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
& $restoreAction $Backup
|
||||||
|
Write-Host "Restore completed for $friendlyTarget."
|
||||||
|
}
|
||||||
@@ -6,11 +6,14 @@ function SaveToFile {
|
|||||||
[hashtable]$Config,
|
[hashtable]$Config,
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
[Parameter(Mandatory=$true)]
|
||||||
[string]$FilePath
|
[string]$FilePath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[int]$MaxDepth = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$Config | ConvertTo-Json -Depth 10 | Set-Content -Path $FilePath -Encoding UTF8
|
$Config | ConvertTo-Json -Depth $MaxDepth | Set-Content -Path $FilePath -Encoding UTF8
|
||||||
return $true
|
return $true
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
|
|||||||
175
Scripts/GUI/RestoreBackupDialogFeatureLists.ps1
Normal file
175
Scripts/GUI/RestoreBackupDialogFeatureLists.ps1
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
function New-RestoreDialogState {
|
||||||
|
param(
|
||||||
|
[string]$Result = 'Cancel',
|
||||||
|
[string]$SelectedFile = $null,
|
||||||
|
$Backup = $null
|
||||||
|
)
|
||||||
|
|
||||||
|
return @{ Result = $Result; SelectedFile = $SelectedFile; Backup = $Backup }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RestoreDialogFeatureDefinition {
|
||||||
|
param(
|
||||||
|
[string]$FeatureId,
|
||||||
|
[hashtable]$Features
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($FeatureId) -or -not $Features) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Features.ContainsKey($FeatureId)) {
|
||||||
|
return $Features[$FeatureId]
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-RestoreDialogFeatureCanAutoRevert {
|
||||||
|
param(
|
||||||
|
[string]$FeatureId,
|
||||||
|
[hashtable]$Features
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($FeatureId)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$featureDefinition = Get-RestoreDialogFeatureDefinition -FeatureId $FeatureId -Features $Features
|
||||||
|
if ($featureDefinition) {
|
||||||
|
return -not [string]::IsNullOrWhiteSpace([string]$featureDefinition.RegistryKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RestoreDialogFeatureDisplayLabel {
|
||||||
|
param(
|
||||||
|
[string]$FeatureId,
|
||||||
|
[hashtable]$Features
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($FeatureId)) {
|
||||||
|
return 'Unknown feature'
|
||||||
|
}
|
||||||
|
|
||||||
|
$featureDefinition = Get-RestoreDialogFeatureDefinition -FeatureId $FeatureId -Features $Features
|
||||||
|
if ($featureDefinition -and -not [string]::IsNullOrWhiteSpace([string]$featureDefinition.Label)) {
|
||||||
|
return [string]$featureDefinition.Label
|
||||||
|
}
|
||||||
|
|
||||||
|
return $FeatureId
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-RestoreDialogFeatureVisibleInOverview {
|
||||||
|
param(
|
||||||
|
[string]$FeatureId,
|
||||||
|
[hashtable]$Features
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($FeatureId)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$featureDefinition = Get-RestoreDialogFeatureDefinition -FeatureId $FeatureId -Features $Features
|
||||||
|
if (-not $featureDefinition) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
return -not [string]::IsNullOrWhiteSpace([string]$featureDefinition.Category)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SelectedForwardFeatureIdsFromBackup {
|
||||||
|
param($SelectedBackup)
|
||||||
|
|
||||||
|
$selectedFeatureIds = New-Object System.Collections.Generic.List[string]
|
||||||
|
$seenSelectedFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
|
||||||
|
foreach ($featureId in @($SelectedBackup.SelectedFeatures)) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$featureId)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedId = [string]$featureId
|
||||||
|
if ($seenSelectedFeatureIds.Add($normalizedId)) {
|
||||||
|
$selectedFeatureIds.Add($normalizedId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return @($selectedFeatureIds.ToArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SelectedUndoFeatureIdsFromBackup {
|
||||||
|
param($SelectedBackup)
|
||||||
|
|
||||||
|
$selectedUndoFeatureIds = New-Object System.Collections.Generic.List[string]
|
||||||
|
$seenUndoFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
|
||||||
|
foreach ($featureId in @($SelectedBackup.SelectedUndoFeatures)) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$featureId)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedId = [string]$featureId
|
||||||
|
if ($seenUndoFeatureIds.Add($normalizedId)) {
|
||||||
|
$selectedUndoFeatureIds.Add($normalizedId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return @($selectedUndoFeatureIds.ToArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-CombinedSelectedFeatureIdsFromBackup {
|
||||||
|
param($SelectedBackup)
|
||||||
|
|
||||||
|
$featureIds = New-Object System.Collections.Generic.List[string]
|
||||||
|
$seenIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
|
||||||
|
foreach ($featureId in @(Get-SelectedForwardFeatureIdsFromBackup -SelectedBackup $SelectedBackup) + @(Get-SelectedUndoFeatureIdsFromBackup -SelectedBackup $SelectedBackup)) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$featureId)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedId = [string]$featureId
|
||||||
|
if ($seenIds.Add($normalizedId)) {
|
||||||
|
$featureIds.Add($normalizedId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return @($featureIds.ToArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SelectedFeatureIdsFromBackup {
|
||||||
|
param($SelectedBackup)
|
||||||
|
|
||||||
|
return @(Get-CombinedSelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RestoreBackupFeatureLists {
|
||||||
|
param(
|
||||||
|
[string[]]$SelectedFeatureIds,
|
||||||
|
[hashtable]$Features
|
||||||
|
)
|
||||||
|
|
||||||
|
$revertibleFeaturesList = @()
|
||||||
|
$nonRevertibleFeaturesList = @()
|
||||||
|
|
||||||
|
foreach ($featureId in $SelectedFeatureIds) {
|
||||||
|
if (-not (Test-RestoreDialogFeatureVisibleInOverview -FeatureId $featureId -Features $Features)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$displayItem = [PSCustomObject]@{ DisplayText = "- $(Get-RestoreDialogFeatureDisplayLabel -FeatureId $featureId -Features $Features)" }
|
||||||
|
if (Test-RestoreDialogFeatureCanAutoRevert -FeatureId $featureId -Features $Features) {
|
||||||
|
$revertibleFeaturesList += $displayItem
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$nonRevertibleFeaturesList += $displayItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Revertible = @($revertibleFeaturesList)
|
||||||
|
NonRevertible = @($nonRevertibleFeaturesList)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,71 @@
|
|||||||
# Sets resource colors for a WPF window based on dark mode preference
|
# Sets resource colors for a WPF window based on dark mode preference
|
||||||
|
function GetIconFontFamilyName {
|
||||||
|
if ($script:IconFontFamilyName) {
|
||||||
|
return $script:IconFontFamilyName
|
||||||
|
}
|
||||||
|
|
||||||
|
$preferredFont = 'Segoe Fluent Icons'
|
||||||
|
$fallbackFont = 'Segoe MDL2 Assets'
|
||||||
|
|
||||||
|
try {
|
||||||
|
$systemFonts = [System.Windows.Media.Fonts]::SystemFontFamilies | ForEach-Object { $_.Source }
|
||||||
|
|
||||||
|
if ($systemFonts -contains $preferredFont) {
|
||||||
|
$script:IconFontFamilyName = $preferredFont
|
||||||
|
}
|
||||||
|
elseif ($systemFonts -contains $fallbackFont) {
|
||||||
|
$script:IconFontFamilyName = $fallbackFont
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# Last resort fallback if the expected symbol fonts are unavailable.
|
||||||
|
$script:IconFontFamilyName = 'Segoe UI Symbol'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$script:IconFontFamilyName = $fallbackFont
|
||||||
|
}
|
||||||
|
|
||||||
|
return $script:IconFontFamilyName
|
||||||
|
}
|
||||||
|
|
||||||
|
function SetIconFontFallback {
|
||||||
|
param($window)
|
||||||
|
|
||||||
|
if (-not $window) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetFontName = GetIconFontFamilyName
|
||||||
|
if ($targetFontName -eq 'Segoe Fluent Icons') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetFontFamily = [System.Windows.Media.FontFamily]::new($targetFontName)
|
||||||
|
$queue = [System.Collections.Queue]::new()
|
||||||
|
$queue.Enqueue($window)
|
||||||
|
|
||||||
|
while ($queue.Count -gt 0) {
|
||||||
|
$node = $queue.Dequeue()
|
||||||
|
|
||||||
|
if ($node -is [System.Windows.Controls.TextBlock]) {
|
||||||
|
if ($node.FontFamily -and $node.FontFamily.Source -eq 'Segoe Fluent Icons') {
|
||||||
|
$node.FontFamily = $targetFontFamily
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($node -is [System.Windows.Controls.Control]) {
|
||||||
|
if ($node.FontFamily -and $node.FontFamily.Source -eq 'Segoe Fluent Icons') {
|
||||||
|
$node.FontFamily = $targetFontFamily
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($child in [System.Windows.LogicalTreeHelper]::GetChildren($node)) {
|
||||||
|
if ($child -is [System.Windows.DependencyObject]) {
|
||||||
|
$queue.Enqueue($child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function SetWindowThemeResources {
|
function SetWindowThemeResources {
|
||||||
param (
|
param (
|
||||||
$window,
|
$window,
|
||||||
@@ -75,6 +142,7 @@ function SetWindowThemeResources {
|
|||||||
$window.Resources.Add("ButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#3284cc")))
|
$window.Resources.Add("ButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#3284cc")))
|
||||||
$window.Resources.Add("CloseHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#c42b1c")))
|
$window.Resources.Add("CloseHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#c42b1c")))
|
||||||
$window.Resources.Add("InformationIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0078D4")))
|
$window.Resources.Add("InformationIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0078D4")))
|
||||||
|
$window.Resources.Add("SuccessIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#107C10")))
|
||||||
$window.Resources.Add("WarningIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFB900")))
|
$window.Resources.Add("WarningIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFB900")))
|
||||||
$window.Resources.Add("ErrorIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#E81123")))
|
$window.Resources.Add("ErrorIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#E81123")))
|
||||||
$window.Resources.Add("QuestionIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0078D4")))
|
$window.Resources.Add("QuestionIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0078D4")))
|
||||||
@@ -91,4 +159,6 @@ function SetWindowThemeResources {
|
|||||||
$sharedReader.Close()
|
$sharedReader.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SetIconFontFallback -window $window
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ function Show-ApplyModal {
|
|||||||
try {
|
try {
|
||||||
ExecuteAllChanges
|
ExecuteAllChanges
|
||||||
|
|
||||||
|
$registryImportFailureCount = [int]$script:RegistryImportFailures
|
||||||
|
|
||||||
# Restart explorer if requested
|
# Restart explorer if requested
|
||||||
if ($RestartExplorer -and -not $script:CancelRequested) {
|
if ($RestartExplorer -and -not $script:CancelRequested) {
|
||||||
RestartExplorer
|
RestartExplorer
|
||||||
@@ -139,7 +141,7 @@ function Show-ApplyModal {
|
|||||||
Write-Host ""
|
Write-Host ""
|
||||||
if ($script:CancelRequested) {
|
if ($script:CancelRequested) {
|
||||||
Write-Host "Script execution was cancelled by the user. Some changes may not have been applied."
|
Write-Host "Script execution was cancelled by the user. Some changes may not have been applied."
|
||||||
} else {
|
} elseif ($registryImportFailureCount -eq 0) {
|
||||||
Write-Host "All changes have been applied successfully!"
|
Write-Host "All changes have been applied successfully!"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +155,11 @@ function Show-ApplyModal {
|
|||||||
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d"))
|
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d"))
|
||||||
$script:ApplyCompletionTitleEl.Text = "Cancelled"
|
$script:ApplyCompletionTitleEl.Text = "Cancelled"
|
||||||
$script:ApplyCompletionMessageEl.Text = "Script execution was cancelled by the user."
|
$script:ApplyCompletionMessageEl.Text = "Script execution was cancelled by the user."
|
||||||
|
} elseif ($registryImportFailureCount -gt 0) {
|
||||||
|
$script:ApplyCompletionIconEl.Text = [char]0xE7BA
|
||||||
|
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d"))
|
||||||
|
$script:ApplyCompletionTitleEl.Text = "Changes Applied with Errors"
|
||||||
|
$script:ApplyCompletionMessageEl.Text = "$registryImportFailureCount registry change(s) failed. See console for details."
|
||||||
} else {
|
} else {
|
||||||
$script:ApplyCompletionTitleEl.Text = "Changes Applied"
|
$script:ApplyCompletionTitleEl.Text = "Changes Applied"
|
||||||
|
|
||||||
@@ -162,7 +169,7 @@ function Show-ApplyModal {
|
|||||||
foreach ($paramKey in $script:Params.Keys) {
|
foreach ($paramKey in $script:Params.Keys) {
|
||||||
if ($script:Features.ContainsKey($paramKey) -and $script:Features[$paramKey].RequiresReboot -eq $true) {
|
if ($script:Features.ContainsKey($paramKey) -and $script:Features[$paramKey].RequiresReboot -eq $true) {
|
||||||
$feature = $script:Features[$paramKey]
|
$feature = $script:Features[$paramKey]
|
||||||
$rebootFeatures += "$($feature.Action) $($feature.Label)"
|
$rebootFeatures += "$($feature.Label)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +186,7 @@ function Show-ApplyModal {
|
|||||||
$applyRebootPanel.Visibility = 'Visible'
|
$applyRebootPanel.Visibility = 'Visible'
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$script:ApplyCompletionMessageEl.Text = "Your clean system is ready. Thanks for using Win11Debloat!"
|
$script:ApplyCompletionMessageEl.Text = "Your system is ready. Thanks for using Win11Debloat!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ function Show-ImportExportConfigWindow {
|
|||||||
[string]$Prompt,
|
[string]$Prompt,
|
||||||
[string[]]$Categories = @('Applications', 'System Tweaks', 'Deployment Settings'),
|
[string[]]$Categories = @('Applications', 'System Tweaks', 'Deployment Settings'),
|
||||||
[string[]]$DisabledCategories = @(),
|
[string[]]$DisabledCategories = @(),
|
||||||
[hashtable]$CategoryDetails = @()
|
[hashtable]$CategoryDetails = @(),
|
||||||
|
[string]$ActionLabel = 'OK'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Show overlay on owner window
|
# Show overlay on owner window
|
||||||
@@ -105,6 +106,7 @@ function Show-ImportExportConfigWindow {
|
|||||||
|
|
||||||
$okBtn = $dlg.FindName('OkButton')
|
$okBtn = $dlg.FindName('OkButton')
|
||||||
$cancelBtn = $dlg.FindName('CancelButton')
|
$cancelBtn = $dlg.FindName('CancelButton')
|
||||||
|
$okBtn.Content = $ActionLabel
|
||||||
$okBtn.Add_Click({ $dlg.Tag = 'OK'; $dlg.Close() })
|
$okBtn.Add_Click({ $dlg.Tag = 'OK'; $dlg.Close() })
|
||||||
$cancelBtn.Add_Click({ $dlg.Tag = 'Cancel'; $dlg.Close() })
|
$cancelBtn.Add_Click({ $dlg.Tag = 'Cancel'; $dlg.Close() })
|
||||||
|
|
||||||
@@ -379,8 +381,11 @@ function Export-Configuration {
|
|||||||
$deploymentSettings = Get-DeploymentSettings -Owner $Owner -UserSelectionCombo $UserSelectionCombo -OtherUsernameTextBox $OtherUsernameTextBox
|
$deploymentSettings = Get-DeploymentSettings -Owner $Owner -UserSelectionCombo $UserSelectionCombo -OtherUsernameTextBox $OtherUsernameTextBox
|
||||||
$categoryDetails = Build-CategoryDetails -AppCount $selectedApps.Count -TweakCount $tweakSettings.Count -DeploymentSettings $deploymentSettings
|
$categoryDetails = Build-CategoryDetails -AppCount $selectedApps.Count -TweakCount $tweakSettings.Count -DeploymentSettings $deploymentSettings
|
||||||
|
|
||||||
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Export Configuration' -Prompt 'Select which settings to include in the export:' -DisabledCategories $disabledCategories -CategoryDetails $categoryDetails
|
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Export Configuration' -Prompt 'Select the settings you wish to include in your export.' -DisabledCategories $disabledCategories -CategoryDetails $categoryDetails -ActionLabel 'Export Settings'
|
||||||
if (-not $categories) { return }
|
if (-not $categories) {
|
||||||
|
Write-Host 'Export canceled.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
$config = @{ Version = '1.0' }
|
$config = @{ Version = '1.0' }
|
||||||
|
|
||||||
@@ -401,12 +406,19 @@ function Export-Configuration {
|
|||||||
$saveDialog.DefaultExt = '.json'
|
$saveDialog.DefaultExt = '.json'
|
||||||
$saveDialog.FileName = "Win11Debloat-Config-$(Get-Date -Format 'yyyyMMdd').json"
|
$saveDialog.FileName = "Win11Debloat-Config-$(Get-Date -Format 'yyyyMMdd').json"
|
||||||
|
|
||||||
if ($saveDialog.ShowDialog($Owner) -ne $true) { return }
|
if ($saveDialog.ShowDialog($Owner) -ne $true) {
|
||||||
|
Write-Host 'Export save dialog canceled.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Exporting configuration to '$($saveDialog.FileName)'... (Categories: $($categories -join ', '))"
|
||||||
|
|
||||||
if (SaveToFile -Config $config -FilePath $saveDialog.FileName) {
|
if (SaveToFile -Config $config -FilePath $saveDialog.FileName) {
|
||||||
|
Write-Host "Configuration exported successfully: $($saveDialog.FileName)"
|
||||||
Show-MessageBox -Message "Configuration exported successfully." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null
|
Show-MessageBox -Message "Configuration exported successfully." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
Write-Error "Failed to export configuration to '$($saveDialog.FileName)'"
|
||||||
Show-MessageBox -Message "Failed to export configuration" -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null
|
Show-MessageBox -Message "Failed to export configuration" -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,36 +437,49 @@ function Import-Configuration {
|
|||||||
|
|
||||||
# Show native open-file dialog
|
# Show native open-file dialog
|
||||||
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
|
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
|
||||||
$openDialog.Title = 'Import Configuration'
|
$openDialog.Title = 'Select Configuration File'
|
||||||
$openDialog.Filter = 'JSON files (*.json)|*.json|All files (*.*)|*.*'
|
$openDialog.Filter = 'JSON files (*.json)|*.json|All files (*.*)|*.*'
|
||||||
$openDialog.DefaultExt = '.json'
|
$openDialog.DefaultExt = '.json'
|
||||||
|
|
||||||
if ($openDialog.ShowDialog($Owner) -ne $true) { return }
|
if ($openDialog.ShowDialog($Owner) -ne $true) {
|
||||||
|
Write-Host 'Import file dialog canceled.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Importing configuration from '$($openDialog.FileName)'..."
|
||||||
|
|
||||||
$config = LoadJsonFile -filePath $openDialog.FileName -expectedVersion '1.0'
|
$config = LoadJsonFile -filePath $openDialog.FileName -expectedVersion '1.0'
|
||||||
if (-not $config) {
|
if (-not $config) {
|
||||||
Show-MessageBox -Message "Failed to read configuration file" -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null
|
Write-Error "Failed to read configuration file '$($openDialog.FileName)'"
|
||||||
|
Show-MessageBox -Message "Failed to read configuration file" -Title 'Invalid Config' -Button 'OK' -Icon 'Error' | Out-Null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not $config.Version) {
|
if (-not $config.Version) {
|
||||||
Show-MessageBox -Message "Invalid configuration file format." -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null
|
Write-Error "Invalid configuration file format: '$($openDialog.FileName)'"
|
||||||
|
Show-MessageBox -Message "Invalid configuration file format." -Title 'Invalid Config' -Button 'OK' -Icon 'Error' | Out-Null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
$availableCategories = Get-AvailableImportExportCategories -Config $config
|
$availableCategories = Get-AvailableImportExportCategories -Config $config
|
||||||
|
|
||||||
if ($availableCategories.Count -eq 0) {
|
if ($availableCategories.Count -eq 0) {
|
||||||
Show-MessageBox -Message "The configuration file contains no importable data." -Title 'Import Configuration' -Button 'OK' -Icon 'Information' | Out-Null
|
Write-Warning "Configuration file '$($openDialog.FileName)' contains no importable data."
|
||||||
|
Show-MessageBox -Message "The selected file contains no importable data." -Title 'Invalid Config' -Button 'OK' -Icon 'Error' | Out-Null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Write-Host "Available categories in config: $($availableCategories -join ', ')"
|
||||||
|
|
||||||
$appCount = @($config.Apps | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) }).Count
|
$appCount = @($config.Apps | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) }).Count
|
||||||
$tweakCount = @($config.Tweaks | Where-Object { $_ -and $_.Name -and $_.Value -eq $true }).Count
|
$tweakCount = @($config.Tweaks | Where-Object { $_ -and $_.Name -and $_.Value -eq $true }).Count
|
||||||
$categoryDetails = Build-CategoryDetails -AppCount $appCount -TweakCount $tweakCount -DeploymentSettings @($config.Deployment)
|
$categoryDetails = Build-CategoryDetails -AppCount $appCount -TweakCount $tweakCount -DeploymentSettings @($config.Deployment)
|
||||||
|
|
||||||
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Import Configuration' -Prompt 'Select which settings to import:' -Categories $availableCategories -CategoryDetails $categoryDetails
|
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Import Configuration' -Prompt 'Select the settings you wish to import. You can review and modify them before they are applied.' -Categories $availableCategories -CategoryDetails $categoryDetails -ActionLabel 'Import Settings'
|
||||||
if (-not $categories) { return }
|
if (-not $categories) {
|
||||||
|
Write-Host 'Import canceled.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if ($categories -contains 'Applications' -and $config.Apps) {
|
if ($categories -contains 'Applications' -and $config.Apps) {
|
||||||
$appIds = @(
|
$appIds = @(
|
||||||
@@ -464,6 +489,7 @@ function Import-Configuration {
|
|||||||
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
|
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Write-Host "Importing $($appIds.Count) app selection(s)."
|
||||||
Apply-ImportedApplications -AppsPanel $AppsPanel -AppIds $appIds
|
Apply-ImportedApplications -AppsPanel $AppsPanel -AppIds $appIds
|
||||||
|
|
||||||
if ($OnAppsImported) {
|
if ($OnAppsImported) {
|
||||||
@@ -471,12 +497,16 @@ function Import-Configuration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($categories -contains 'System Tweaks' -and $config.Tweaks) {
|
if ($categories -contains 'System Tweaks' -and $config.Tweaks) {
|
||||||
|
$tweakCount = @($config.Tweaks).Count
|
||||||
|
Write-Host "Importing $tweakCount tweak(s)."
|
||||||
Apply-ImportedTweakSettings -Owner $Owner -UiControlMappings $UiControlMappings -TweakSettings @($config.Tweaks)
|
Apply-ImportedTweakSettings -Owner $Owner -UiControlMappings $UiControlMappings -TweakSettings @($config.Tweaks)
|
||||||
}
|
}
|
||||||
if ($categories -contains 'Deployment Settings' -and $config.Deployment) {
|
if ($categories -contains 'Deployment Settings' -and $config.Deployment) {
|
||||||
|
Write-Host 'Importing deployment settings.'
|
||||||
Apply-ImportedDeploymentSettings -Owner $Owner -UserSelectionCombo $UserSelectionCombo -OtherUsernameTextBox $OtherUsernameTextBox -DeploymentSettings @($config.Deployment)
|
Apply-ImportedDeploymentSettings -Owner $Owner -UserSelectionCombo $UserSelectionCombo -OtherUsernameTextBox $OtherUsernameTextBox -DeploymentSettings @($config.Deployment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Write-Host 'Configuration imported successfully.'
|
||||||
Show-MessageBox -Message "Configuration imported successfully." -Title 'Import Configuration' -Button 'OK' -Icon 'Information' | Out-Null
|
Show-MessageBox -Message "Configuration imported successfully." -Title 'Import Configuration' -Button 'OK' -Icon 'Information' | Out-Null
|
||||||
|
|
||||||
if ($OnImportCompleted) {
|
if ($OnImportCompleted) {
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ function Show-MainWindow {
|
|||||||
$menuAbout = $window.FindName('MenuAbout')
|
$menuAbout = $window.FindName('MenuAbout')
|
||||||
$importConfigBtn = $window.FindName('ImportConfigBtn')
|
$importConfigBtn = $window.FindName('ImportConfigBtn')
|
||||||
$exportConfigBtn = $window.FindName('ExportConfigBtn')
|
$exportConfigBtn = $window.FindName('ExportConfigBtn')
|
||||||
|
$restoreBackupBtn = $window.FindName('RestoreBackupBtn')
|
||||||
|
|
||||||
$windowStateNormal = [System.Windows.WindowState]::Normal
|
$windowStateNormal = [System.Windows.WindowState]::Normal
|
||||||
$windowStateMaximized = [System.Windows.WindowState]::Maximized
|
$windowStateMaximized = [System.Windows.WindowState]::Maximized
|
||||||
@@ -198,7 +199,10 @@ function Show-MainWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$window.Add_SizeChanged({ & $updateContentMargin })
|
$window.Add_SizeChanged({
|
||||||
|
& $updateContentMargin
|
||||||
|
UpdateTweaksResponsiveColumns
|
||||||
|
})
|
||||||
|
|
||||||
$window.Add_StateChanged({
|
$window.Add_StateChanged({
|
||||||
& $updateWindowChrome
|
& $updateWindowChrome
|
||||||
@@ -231,7 +235,7 @@ function Show-MainWindow {
|
|||||||
})
|
})
|
||||||
|
|
||||||
$menuLogs.Add_Click({
|
$menuLogs.Add_Click({
|
||||||
$logsFolder = Join-Path $PSScriptRoot "../../Logs"
|
$logsFolder = Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) 'Logs'
|
||||||
if (Test-Path $logsFolder) {
|
if (Test-Path $logsFolder) {
|
||||||
Start-Process "explorer.exe" -ArgumentList $logsFolder
|
Start-Process "explorer.exe" -ArgumentList $logsFolder
|
||||||
}
|
}
|
||||||
@@ -244,12 +248,18 @@ function Show-MainWindow {
|
|||||||
Show-AboutDialog -Owner $window
|
Show-AboutDialog -Owner $window
|
||||||
})
|
})
|
||||||
|
|
||||||
# --- Import/Export Configuration ---
|
|
||||||
$exportConfigBtn.Add_Click({
|
$exportConfigBtn.Add_Click({
|
||||||
|
try {
|
||||||
Export-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox
|
Export-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Export configuration failed: $($_.Exception.Message)"
|
||||||
|
Show-MessageBox -Owner $window -Message "Unable to open export configuration dialog: $($_.Exception.Message)" -Title 'Export Configuration Failed' -Button 'OK' -Icon 'Error' | Out-Null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$importConfigBtn.Add_Click({
|
$importConfigBtn.Add_Click({
|
||||||
|
try {
|
||||||
Import-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -OnAppsImported { UpdateAppSelectionStatus; UpdatePresetStates } -OnImportCompleted {
|
Import-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -OnAppsImported { UpdateAppSelectionStatus; UpdatePresetStates } -OnImportCompleted {
|
||||||
$tabControl.SelectedIndex = 3
|
$tabControl.SelectedIndex = 3
|
||||||
UpdateNavigationButtons
|
UpdateNavigationButtons
|
||||||
@@ -258,8 +268,33 @@ function Show-MainWindow {
|
|||||||
Show-Bubble -TargetControl $reviewChangesBtn -Message 'View the selected changes here'
|
Show-Bubble -TargetControl $reviewChangesBtn -Message 'View the selected changes here'
|
||||||
}) | Out-Null
|
}) | Out-Null
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Import configuration failed: $($_.Exception.Message)"
|
||||||
|
Show-MessageBox -Owner $window -Message "Unable to open import configuration dialog: $($_.Exception.Message)" -Title 'Import Configuration Failed' -Button 'OK' -Icon 'Error' | Out-Null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if ($restoreBackupBtn) {
|
||||||
|
$restoreBackupBtn.Add_Click({
|
||||||
|
try {
|
||||||
|
$restoreResult = Show-RestoreBackupWindow -Owner $window
|
||||||
|
if ($restoreResult -and $restoreResult.RestoredRegistry -eq $true) {
|
||||||
|
RefreshCurrentTweakSystemState -ApplyToUi:$false
|
||||||
|
|
||||||
|
if ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -eq $true) {
|
||||||
|
ResetTweaksToSystemState -loadSystemState $true
|
||||||
|
UpdateTweakPresetStates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Restore backup action failed: $($_.Exception.Message)"
|
||||||
|
Show-MessageBox -Owner $window -Message "Unable to open restore backup dialog: $($_.Exception.Message)" -Title 'Restore Backup Failed' -Button 'OK' -Icon 'Error' | Out-Null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
$closeBtn.Add_Click({
|
$closeBtn.Add_Click({
|
||||||
$window.Close()
|
$window.Close()
|
||||||
})
|
})
|
||||||
@@ -349,6 +384,21 @@ function Show-MainWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ClearTweakSelections {
|
||||||
|
if (-not $script:UiControlMappings) { return }
|
||||||
|
|
||||||
|
foreach ($controlName in $script:UiControlMappings.Keys) {
|
||||||
|
$control = $window.FindName($controlName)
|
||||||
|
if ($control -is [System.Windows.Controls.CheckBox]) {
|
||||||
|
$control.IsChecked = $false
|
||||||
|
$control.IsEnabled = $true
|
||||||
|
}
|
||||||
|
elseif ($control -is [System.Windows.Controls.ComboBox]) {
|
||||||
|
$control.SelectedIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function AnimateDropdownArrow {
|
function AnimateDropdownArrow {
|
||||||
param(
|
param(
|
||||||
[System.Windows.Controls.TextBlock]$arrow,
|
[System.Windows.Controls.TextBlock]$arrow,
|
||||||
@@ -504,6 +554,19 @@ function Show-MainWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$appSelectionStatus.Text = "$selectedCount app(s) selected for removal"
|
$appSelectionStatus.Text = "$selectedCount app(s) selected for removal"
|
||||||
|
|
||||||
|
if ($appRemovalScopeCombo -and $appRemovalScopeSection -and $appRemovalScopeDescription) {
|
||||||
|
if ($selectedCount -gt 0) {
|
||||||
|
if ($userSelectionCombo.SelectedIndex -ne 2) {
|
||||||
|
$appRemovalScopeCombo.IsEnabled = $true
|
||||||
|
}
|
||||||
|
UpdateAppRemovalScopeDescription
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$appRemovalScopeCombo.IsEnabled = $false
|
||||||
|
$appRemovalScopeDescription.Text = "No apps selected for removal."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Applies a preset by checking/unchecking apps that match the given filter
|
# Applies a preset by checking/unchecking apps that match the given filter
|
||||||
@@ -582,6 +645,8 @@ function Show-MainWindow {
|
|||||||
|
|
||||||
$script:UiControlMappings = @{}
|
$script:UiControlMappings = @{}
|
||||||
$script:CategoryCardMap = @{}
|
$script:CategoryCardMap = @{}
|
||||||
|
$script:TweaksCompactMode = $null
|
||||||
|
$script:TweaksCardsMovedFromCol2 = @()
|
||||||
|
|
||||||
function CreateLabeledCombo($parent, $labelText, $comboName, $items) {
|
function CreateLabeledCombo($parent, $labelText, $comboName, $items) {
|
||||||
# If only 2 items (No Change + one option), use a checkbox instead
|
# If only 2 items (No Change + one option), use a checkbox instead
|
||||||
@@ -825,19 +890,25 @@ function Show-MainWindow {
|
|||||||
if ($feature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($feature.FeatureId -match '^Enable') { $opt = 'Enable' }
|
if ($feature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($feature.FeatureId -match '^Enable') { $opt = 'Enable' }
|
||||||
$items = @('No Change', $opt)
|
$items = @('No Change', $opt)
|
||||||
$comboName = ("Feature_{0}_Combo" -f $feature.FeatureId) -replace '[^a-zA-Z0-9_]',''
|
$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.Label -comboName $comboName -items $items
|
||||||
# attach tooltip from Features.json if present
|
# attach tooltip from Features.json if present, and include the disabled-state reason
|
||||||
if ($feature.ToolTip) {
|
if ($feature.ToolTip -or $feature.DisableWhenApplied -eq $true) {
|
||||||
|
$tooltipText = $feature.ToolTip
|
||||||
|
if ($feature.DisableWhenApplied -eq $true) {
|
||||||
|
$tooltipText = "This tweak is already applied and cannot be undone automatically. Visit the Win11Debloat wiki for instructions on how to manually revert this change."
|
||||||
|
}
|
||||||
|
|
||||||
$tipBlock = New-Object System.Windows.Controls.TextBlock
|
$tipBlock = New-Object System.Windows.Controls.TextBlock
|
||||||
$tipBlock.Text = $feature.ToolTip
|
$tipBlock.Text = $tooltipText
|
||||||
$tipBlock.TextWrapping = 'Wrap'
|
$tipBlock.TextWrapping = 'Wrap'
|
||||||
$tipBlock.MaxWidth = 420
|
$tipBlock.MaxWidth = 420
|
||||||
$combo.ToolTip = $tipBlock
|
$combo.ToolTip = $tipBlock
|
||||||
|
[System.Windows.Controls.ToolTipService]::SetShowOnDisabled($combo, $true)
|
||||||
$lblBorderObj = $null
|
$lblBorderObj = $null
|
||||||
try { $lblBorderObj = $window.FindName("$comboName`_LabelBorder") } catch {}
|
try { $lblBorderObj = $window.FindName("$comboName`_LabelBorder") } catch {}
|
||||||
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
|
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
|
||||||
}
|
}
|
||||||
$script:UiControlMappings[$comboName] = @{ Type='feature'; FeatureId = $feature.FeatureId; Action = $feature.Action; Label = $feature.Label; Category = $categoryName }
|
$script:UiControlMappings[$comboName] = @{ Type='feature'; FeatureId = $feature.FeatureId; Label = $feature.Label; Category = $categoryName }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -845,10 +916,69 @@ function Show-MainWindow {
|
|||||||
# Build a feature-label lookup so GenerateOverview can resolve feature IDs without reloading JSON
|
# Build a feature-label lookup so GenerateOverview can resolve feature IDs without reloading JSON
|
||||||
$script:FeatureLabelLookup = @{}
|
$script:FeatureLabelLookup = @{}
|
||||||
foreach ($f in $featuresJson.Features) {
|
foreach ($f in $featuresJson.Features) {
|
||||||
$script:FeatureLabelLookup[$f.FeatureId] = $f.Action + ' ' + $f.Label
|
$script:FeatureLabelLookup[$f.FeatureId] = $f.Label
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RefreshCurrentTweakSystemState {
|
||||||
|
param([bool]$ApplyToUi)
|
||||||
|
|
||||||
|
if (-not $script:UiControlMappings) { return }
|
||||||
|
if (-not $script:Features) { return }
|
||||||
|
|
||||||
|
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0"
|
||||||
|
if (-not $featuresJson) { return }
|
||||||
|
|
||||||
|
$groupMap = @{}
|
||||||
|
if ($featuresJson.UiGroups) {
|
||||||
|
foreach ($g in $featuresJson.UiGroups) {
|
||||||
|
$groupMap[$g.GroupId] = $g
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($controlName in $script:UiControlMappings.Keys) {
|
||||||
|
$control = $window.FindName($controlName)
|
||||||
|
if (-not $control) { continue }
|
||||||
|
$mapping = $script:UiControlMappings[$controlName]
|
||||||
|
|
||||||
|
if ($control -is [System.Windows.Controls.CheckBox] -and $mapping.Type -eq 'feature') {
|
||||||
|
$applied = $false
|
||||||
|
try { $applied = [bool](Test-FeatureApplied -FeatureId $mapping.FeatureId) } catch {}
|
||||||
|
$featureObj = $script:Features[$mapping.FeatureId]
|
||||||
|
$disableWhenApplied = $featureObj -and $featureObj.DisableWhenApplied -eq $true
|
||||||
|
Add-Member -InputObject $control -MemberType NoteProperty -Name 'SystemState' -Value $applied -Force
|
||||||
|
Add-Member -InputObject $control -MemberType NoteProperty -Name 'DisableWhenApplied' -Value $disableWhenApplied -Force
|
||||||
|
|
||||||
|
if ($ApplyToUi) {
|
||||||
|
$control.IsChecked = $applied
|
||||||
|
$control.IsEnabled = -not ($applied -and $disableWhenApplied)
|
||||||
|
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialState' -Value $applied -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($control -is [System.Windows.Controls.ComboBox] -and $mapping.Type -eq 'group') {
|
||||||
|
$groupId = $null
|
||||||
|
if ($controlName -match '^Group_(.+)Combo$') { $groupId = $matches[1] }
|
||||||
|
$activeIndex = 0
|
||||||
|
if ($groupId -and $groupMap.ContainsKey($groupId)) {
|
||||||
|
try { $activeIndex = Get-CurrentGroupActiveIndex -Group $groupMap[$groupId] } catch {}
|
||||||
|
}
|
||||||
|
Add-Member -InputObject $control -MemberType NoteProperty -Name 'SystemIndex' -Value $activeIndex -Force
|
||||||
|
|
||||||
|
if ($ApplyToUi) {
|
||||||
|
$control.SelectedIndex = $activeIndex
|
||||||
|
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialIndex' -Value $activeIndex -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reads current registry state and sets each tweak control to reflect whether that tweak is
|
||||||
|
# currently applied. Also stores the initial state on each control as a NoteProperty so the
|
||||||
|
# apply handler can detect which controls actually changed.
|
||||||
|
function LoadCurrentTweakStateIntoUI {
|
||||||
|
RefreshCurrentTweakSystemState -ApplyToUi:$true
|
||||||
|
}
|
||||||
|
|
||||||
# Helper function to load apps and populate the app list panel
|
# Helper function to load apps and populate the app list panel
|
||||||
function script:LoadAppsWithList($listOfApps) {
|
function script:LoadAppsWithList($listOfApps) {
|
||||||
$script:MainWindowLastSelectedCheckbox = $null
|
$script:MainWindowLastSelectedCheckbox = $null
|
||||||
@@ -1242,6 +1372,123 @@ function Show-MainWindow {
|
|||||||
$col0 = $window.FindName('Column0Panel')
|
$col0 = $window.FindName('Column0Panel')
|
||||||
$col1 = $window.FindName('Column1Panel')
|
$col1 = $window.FindName('Column1Panel')
|
||||||
$col2 = $window.FindName('Column2Panel')
|
$col2 = $window.FindName('Column2Panel')
|
||||||
|
$ShowCurrentlyAppliedTweaksCheckBox = $window.FindName('ShowCurrentlyAppliedTweaksCheckBox')
|
||||||
|
|
||||||
|
# Loads the currently applied tweaks from registry state into UI controls.
|
||||||
|
# When checkbox is checked: sets controls to their currently applied state
|
||||||
|
# When checkbox is unchecked: clears all control selections
|
||||||
|
function ResetTweaksToSystemState {
|
||||||
|
param ([bool]$loadSystemState)
|
||||||
|
|
||||||
|
if (-not $script:UiControlMappings) { return }
|
||||||
|
|
||||||
|
foreach ($controlName in $script:UiControlMappings.Keys) {
|
||||||
|
$control = $window.FindName($controlName)
|
||||||
|
if (-not $control) { continue }
|
||||||
|
|
||||||
|
if ($control -is [System.Windows.Controls.CheckBox]) {
|
||||||
|
if ($loadSystemState) {
|
||||||
|
# Set checkbox to the currently applied state from registry
|
||||||
|
$applied = if ($null -ne $control.PSObject.Properties['SystemState']) { [bool]$control.SystemState } else { $false }
|
||||||
|
$disableWhenApplied = $null -ne $control.PSObject.Properties['DisableWhenApplied'] -and [bool]$control.DisableWhenApplied
|
||||||
|
$control.IsChecked = $applied
|
||||||
|
$control.IsEnabled = -not ($applied -and $disableWhenApplied)
|
||||||
|
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialState' -Value $applied -Force
|
||||||
|
} else {
|
||||||
|
# Clear the checkbox
|
||||||
|
$control.IsChecked = $false
|
||||||
|
$control.IsEnabled = $true
|
||||||
|
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialState' -Value $false -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($control -is [System.Windows.Controls.ComboBox]) {
|
||||||
|
if ($loadSystemState) {
|
||||||
|
# Set combobox to the currently applied state from registry
|
||||||
|
$idx = if ($null -ne $control.PSObject.Properties['SystemIndex']) { [int]$control.SystemIndex } else { 0 }
|
||||||
|
$control.SelectedIndex = $idx
|
||||||
|
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialIndex' -Value $idx -Force
|
||||||
|
} else {
|
||||||
|
# Reset to first item (No Change)
|
||||||
|
$control.SelectedIndex = 0
|
||||||
|
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialIndex' -Value 0 -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:SuppressAppliedTweaksUserSync = $false
|
||||||
|
|
||||||
|
function UpdateAppliedTweaksUserModeState {
|
||||||
|
$showAppliedTweaksMode = ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -eq $true)
|
||||||
|
|
||||||
|
if ($showAppliedTweaksMode) {
|
||||||
|
if ($userSelectionCombo.SelectedIndex -ne 0 -and -not $script:SuppressAppliedTweaksUserSync) {
|
||||||
|
$script:SuppressAppliedTweaksUserSync = $true
|
||||||
|
try {
|
||||||
|
$userSelectionCombo.SelectedIndex = 0
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$script:SuppressAppliedTweaksUserSync = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$userSelectionCombo.IsEnabled = $false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($script:Params.ContainsKey('Sysprep') -or $script:Params.ContainsKey('User')) {
|
||||||
|
$userSelectionCombo.IsEnabled = $false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$userSelectionCombo.IsEnabled = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpdateTweaksResponsiveColumns {
|
||||||
|
if (-not $tweaksGrid -or -not $col0 -or -not $col1 -or -not $col2) { return }
|
||||||
|
if ($tweaksGrid.ColumnDefinitions.Count -lt 3) { return }
|
||||||
|
if ($null -eq $script:TweaksCardsMovedFromCol2) { $script:TweaksCardsMovedFromCol2 = @() }
|
||||||
|
|
||||||
|
$useTwoColumns = $window.ActualWidth -lt 1200
|
||||||
|
if ($script:TweaksCompactMode -eq $useTwoColumns) { return }
|
||||||
|
$script:TweaksCompactMode = $useTwoColumns
|
||||||
|
|
||||||
|
if ($useTwoColumns) {
|
||||||
|
$tweaksGrid.ColumnDefinitions[0].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star)
|
||||||
|
$tweaksGrid.ColumnDefinitions[1].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star)
|
||||||
|
$tweaksGrid.ColumnDefinitions[2].Width = [System.Windows.GridLength]::new(0)
|
||||||
|
$col2.Visibility = 'Collapsed'
|
||||||
|
|
||||||
|
# Move third-column cards once when entering compact mode.
|
||||||
|
$cardsToMove = @($col2.Children) | Where-Object { $_ -is [System.Windows.UIElement] }
|
||||||
|
$script:TweaksCardsMovedFromCol2 = @($cardsToMove)
|
||||||
|
$col2.Children.Clear()
|
||||||
|
$targetColumns = @($col0, $col1)
|
||||||
|
foreach ($card in $cardsToMove) {
|
||||||
|
$target = $targetColumns |
|
||||||
|
Sort-Object @{Expression={$_.Children.Count}; Ascending=$true}, @{Expression={$targetColumns.IndexOf($_)}; Ascending=$true} |
|
||||||
|
Select-Object -First 1
|
||||||
|
$target.Children.Add($card) | Out-Null
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$tweaksGrid.ColumnDefinitions[0].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star)
|
||||||
|
$tweaksGrid.ColumnDefinitions[1].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star)
|
||||||
|
$tweaksGrid.ColumnDefinitions[2].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star)
|
||||||
|
$col2.Visibility = 'Visible'
|
||||||
|
|
||||||
|
foreach ($card in (@($script:TweaksCardsMovedFromCol2) | Where-Object { $_ -is [System.Windows.UIElement] })) {
|
||||||
|
if ($col0.Children.Contains($card)) {
|
||||||
|
$col0.Children.Remove($card) | Out-Null
|
||||||
|
}
|
||||||
|
elseif ($col1.Children.Contains($card)) {
|
||||||
|
$col1.Children.Remove($card) | Out-Null
|
||||||
|
}
|
||||||
|
$col2.Children.Add($card) | Out-Null
|
||||||
|
}
|
||||||
|
$script:TweaksCardsMovedFromCol2 = @()
|
||||||
|
}
|
||||||
|
|
||||||
# Monitor scrollbar visibility and adjust searchbar margin
|
# Monitor scrollbar visibility and adjust searchbar margin
|
||||||
$tweaksScrollViewer.Add_ScrollChanged({
|
$tweaksScrollViewer.Add_ScrollChanged({
|
||||||
@@ -1338,6 +1585,12 @@ function Show-MainWindow {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Only show changed settings checkbox
|
||||||
|
if ($ShowCurrentlyAppliedTweaksCheckBox) {
|
||||||
|
$ShowCurrentlyAppliedTweaksCheckBox.Add_Checked({ ResetTweaksToSystemState -loadSystemState $true; UpdateAppliedTweaksUserModeState })
|
||||||
|
$ShowCurrentlyAppliedTweaksCheckBox.Add_Unchecked({ ResetTweaksToSystemState -loadSystemState $false; UpdateAppliedTweaksUserModeState })
|
||||||
|
}
|
||||||
|
|
||||||
# Add Ctrl+F keyboard shortcut to focus search box on current tab
|
# Add Ctrl+F keyboard shortcut to focus search box on current tab
|
||||||
$window.Add_KeyDown({
|
$window.Add_KeyDown({
|
||||||
param($sourceControl, $e)
|
param($sourceControl, $e)
|
||||||
@@ -1436,6 +1689,17 @@ function Show-MainWindow {
|
|||||||
|
|
||||||
# Update user selection description and show/hide other user panel
|
# Update user selection description and show/hide other user panel
|
||||||
$userSelectionCombo.Add_SelectionChanged({
|
$userSelectionCombo.Add_SelectionChanged({
|
||||||
|
if ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -eq $true -and $userSelectionCombo.SelectedIndex -ne 0 -and -not $script:SuppressAppliedTweaksUserSync) {
|
||||||
|
$script:SuppressAppliedTweaksUserSync = $true
|
||||||
|
try {
|
||||||
|
$userSelectionCombo.SelectedIndex = 0
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$script:SuppressAppliedTweaksUserSync = $false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch ($userSelectionCombo.SelectedIndex) {
|
switch ($userSelectionCombo.SelectedIndex) {
|
||||||
0 {
|
0 {
|
||||||
$userSelectionDescription.Text = "Changes will be applied to the currently logged-in user profile."
|
$userSelectionDescription.Text = "Changes will be applied to the currently logged-in user profile."
|
||||||
@@ -1444,8 +1708,6 @@ function Show-MainWindow {
|
|||||||
# Show "Current user only" option, hide "Target user only" option
|
# Show "Current user only" option, hide "Target user only" option
|
||||||
$appRemovalScopeCurrentUser.Visibility = 'Visible'
|
$appRemovalScopeCurrentUser.Visibility = 'Visible'
|
||||||
$appRemovalScopeTargetUser.Visibility = 'Collapsed'
|
$appRemovalScopeTargetUser.Visibility = 'Collapsed'
|
||||||
# Enable app removal scope selection for current user
|
|
||||||
$appRemovalScopeCombo.IsEnabled = $true
|
|
||||||
$appRemovalScopeCombo.SelectedIndex = 0
|
$appRemovalScopeCombo.SelectedIndex = 0
|
||||||
}
|
}
|
||||||
1 {
|
1 {
|
||||||
@@ -1455,8 +1717,6 @@ function Show-MainWindow {
|
|||||||
# Hide "Current user only" option, show "Target user only" option
|
# Hide "Current user only" option, show "Target user only" option
|
||||||
$appRemovalScopeCurrentUser.Visibility = 'Collapsed'
|
$appRemovalScopeCurrentUser.Visibility = 'Collapsed'
|
||||||
$appRemovalScopeTargetUser.Visibility = 'Visible'
|
$appRemovalScopeTargetUser.Visibility = 'Visible'
|
||||||
# Enable app removal scope selection for other user
|
|
||||||
$appRemovalScopeCombo.IsEnabled = $true
|
|
||||||
$appRemovalScopeCombo.SelectedIndex = 0
|
$appRemovalScopeCombo.SelectedIndex = 0
|
||||||
}
|
}
|
||||||
2 {
|
2 {
|
||||||
@@ -1467,10 +1727,13 @@ function Show-MainWindow {
|
|||||||
$appRemovalScopeCurrentUser.Visibility = 'Collapsed'
|
$appRemovalScopeCurrentUser.Visibility = 'Collapsed'
|
||||||
$appRemovalScopeTargetUser.Visibility = 'Collapsed'
|
$appRemovalScopeTargetUser.Visibility = 'Collapsed'
|
||||||
# Lock app removal scope to "All users" when applying to sysprep
|
# Lock app removal scope to "All users" when applying to sysprep
|
||||||
$appRemovalScopeCombo.IsEnabled = $false
|
|
||||||
$appRemovalScopeCombo.SelectedIndex = 0
|
$appRemovalScopeCombo.SelectedIndex = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Keep enabled/disabled state in sync with both app selection and user mode.
|
||||||
|
UpdateAppSelectionStatus
|
||||||
|
UpdateAppliedTweaksUserModeState
|
||||||
})
|
})
|
||||||
|
|
||||||
# Helper function to update app removal scope description
|
# Helper function to update app removal scope description
|
||||||
@@ -1513,44 +1776,116 @@ function Show-MainWindow {
|
|||||||
return $true
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
$username = $otherUsernameTextBox.Text.Trim()
|
|
||||||
|
|
||||||
$errorBrush = $window.Resources['ValidationErrorColor']
|
$errorBrush = $window.Resources['ValidationErrorColor']
|
||||||
$successBrush = $window.Resources['ValidationSuccessColor']
|
$successBrush = $window.Resources['ValidationSuccessColor']
|
||||||
|
$validationResult = Test-TargetUserName -UserName $otherUsernameTextBox.Text
|
||||||
|
|
||||||
if ($username.Length -eq 0) {
|
$usernameValidationMessage.Text = $validationResult.Message
|
||||||
$usernameValidationMessage.Text = "Please enter a username"
|
if ($validationResult.IsValid) {
|
||||||
$usernameValidationMessage.Foreground = $errorBrush
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($username -eq $env:USERNAME) {
|
|
||||||
$usernameValidationMessage.Text = "Cannot enter your own username, use 'Current User' option instead"
|
|
||||||
$usernameValidationMessage.Foreground = $errorBrush
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
$userExists = CheckIfUserExists -Username $username
|
|
||||||
|
|
||||||
if ($userExists) {
|
|
||||||
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
|
$usernameValidationMessage.Foreground = $successBrush
|
||||||
return $true
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
$usernameValidationMessage.Text = "User not found, please enter a valid username"
|
|
||||||
$usernameValidationMessage.Foreground = $errorBrush
|
$usernameValidationMessage.Foreground = $errorBrush
|
||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-FeatureLabel {
|
||||||
|
param(
|
||||||
|
[string]$FeatureId,
|
||||||
|
$FallbackLabel = $null
|
||||||
|
)
|
||||||
|
|
||||||
|
$label = $script:FeatureLabelLookup[$FeatureId]
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace([string]$label)) {
|
||||||
|
return [string]$label
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace([string]$FallbackLabel)) {
|
||||||
|
return [string]$FallbackLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
return [string]$FeatureId
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PendingTweakActions {
|
||||||
|
param(
|
||||||
|
[bool]$ShowAppliedTweaksMode
|
||||||
|
)
|
||||||
|
|
||||||
|
$actions = New-Object System.Collections.Generic.List[object]
|
||||||
|
|
||||||
|
if (-not $script:UiControlMappings) {
|
||||||
|
return @($actions.ToArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($mappingKey in $script:UiControlMappings.Keys) {
|
||||||
|
$control = $window.FindName($mappingKey)
|
||||||
|
if (-not $control) { continue }
|
||||||
|
$mapping = $script:UiControlMappings[$mappingKey]
|
||||||
|
|
||||||
|
if ($control -is [System.Windows.Controls.CheckBox] -and $mapping.Type -eq 'feature') {
|
||||||
|
$wasApplied = $false
|
||||||
|
if ($ShowAppliedTweaksMode -and $null -ne $control.PSObject.Properties['SystemState']) {
|
||||||
|
$wasApplied = [bool]$control.SystemState
|
||||||
|
}
|
||||||
|
elseif ($null -ne $control.PSObject.Properties['InitialState']) {
|
||||||
|
$wasApplied = [bool]$control.InitialState
|
||||||
|
}
|
||||||
|
elseif ($null -ne $control.PSObject.Properties['SystemState']) {
|
||||||
|
$wasApplied = [bool]$control.SystemState
|
||||||
|
}
|
||||||
|
$isNowChecked = $control.IsChecked -eq $true
|
||||||
|
|
||||||
|
if (-not $wasApplied -and $isNowChecked) {
|
||||||
|
$actions.Add([PSCustomObject]@{
|
||||||
|
Action = 'Apply'
|
||||||
|
FeatureId = [string]$mapping.FeatureId
|
||||||
|
Label = (Get-FeatureLabel -FeatureId $mapping.FeatureId -FallbackLabel $mapping.Label)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
elseif ($wasApplied -and -not $isNowChecked) {
|
||||||
|
$actions.Add([PSCustomObject]@{
|
||||||
|
Action = 'Undo'
|
||||||
|
FeatureId = [string]$mapping.FeatureId
|
||||||
|
Label = (Get-FeatureLabel -FeatureId $mapping.FeatureId -FallbackLabel $mapping.Label)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($control -is [System.Windows.Controls.ComboBox] -and $mapping.Type -eq 'group') {
|
||||||
|
$wasIndex = 0
|
||||||
|
if ($ShowAppliedTweaksMode -and $null -ne $control.PSObject.Properties['SystemIndex']) {
|
||||||
|
$wasIndex = [int]$control.SystemIndex
|
||||||
|
}
|
||||||
|
elseif ($null -ne $control.PSObject.Properties['InitialIndex']) {
|
||||||
|
$wasIndex = [int]$control.InitialIndex
|
||||||
|
}
|
||||||
|
elseif ($null -ne $control.PSObject.Properties['SystemIndex']) {
|
||||||
|
$wasIndex = [int]$control.SystemIndex
|
||||||
|
}
|
||||||
|
$isNowIndex = $control.SelectedIndex
|
||||||
|
|
||||||
|
if ($wasIndex -eq $isNowIndex) { continue }
|
||||||
|
|
||||||
|
if ($isNowIndex -gt 0 -and $isNowIndex -le $mapping.Values.Count) {
|
||||||
|
$selectedValue = $mapping.Values[$isNowIndex - 1]
|
||||||
|
foreach ($fid in $selectedValue.FeatureIds) {
|
||||||
|
$actions.Add([PSCustomObject]@{
|
||||||
|
Action = 'Apply'
|
||||||
|
FeatureId = [string]$fid
|
||||||
|
Label = (Get-FeatureLabel -FeatureId $fid)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return @($actions.ToArray())
|
||||||
|
}
|
||||||
|
|
||||||
function GenerateOverview {
|
function GenerateOverview {
|
||||||
$changesList = @()
|
$changesList = @()
|
||||||
|
$showAppliedTweaksMode = ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -eq $true)
|
||||||
|
|
||||||
# Collect selected apps
|
# Collect selected apps
|
||||||
$selectedAppsCount = 0
|
$selectedAppsCount = 0
|
||||||
@@ -1563,52 +1898,14 @@ function Show-MainWindow {
|
|||||||
$changesList += "Remove $selectedAppsCount application(s)"
|
$changesList += "Remove $selectedAppsCount application(s)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Update app removal scope section based on whether apps are selected
|
UpdateAppSelectionStatus
|
||||||
if ($selectedAppsCount -gt 0) {
|
|
||||||
# Enable app removal scope selection (unless locked by sysprep mode)
|
foreach ($tweakAction in @(Get-PendingTweakActions -ShowAppliedTweaksMode:$showAppliedTweaksMode)) {
|
||||||
if ($userSelectionCombo.SelectedIndex -ne 2) {
|
if ($tweakAction.Action -eq 'Undo') {
|
||||||
$appRemovalScopeCombo.IsEnabled = $true
|
$changesList += "Undo: $($tweakAction.Label)"
|
||||||
}
|
|
||||||
$appRemovalScopeSection.Opacity = 1.0
|
|
||||||
UpdateAppRemovalScopeDescription
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
# Disable app removal scope selection when no apps selected
|
$changesList += $tweakAction.Label
|
||||||
$appRemovalScopeCombo.IsEnabled = $false
|
|
||||||
$appRemovalScopeSection.Opacity = 0.5
|
|
||||||
$appRemovalScopeDescription.Text = "No apps selected for removal."
|
|
||||||
}
|
|
||||||
|
|
||||||
# Collect all ComboBox/CheckBox selections from dynamically created controls
|
|
||||||
if ($script:UiControlMappings) {
|
|
||||||
foreach ($mappingKey in $script:UiControlMappings.Keys) {
|
|
||||||
$control = $window.FindName($mappingKey)
|
|
||||||
$isSelected = $false
|
|
||||||
|
|
||||||
# Check if it's a checkbox or combobox
|
|
||||||
if ($control -is [System.Windows.Controls.CheckBox]) {
|
|
||||||
$isSelected = $control.IsChecked -eq $true
|
|
||||||
}
|
|
||||||
elseif ($control -is [System.Windows.Controls.ComboBox]) {
|
|
||||||
$isSelected = $control.SelectedIndex -gt 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($control -and $isSelected) {
|
|
||||||
$mapping = $script:UiControlMappings[$mappingKey]
|
|
||||||
if ($mapping.Type -eq 'group') {
|
|
||||||
# For combobox: SelectedIndex 0 = No Change, so subtract 1 to index into Values
|
|
||||||
$selectedValue = $mapping.Values[$control.SelectedIndex - 1]
|
|
||||||
foreach ($fid in $selectedValue.FeatureIds) {
|
|
||||||
$label = $script:FeatureLabelLookup[$fid]
|
|
||||||
if ($label) { $changesList += $label }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elseif ($mapping.Type -eq 'feature') {
|
|
||||||
$label = $script:FeatureLabelLookup[$mapping.FeatureId]
|
|
||||||
if (-not $label) { $label = $mapping.Action + ' ' + $mapping.Label }
|
|
||||||
$changesList += $label
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1653,6 +1950,10 @@ function Show-MainWindow {
|
|||||||
# Handle Home Default Mode button - apply defaults and navigate directly to overview
|
# Handle Home Default Mode button - apply defaults and navigate directly to overview
|
||||||
$homeDefaultModeBtn = $window.FindName('HomeDefaultModeBtn')
|
$homeDefaultModeBtn = $window.FindName('HomeDefaultModeBtn')
|
||||||
$homeDefaultModeBtn.Add_Click({
|
$homeDefaultModeBtn.Add_Click({
|
||||||
|
if ($ShowCurrentlyAppliedTweaksCheckBox) {
|
||||||
|
$ShowCurrentlyAppliedTweaksCheckBox.IsChecked = $false
|
||||||
|
}
|
||||||
|
|
||||||
# Load and apply default settings
|
# Load and apply default settings
|
||||||
$defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
|
$defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
|
||||||
if ($defaultsJson) {
|
if ($defaultsJson) {
|
||||||
@@ -1699,6 +2000,9 @@ function Show-MainWindow {
|
|||||||
|
|
||||||
Hide-Bubble -Immediate
|
Hide-Bubble -Immediate
|
||||||
|
|
||||||
|
$showAppliedTweaksMode = ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -eq $true)
|
||||||
|
$selectedForwardFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
|
||||||
# App Removal - collect selected apps from integrated UI
|
# App Removal - collect selected apps from integrated UI
|
||||||
$selectedApps = @()
|
$selectedApps = @()
|
||||||
foreach ($child in $appsPanel.Children) {
|
foreach ($child in $appsPanel.Children) {
|
||||||
@@ -1707,6 +2011,7 @@ function Show-MainWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$selectedApps = @($selectedApps | Where-Object { $_ } | Select-Object -Unique)
|
$selectedApps = @($selectedApps | Where-Object { $_ } | Select-Object -Unique)
|
||||||
|
$hasAppSelection = ($selectedApps.Count -gt 0)
|
||||||
|
|
||||||
if ($selectedApps.Count -gt 0) {
|
if ($selectedApps.Count -gt 0) {
|
||||||
# Check if Microsoft Store is selected
|
# Check if Microsoft Store is selected
|
||||||
@@ -1739,56 +2044,18 @@ function Show-MainWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Apply dynamic tweaks selections
|
# Apply dynamic tweaks - only controls that changed from their current baseline state
|
||||||
if ($script:UiControlMappings) {
|
foreach ($tweakAction in @(Get-PendingTweakActions -ShowAppliedTweaksMode:$showAppliedTweaksMode)) {
|
||||||
foreach ($mappingKey in $script:UiControlMappings.Keys) {
|
if ($tweakAction.Action -eq 'Apply') {
|
||||||
$control = $window.FindName($mappingKey)
|
AddParameter $tweakAction.FeatureId
|
||||||
$isSelected = $false
|
$null = $selectedForwardFeatureIds.Add([string]$tweakAction.FeatureId)
|
||||||
$selectedIndex = 0
|
continue
|
||||||
|
|
||||||
# Check if it's a checkbox or combobox
|
|
||||||
if ($control -is [System.Windows.Controls.CheckBox]) {
|
|
||||||
$isSelected = $control.IsChecked -eq $true
|
|
||||||
$selectedIndex = if ($isSelected) { 1 } else { 0 }
|
|
||||||
}
|
|
||||||
elseif ($control -is [System.Windows.Controls.ComboBox]) {
|
|
||||||
$isSelected = $control.SelectedIndex -gt 0
|
|
||||||
$selectedIndex = $control.SelectedIndex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($control -and $isSelected) {
|
$script:UndoParams[[string]$tweakAction.FeatureId] = $true
|
||||||
$mapping = $script:UiControlMappings[$mappingKey]
|
|
||||||
if ($mapping.Type -eq 'group') {
|
|
||||||
if ($selectedIndex -gt 0 -and $selectedIndex -le $mapping.Values.Count) {
|
|
||||||
$selectedValue = $mapping.Values[$selectedIndex - 1]
|
|
||||||
foreach ($fid in $selectedValue.FeatureIds) {
|
|
||||||
AddParameter $fid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elseif ($mapping.Type -eq 'feature') {
|
|
||||||
AddParameter $mapping.FeatureId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$controlParamsCount = 0
|
if (-not $hasAppSelection -and $selectedForwardFeatureIds.Count -eq 0 -and $script:UndoParams.Count -eq 0) {
|
||||||
foreach ($Param in $script:ControlParams) {
|
|
||||||
if ($script:Params.ContainsKey($Param)) {
|
|
||||||
$controlParamsCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check if any changes were selected
|
|
||||||
$totalChanges = $script:Params.Count - $controlParamsCount
|
|
||||||
|
|
||||||
# Apps parameter does not count as a change itself
|
|
||||||
if ($script:Params.ContainsKey('Apps')) {
|
|
||||||
$totalChanges = $totalChanges - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($totalChanges -eq 0) {
|
|
||||||
Show-MessageBox -Message 'No changes have been selected, please select at least one option to proceed.' -Title 'No Changes Selected' -Button 'OK' -Icon 'Information'
|
Show-MessageBox -Message 'No changes have been selected, please select at least one option to proceed.' -Title 'No Changes Selected' -Button 'OK' -Icon 'Information'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1830,6 +2097,8 @@ function Show-MainWindow {
|
|||||||
# Initialize UI elements on window load
|
# Initialize UI elements on window load
|
||||||
$window.Add_Loaded({
|
$window.Add_Loaded({
|
||||||
BuildDynamicTweaks
|
BuildDynamicTweaks
|
||||||
|
LoadCurrentTweakStateIntoUI
|
||||||
|
UpdateTweaksResponsiveColumns
|
||||||
RefreshTweakPresetSources -defaultSettingsJson $defaultsJson -lastUsedSettingsJson $lastUsedSettingsJson
|
RefreshTweakPresetSources -defaultSettingsJson $defaultsJson -lastUsedSettingsJson $lastUsedSettingsJson
|
||||||
RegisterTweakPresetControlStateHandlers
|
RegisterTweakPresetControlStateHandlers
|
||||||
UpdateTweakPresetStates
|
UpdateTweakPresetStates
|
||||||
@@ -1863,6 +2132,7 @@ function Show-MainWindow {
|
|||||||
$otherUsernameTextBox.IsEnabled = $false
|
$otherUsernameTextBox.IsEnabled = $false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateAppliedTweaksUserModeState
|
||||||
UpdateNavigationButtons
|
UpdateNavigationButtons
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2159,18 +2429,11 @@ function Show-MainWindow {
|
|||||||
# Clear All Tweaks button
|
# Clear All Tweaks button
|
||||||
$clearAllTweaksBtn = $window.FindName('ClearAllTweaksBtn')
|
$clearAllTweaksBtn = $window.FindName('ClearAllTweaksBtn')
|
||||||
$clearAllTweaksBtn.Add_Click({
|
$clearAllTweaksBtn.Add_Click({
|
||||||
# Reset all ComboBoxes to index 0 (No Change) and uncheck all CheckBoxes
|
if ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -eq $true) {
|
||||||
if ($script:UiControlMappings) {
|
# Keep the toggle state aligned with the cleared UI selection state.
|
||||||
foreach ($comboName in $script:UiControlMappings.Keys) {
|
$ShowCurrentlyAppliedTweaksCheckBox.IsChecked = $false
|
||||||
$control = $window.FindName($comboName)
|
|
||||||
if ($control -is [System.Windows.Controls.CheckBox]) {
|
|
||||||
$control.IsChecked = $false
|
|
||||||
}
|
|
||||||
elseif ($control -is [System.Windows.Controls.ComboBox]) {
|
|
||||||
$control.SelectedIndex = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
ClearTweakSelections
|
||||||
UpdateTweakPresetStates
|
UpdateTweakPresetStates
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function Show-MessageBox {
|
|||||||
[string]$Button = 'OK',
|
[string]$Button = 'OK',
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[ValidateSet('None', 'Information', 'Warning', 'Error', 'Question')]
|
[ValidateSet('None', 'Information', 'Success', 'Warning', 'Error', 'Question')]
|
||||||
[string]$Icon = 'None',
|
[string]$Icon = 'None',
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
@@ -90,6 +90,11 @@ function Show-MessageBox {
|
|||||||
$iconText.Foreground = $msgWindow.FindResource('InformationIconColor')
|
$iconText.Foreground = $msgWindow.FindResource('InformationIconColor')
|
||||||
$iconText.Visibility = 'Visible'
|
$iconText.Visibility = 'Visible'
|
||||||
}
|
}
|
||||||
|
'Success' {
|
||||||
|
$iconText.Text = [char]0xE73E
|
||||||
|
$iconText.Foreground = $msgWindow.FindResource('SuccessIconColor')
|
||||||
|
$iconText.Visibility = 'Visible'
|
||||||
|
}
|
||||||
'Warning' {
|
'Warning' {
|
||||||
$iconText.Text = [char]0xE7BA
|
$iconText.Text = [char]0xE7BA
|
||||||
$iconText.Foreground = $msgWindow.FindResource('WarningIconColor')
|
$iconText.Foreground = $msgWindow.FindResource('WarningIconColor')
|
||||||
|
|||||||
431
Scripts/GUI/Show-RestoreBackupDialog.ps1
Normal file
431
Scripts/GUI/Show-RestoreBackupDialog.ps1
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
function Show-RestoreBackupDialog {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[System.Windows.Window]$Owner = $null
|
||||||
|
)
|
||||||
|
|
||||||
|
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
|
||||||
|
|
||||||
|
$usesDarkMode = GetSystemUsesDarkMode
|
||||||
|
$ownerWindow = if ($Owner) { $Owner } else { $script:GuiWindow }
|
||||||
|
|
||||||
|
$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 { }
|
||||||
|
}
|
||||||
|
|
||||||
|
$schemaPath = $script:RestoreBackupWindowSchema
|
||||||
|
if (-not $schemaPath -or -not (Test-Path $schemaPath)) {
|
||||||
|
throw 'Restore backup window schema file could not be found.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$xaml = Get-Content -Path $schemaPath -Raw
|
||||||
|
|
||||||
|
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
|
||||||
|
try {
|
||||||
|
$window = [System.Windows.Markup.XamlReader]::Load($reader)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$reader.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ownerWindow) {
|
||||||
|
try {
|
||||||
|
$window.Owner = $ownerWindow
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
SetWindowThemeResources -window $window -usesDarkMode $usesDarkMode
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
$titleBar = $window.FindName('TitleBar')
|
||||||
|
$titleText = $window.FindName('TitleText')
|
||||||
|
$closeBtn = $window.FindName('CloseBtn')
|
||||||
|
$backBtn = $window.FindName('BackBtn')
|
||||||
|
$primaryActionBtn = $window.FindName('PrimaryActionBtn')
|
||||||
|
$chooseRegistryBtn = $window.FindName('ChooseRegistryBtn')
|
||||||
|
$chooseStartMenuBtn = $window.FindName('ChooseStartMenuBtn')
|
||||||
|
$restoreModeTabs = $window.FindName('RestoreModeTabs')
|
||||||
|
$startMenuIntroPanel = $window.FindName('StartMenuIntroPanel')
|
||||||
|
$startMenuScopeCombo = $window.FindName('StartMenuScopeCombo')
|
||||||
|
$startMenuAutoBackupCheck = $window.FindName('StartMenuAutoBackupCheck')
|
||||||
|
$introInfoPanel = $window.FindName('IntroInfoPanel')
|
||||||
|
$overviewPanel = $window.FindName('OverviewPanel')
|
||||||
|
$overviewFeaturesSection = $window.FindName('OverviewFeaturesSection')
|
||||||
|
$overviewSummaryText = $window.FindName('OverviewSummaryText')
|
||||||
|
$backupFileText = $window.FindName('BackupFileText')
|
||||||
|
$backupCreatedText = $window.FindName('BackupCreatedText')
|
||||||
|
$backupTargetText = $window.FindName('BackupTargetText')
|
||||||
|
$featuresItemsControl = $window.FindName('FeaturesItemsControl')
|
||||||
|
$reappliedSeparator = $window.FindName('ReappliedSeparator')
|
||||||
|
$reappliedPanel = $window.FindName('ReappliedPanel')
|
||||||
|
$reappliedFeaturesItemsControl = $window.FindName('ReappliedFeaturesItemsControl')
|
||||||
|
$nonRevertibleSeparator = $window.FindName('NonRevertibleSeparator')
|
||||||
|
$nonRevertiblePanel = $window.FindName('NonRevertiblePanel')
|
||||||
|
$nonRevertibleFeaturesItemsControl = $window.FindName('NonRevertibleFeaturesItemsControl')
|
||||||
|
$nonRevertibleWikiLink = $window.FindName('NonRevertibleWikiLink')
|
||||||
|
|
||||||
|
$titleBar.Add_MouseLeftButtonDown({ $window.DragMove() })
|
||||||
|
$window.Tag = New-RestoreDialogState
|
||||||
|
$chooseRegistryBtn.IsDefault = $true
|
||||||
|
|
||||||
|
$state = @{ WizardStep = 'SelectType'; SelectedRegistryBackup = $null; SelectedStartMenuBackupFilePath = $null }
|
||||||
|
|
||||||
|
$getStartMenuScopeInfo = {
|
||||||
|
$isAllUsersScope = ($startMenuScopeCombo.SelectedItem.Tag -eq 'AllUsers')
|
||||||
|
$scopeValue = if ($isAllUsersScope) { 'AllUsers' } else { 'CurrentUser' }
|
||||||
|
$summaryScopeText = if ($isAllUsersScope) { 'all users' } else { 'the current user' }
|
||||||
|
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Scope = $scopeValue
|
||||||
|
Target = $scopeValue
|
||||||
|
SummaryText = $summaryScopeText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$showStartMenuIntroState = {
|
||||||
|
$backupFileText.Text = 'Not selected'
|
||||||
|
$backupCreatedText.Text = 'N/A'
|
||||||
|
$overviewSummaryText.Visibility = 'Collapsed'
|
||||||
|
$overviewPanel.Visibility = 'Collapsed'
|
||||||
|
$startMenuIntroPanel.Visibility = 'Visible'
|
||||||
|
$restoreModeTabs.SelectedIndex = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
$showStartMenuOverviewState = {
|
||||||
|
param([string]$BackupFilePath)
|
||||||
|
|
||||||
|
$scopeInfo = & $getStartMenuScopeInfo
|
||||||
|
$backupTargetText.Text = GetFriendlyRegistryBackupTarget -Target $scopeInfo.Target
|
||||||
|
$overviewSummaryText.Text = "This will replace the current Start Menu pinned apps layout for $($scopeInfo.SummaryText) with the selected backup."
|
||||||
|
$backupFileText.Text = Split-Path -Path $BackupFilePath -Leaf
|
||||||
|
|
||||||
|
$createdText = 'Unknown'
|
||||||
|
try {
|
||||||
|
$createdText = (Get-Item -LiteralPath $BackupFilePath -ErrorAction Stop).LastWriteTime.ToString('yyyy-MM-dd HH:mm')
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
$backupCreatedText.Text = $createdText
|
||||||
|
|
||||||
|
$overviewFeaturesSection.Visibility = 'Collapsed'
|
||||||
|
$overviewSummaryText.Visibility = 'Visible'
|
||||||
|
$reappliedSeparator.Visibility = 'Collapsed'
|
||||||
|
$reappliedPanel.Visibility = 'Collapsed'
|
||||||
|
$nonRevertibleSeparator.Visibility = 'Collapsed'
|
||||||
|
$nonRevertiblePanel.Visibility = 'Collapsed'
|
||||||
|
$introInfoPanel.Visibility = 'Collapsed'
|
||||||
|
$overviewPanel.Visibility = 'Visible'
|
||||||
|
$restoreModeTabs.SelectedIndex = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$updateStartMenuOverviewPanel = {
|
||||||
|
if ($state.WizardStep -ne 'StartMenu') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)) {
|
||||||
|
& $showStartMenuIntroState
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
& $showStartMenuOverviewState $state.SelectedStartMenuBackupFilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
$updateStartMenuPrimaryActionText = {
|
||||||
|
if ($state.WizardStep -ne 'StartMenu') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$isAutoBackupEnabled = ($startMenuAutoBackupCheck.IsChecked -eq $true)
|
||||||
|
$hasSelectedManualFile = -not [string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)
|
||||||
|
if ($isAutoBackupEnabled -or $hasSelectedManualFile) {
|
||||||
|
$primaryActionBtn.Content = 'Restore backup'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$primaryActionBtn.Content = 'Select backup file'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$refreshStartMenuUi = {
|
||||||
|
& $updateStartMenuOverviewPanel
|
||||||
|
& $updateStartMenuPrimaryActionText
|
||||||
|
}
|
||||||
|
|
||||||
|
$enterSelectTypeStep = {
|
||||||
|
$titleText.Text = 'Restore Backup'
|
||||||
|
$restoreModeTabs.SelectedIndex = 0
|
||||||
|
$backBtn.Visibility = 'Visible'
|
||||||
|
$backBtn.Content = 'Cancel'
|
||||||
|
$primaryActionBtn.Visibility = 'Collapsed'
|
||||||
|
$chooseRegistryBtn.IsDefault = $true
|
||||||
|
$primaryActionBtn.IsDefault = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$enterRegistryStep = {
|
||||||
|
$titleText.Text = 'Restore Registry Backup'
|
||||||
|
$restoreModeTabs.SelectedIndex = 1
|
||||||
|
$introInfoPanel.Visibility = 'Visible'
|
||||||
|
$overviewPanel.Visibility = 'Collapsed'
|
||||||
|
$overviewFeaturesSection.Visibility = 'Visible'
|
||||||
|
$overviewSummaryText.Visibility = 'Collapsed'
|
||||||
|
$backBtn.Visibility = 'Visible'
|
||||||
|
$backBtn.Content = 'Back'
|
||||||
|
$primaryActionBtn.Visibility = 'Visible'
|
||||||
|
$primaryActionBtn.Content = 'Select backup file'
|
||||||
|
$primaryActionBtn.IsDefault = $true
|
||||||
|
$chooseRegistryBtn.IsDefault = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$enterStartMenuStep = {
|
||||||
|
$titleText.Text = 'Restore Start Menu Backup'
|
||||||
|
$restoreModeTabs.SelectedIndex = 2
|
||||||
|
$backBtn.Visibility = 'Visible'
|
||||||
|
$backBtn.Content = 'Back'
|
||||||
|
$primaryActionBtn.Visibility = 'Visible'
|
||||||
|
$primaryActionBtn.IsDefault = $true
|
||||||
|
$chooseRegistryBtn.IsDefault = $false
|
||||||
|
& $refreshStartMenuUi
|
||||||
|
}
|
||||||
|
|
||||||
|
$showRegistryOverview = {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$SelectedBackup,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SelectedBackupFilePath
|
||||||
|
)
|
||||||
|
|
||||||
|
$createdText = if ([string]::IsNullOrWhiteSpace($SelectedBackup.CreatedAt)) {
|
||||||
|
'Unknown'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
[DateTime]::Parse($SelectedBackup.CreatedAt).ToString('yyyy-MM-dd HH:mm')
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$SelectedBackup.CreatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedForwardFeatureIds = @(Get-SelectedForwardFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
|
||||||
|
$selectedUndoFeatureIds = @(Get-SelectedUndoFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
|
||||||
|
|
||||||
|
$seenForwardFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
foreach ($featureId in $selectedForwardFeatureIds) {
|
||||||
|
[void]$seenForwardFeatureIds.Add([string]$featureId)
|
||||||
|
}
|
||||||
|
|
||||||
|
$filteredUndoFeatureIds = New-Object System.Collections.Generic.List[string]
|
||||||
|
foreach ($featureId in $selectedUndoFeatureIds) {
|
||||||
|
if ($seenForwardFeatureIds.Contains([string]$featureId)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$filteredUndoFeatureIds.Add([string]$featureId)
|
||||||
|
}
|
||||||
|
|
||||||
|
$forwardFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds $selectedForwardFeatureIds -Features $script:Features
|
||||||
|
$undoFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds @($filteredUndoFeatureIds.ToArray()) -Features $script:Features
|
||||||
|
$combinedFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds (Get-SelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup) -Features $script:Features
|
||||||
|
|
||||||
|
$revertibleFeaturesList = @($forwardFeatureLists.Revertible)
|
||||||
|
$reappliedFeaturesList = @($undoFeatureLists.Revertible)
|
||||||
|
$nonRevertibleFeaturesList = @($combinedFeatureLists.NonRevertible)
|
||||||
|
Write-Host "Backup overview prepared. Reverted=$($revertibleFeaturesList.Count), ReApplied=$($reappliedFeaturesList.Count), NonRevertible=$($nonRevertibleFeaturesList.Count)"
|
||||||
|
|
||||||
|
if ($revertibleFeaturesList.Count -eq 0 -and $reappliedFeaturesList.Count -eq 0) {
|
||||||
|
throw 'The selected backup does not contain any changes that can be restored.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$backupFileText.Text = Split-Path $SelectedBackupFilePath -Leaf
|
||||||
|
$backupCreatedText.Text = $createdText
|
||||||
|
$backupTargetText.Text = GetFriendlyRegistryBackupTarget -Target ([string]$SelectedBackup.Target)
|
||||||
|
$featuresItemsControl.ItemsSource = $revertibleFeaturesList
|
||||||
|
$overviewFeaturesSection.Visibility = if ($revertibleFeaturesList.Count -gt 0) { 'Visible' } else { 'Collapsed' }
|
||||||
|
$reappliedFeaturesItemsControl.ItemsSource = $reappliedFeaturesList
|
||||||
|
if ($reappliedFeaturesList.Count -gt 0) { $reappliedPanel.Visibility = 'Visible' } else { $reappliedPanel.Visibility = 'Collapsed' }
|
||||||
|
if ($revertibleFeaturesList.Count -gt 0 -and $reappliedFeaturesList.Count -gt 0) { $reappliedSeparator.Visibility = 'Visible' } else { $reappliedSeparator.Visibility = 'Collapsed' }
|
||||||
|
$overviewSummaryText.Visibility = 'Collapsed'
|
||||||
|
$nonRevertibleFeaturesItemsControl.ItemsSource = $nonRevertibleFeaturesList
|
||||||
|
|
||||||
|
$hasNonRevertibleItems = ($nonRevertibleFeaturesList.Count -gt 0)
|
||||||
|
if ($hasNonRevertibleItems) { $nonRevertiblePanel.Visibility = 'Visible' } else { $nonRevertiblePanel.Visibility = 'Collapsed' }
|
||||||
|
if ($hasNonRevertibleItems -and ($revertibleFeaturesList.Count -gt 0 -or $reappliedFeaturesList.Count -gt 0)) { $nonRevertibleSeparator.Visibility = 'Visible' } else { $nonRevertibleSeparator.Visibility = 'Collapsed' }
|
||||||
|
$introInfoPanel.Visibility = 'Collapsed'
|
||||||
|
$overviewPanel.Visibility = 'Visible'
|
||||||
|
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
$handleRegistryPrimaryAction = {
|
||||||
|
if ($state.SelectedRegistryBackup) {
|
||||||
|
$window.Tag = @{
|
||||||
|
Result = 'RestoreRegistry'
|
||||||
|
Backup = $state.SelectedRegistryBackup
|
||||||
|
}
|
||||||
|
$window.DialogResult = $true
|
||||||
|
$window.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
|
||||||
|
$openDialog.Title = 'Select Registry Backup File'
|
||||||
|
$openDialog.Filter = 'Registry backup (*.json)|*.json'
|
||||||
|
$openDialog.DefaultExt = '.json'
|
||||||
|
$openDialog.InitialDirectory = $script:RegistryBackupsPath
|
||||||
|
|
||||||
|
if ($openDialog.ShowDialog($window) -ne $true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Backup file selected: $($openDialog.FileName)"
|
||||||
|
$selectedBackup = Load-RegistryBackupFromFile -FilePath $openDialog.FileName
|
||||||
|
|
||||||
|
if (-not (& $showRegistryOverview -SelectedBackup $selectedBackup -SelectedBackupFilePath $openDialog.FileName)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$state.SelectedRegistryBackup = $selectedBackup
|
||||||
|
$primaryActionBtn.Content = 'Restore from backup'
|
||||||
|
}
|
||||||
|
|
||||||
|
$handleStartMenuPrimaryAction = {
|
||||||
|
$scope = (& $getStartMenuScopeInfo).Scope
|
||||||
|
$useManualBackupFile = -not ($startMenuAutoBackupCheck.IsChecked -eq $true)
|
||||||
|
|
||||||
|
if ($useManualBackupFile -and [string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)) {
|
||||||
|
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
|
||||||
|
$openDialog.Title = 'Select Start Menu Backup File'
|
||||||
|
$openDialog.Filter = 'Start Menu backup (*.bak)|*.bak'
|
||||||
|
$openDialog.InitialDirectory = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
|
||||||
|
$openDialog.DefaultExt = '.bak'
|
||||||
|
|
||||||
|
if ($openDialog.ShowDialog($window) -ne $true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$state.SelectedStartMenuBackupFilePath = $openDialog.FileName
|
||||||
|
Write-Host "Selected Start Menu backup file: $($state.SelectedStartMenuBackupFilePath)"
|
||||||
|
& $refreshStartMenuUi
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$window.Tag = @{
|
||||||
|
Result = 'RestoreStartMenu'
|
||||||
|
StartMenuScope = $scope
|
||||||
|
UseManualBackupFile = $useManualBackupFile
|
||||||
|
BackupFilePath = $state.SelectedStartMenuBackupFilePath
|
||||||
|
}
|
||||||
|
$window.DialogResult = $true
|
||||||
|
$window.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
$setWizardStep = {
|
||||||
|
param([string]$step)
|
||||||
|
|
||||||
|
$state.WizardStep = $step
|
||||||
|
|
||||||
|
switch ($step) {
|
||||||
|
'SelectType' { & $enterSelectTypeStep }
|
||||||
|
'Registry' { & $enterRegistryStep }
|
||||||
|
'StartMenu' { & $enterStartMenuStep }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$startMenuAutoBackupCheck.Add_Checked({
|
||||||
|
$state.SelectedStartMenuBackupFilePath = $null
|
||||||
|
& $refreshStartMenuUi
|
||||||
|
})
|
||||||
|
$startMenuAutoBackupCheck.Add_Unchecked({
|
||||||
|
& $refreshStartMenuUi
|
||||||
|
})
|
||||||
|
|
||||||
|
$startMenuScopeCombo.Add_SelectionChanged({
|
||||||
|
& $refreshStartMenuUi
|
||||||
|
})
|
||||||
|
|
||||||
|
$nonRevertibleWikiLink.Add_MouseLeftButtonUp({
|
||||||
|
try {
|
||||||
|
Start-Process 'https://github.com/Raphire/Win11Debloat/wiki/Reverting-Changes' | Out-Null
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
})
|
||||||
|
|
||||||
|
$closeBtn.Add_Click({
|
||||||
|
$window.Tag = New-RestoreDialogState
|
||||||
|
$window.DialogResult = $false
|
||||||
|
$window.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
$chooseRegistryBtn.Add_Click({ & $setWizardStep 'Registry' })
|
||||||
|
$chooseStartMenuBtn.Add_Click({ & $setWizardStep 'StartMenu' })
|
||||||
|
|
||||||
|
$backBtn.Add_Click({
|
||||||
|
if ($state.WizardStep -eq 'SelectType') {
|
||||||
|
$window.Tag = New-RestoreDialogState
|
||||||
|
$window.DialogResult = $false
|
||||||
|
$window.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($state.WizardStep -eq 'Registry') {
|
||||||
|
$state.SelectedRegistryBackup = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($state.WizardStep -eq 'StartMenu') {
|
||||||
|
$state.SelectedStartMenuBackupFilePath = $null
|
||||||
|
$startMenuAutoBackupCheck.IsChecked = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
& $setWizardStep 'SelectType'
|
||||||
|
})
|
||||||
|
|
||||||
|
$primaryActionBtn.Add_Click({
|
||||||
|
switch ($state.WizardStep) {
|
||||||
|
'Registry' { & $handleRegistryPrimaryAction }
|
||||||
|
'StartMenu' { & $handleStartMenuPrimaryAction }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$window.Add_KeyDown({
|
||||||
|
param($source, $e)
|
||||||
|
if ($e.Key -eq 'Escape') {
|
||||||
|
$window.Tag = New-RestoreDialogState
|
||||||
|
$window.DialogResult = $false
|
||||||
|
$window.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
& $setWizardStep 'SelectType'
|
||||||
|
|
||||||
|
try {
|
||||||
|
$null = $window.ShowDialog()
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$innerMessage = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { 'None' }
|
||||||
|
throw "Failed to show restore backup dialog. Error: $($_.Exception.Message) Inner: $innerMessage"
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if ($overlay -and -not $overlayWasAlreadyVisible) {
|
||||||
|
try {
|
||||||
|
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' })
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $window.Tag
|
||||||
|
}
|
||||||
102
Scripts/GUI/Show-RestoreBackupWindow.ps1
Normal file
102
Scripts/GUI/Show-RestoreBackupWindow.ps1
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
function Show-RestoreBackupWindow {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[System.Windows.Window]$Owner = $null
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
Write-Host 'Opening restore backup dialog.'
|
||||||
|
|
||||||
|
$restoreResult = [PSCustomObject]@{
|
||||||
|
RestoredRegistry = $false
|
||||||
|
RestoredStartMenu = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$dialogResult = Show-RestoreBackupDialog -Owner $Owner
|
||||||
|
if (-not $dialogResult -or $dialogResult.Result -eq 'Cancel') {
|
||||||
|
Write-Host 'Restore canceled by user.'
|
||||||
|
return $restoreResult
|
||||||
|
}
|
||||||
|
|
||||||
|
$successMessage = $null
|
||||||
|
$warningMessage = $null
|
||||||
|
|
||||||
|
if ($dialogResult.Result -eq 'RestoreRegistry') {
|
||||||
|
$backup = $dialogResult.Backup
|
||||||
|
if (-not $backup) {
|
||||||
|
throw 'Registry backup restore requested without a selected backup.'
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "User confirmed registry restore for $($backup.Target)."
|
||||||
|
Restore-RegistryBackupState -Backup $backup
|
||||||
|
$restoreResult.RestoredRegistry = $true
|
||||||
|
$successMessage = 'Registry backup restored successfully. Some changes may require a restart to take effect.'
|
||||||
|
}
|
||||||
|
elseif ($dialogResult.Result -eq 'RestoreStartMenu') {
|
||||||
|
$scope = $dialogResult.StartMenuScope
|
||||||
|
$useManualBackupFile = ($dialogResult.UseManualBackupFile -eq $true)
|
||||||
|
$backupFilePath = $null
|
||||||
|
if ($dialogResult -is [hashtable] -and $dialogResult.ContainsKey('BackupFilePath')) {
|
||||||
|
$backupFilePath = $dialogResult['BackupFilePath']
|
||||||
|
}
|
||||||
|
elseif ($dialogResult.PSObject.Properties.Match('BackupFilePath').Count -gt 0) {
|
||||||
|
$backupFilePath = $dialogResult.BackupFilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($useManualBackupFile -and [string]::IsNullOrWhiteSpace($backupFilePath)) {
|
||||||
|
throw 'Start Menu restore canceled: no backup file selected.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = if ($scope -eq 'AllUsers') {
|
||||||
|
RestoreStartMenuForAllUsers -BackupFilePath $backupFilePath
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
RestoreStartMenu -BackupFilePath $backupFilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
$resultEntries = @($result)
|
||||||
|
$successCount = @($resultEntries | Where-Object { $_.Result -eq $true }).Count
|
||||||
|
$failedEntries = @($resultEntries | Where-Object { $_.Result -ne $true })
|
||||||
|
|
||||||
|
if ($successCount -eq 0) {
|
||||||
|
$errorSummary = ($resultEntries | ForEach-Object { $_.Message }) -join [Environment]::NewLine
|
||||||
|
throw "Failed to restore the Start Menu backup.`n$errorSummary"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($failedEntries.Count -gt 0) {
|
||||||
|
$failureSummary = ($failedEntries | ForEach-Object { $_.Message }) -join [Environment]::NewLine
|
||||||
|
$warningMessage = "The Start Menu backup was successfully restored for $successCount user(s).`nSome users could not be restored:`n$failureSummary"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ($scope -eq 'AllUsers') {
|
||||||
|
$successMessage = "The Start Menu backup was successfully restored for all users. The changes will apply the next time users sign in."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$successMessage = "The Start Menu backup was successfully restored for the current user. The changes will apply the next time you sign in."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$restoreResult.RestoredStartMenu = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($warningMessage) {
|
||||||
|
Write-Host "$warningMessage"
|
||||||
|
Show-MessageBox -Title 'Backup Restored' -Message $warningMessage -Icon Warning
|
||||||
|
}
|
||||||
|
elseif ($successMessage) {
|
||||||
|
Write-Host "$successMessage"
|
||||||
|
Show-MessageBox -Title 'Backup Restored' -Message $successMessage -Icon Success
|
||||||
|
}
|
||||||
|
|
||||||
|
return $restoreResult
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$errorMessage = if ($_.Exception.Message) { $_.Exception.Message } else { 'An unexpected error occurred.' }
|
||||||
|
Write-Error "Restore operation failed: $errorMessage"
|
||||||
|
Show-MessageBox -Title 'Error' -Message "Restore failed: $errorMessage" -Icon Error
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
RestoredRegistry = $false
|
||||||
|
RestoredStartMenu = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -117,11 +117,15 @@ Write-Output "------------------------------------------------------------------
|
|||||||
Write-Output " Win11Debloat Script - Get Dev"
|
Write-Output " Win11Debloat Script - Get Dev"
|
||||||
Write-Output "-------------------------------------------------------------------------------------------"
|
Write-Output "-------------------------------------------------------------------------------------------"
|
||||||
|
|
||||||
|
$tempRootPath = $env:TEMP
|
||||||
|
$tempWorkPath = Join-Path $tempRootPath 'Win11Debloat'
|
||||||
|
$tempArchivePath = Join-Path $tempRootPath 'win11debloat.zip'
|
||||||
|
|
||||||
Write-Output "> Downloading Win11Debloat for development..."
|
Write-Output "> Downloading Win11Debloat for development..."
|
||||||
|
|
||||||
# Download latest version of Win11Debloat from GitHub master branch as zip archive
|
# Download latest version of Win11Debloat from GitHub master branch as zip archive
|
||||||
try {
|
try {
|
||||||
Invoke-RestMethod "https://github.com/Raphire/Win11Debloat/archive/refs/heads/master.zip" -OutFile "$env:TEMP/win11debloat.zip"
|
Invoke-RestMethod "https://github.com/Raphire/Win11Debloat/archive/refs/heads/master.zip" -OutFile $tempArchivePath
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
Write-Host "Error: Unable to fetch master branch from GitHub. Please check your internet connection and try again." -ForegroundColor Red
|
Write-Host "Error: Unable to fetch master branch from GitHub. Please check your internet connection and try again." -ForegroundColor Red
|
||||||
@@ -135,12 +139,12 @@ Write-Output ""
|
|||||||
Write-Output "> Cleaning up old Win11Debloat folder..."
|
Write-Output "> Cleaning up old Win11Debloat folder..."
|
||||||
|
|
||||||
# Remove old script folder if it exists, but keep config and log files
|
# Remove old script folder if it exists, but keep config and log files
|
||||||
if (Test-Path "$env:TEMP/Win11Debloat") {
|
if (Test-Path $tempWorkPath) {
|
||||||
Get-ChildItem -Path "$env:TEMP/Win11Debloat" -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Config,Logs | Remove-Item -Recurse -Force
|
Get-ChildItem -Path $tempWorkPath -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Config,Logs,Backups | Remove-Item -Recurse -Force
|
||||||
}
|
}
|
||||||
|
|
||||||
$configDir = "$env:TEMP/Win11Debloat/Config"
|
$configDir = Join-Path $tempWorkPath 'Config'
|
||||||
$backupDir = "$env:TEMP/Win11Debloat/ConfigOld"
|
$backupDir = Join-Path $tempWorkPath 'ConfigOld'
|
||||||
|
|
||||||
# Temporarily move existing config files if they exist to prevent them from being overwritten by the new script files, will be moved back after the new script is unpacked
|
# Temporarily move existing config files if they exist to prevent them from being overwritten by the new script files, will be moved back after the new script is unpacked
|
||||||
if (Test-Path "$configDir") {
|
if (Test-Path "$configDir") {
|
||||||
@@ -160,13 +164,13 @@ Write-Output ""
|
|||||||
Write-Output "> Unpacking..."
|
Write-Output "> Unpacking..."
|
||||||
|
|
||||||
# Unzip archive to Win11Debloat folder
|
# Unzip archive to Win11Debloat folder
|
||||||
Expand-Archive "$env:TEMP/win11debloat.zip" "$env:TEMP/Win11Debloat"
|
Expand-Archive $tempArchivePath $tempWorkPath
|
||||||
|
|
||||||
# Remove archive
|
# Remove archive
|
||||||
Remove-Item "$env:TEMP/win11debloat.zip"
|
Remove-Item $tempArchivePath
|
||||||
|
|
||||||
# Move files
|
# Move files
|
||||||
Get-ChildItem -Path "$env:TEMP/Win11Debloat/*Win11Debloat-*" -Recurse | Move-Item -Destination "$env:TEMP/Win11Debloat"
|
Get-ChildItem -Path (Join-Path $tempWorkPath '*Win11Debloat-*') -Recurse | Move-Item -Destination $tempWorkPath
|
||||||
|
|
||||||
# Add existing config files back to Config folder
|
# Add existing config files back to Config folder
|
||||||
if (Test-Path "$backupDir") {
|
if (Test-Path "$backupDir") {
|
||||||
@@ -206,7 +210,8 @@ if ($PSVersionTable.PSVersion.Major -ge 7) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Run Win11Debloat script with the provided arguments
|
# Run Win11Debloat script with the provided arguments
|
||||||
$debloatProcess = Start-Process powershell.exe -WindowStyle $windowStyle -PassThru -ArgumentList "-executionpolicy bypass -File $env:TEMP\Win11Debloat\Win11Debloat.ps1 $arguments" -Verb RunAs
|
$debloatScriptPath = Join-Path $tempWorkPath 'Win11Debloat.ps1'
|
||||||
|
$debloatProcess = Start-Process powershell.exe -WindowStyle $windowStyle -PassThru -ArgumentList "-executionpolicy bypass -File `"$debloatScriptPath`" $arguments" -Verb RunAs
|
||||||
|
|
||||||
# Wait for the process to finish before continuing
|
# Wait for the process to finish before continuing
|
||||||
if ($null -ne $debloatProcess) {
|
if ($null -ne $debloatProcess) {
|
||||||
@@ -214,12 +219,12 @@ if ($null -ne $debloatProcess) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Remove all remaining script files, except for CustomAppsList and LastUsedSettings.json files
|
# Remove all remaining script files, except for CustomAppsList and LastUsedSettings.json files
|
||||||
if (Test-Path "$env:TEMP/Win11Debloat") {
|
if (Test-Path $tempWorkPath) {
|
||||||
Write-Output ""
|
Write-Output ""
|
||||||
Write-Output "> Cleaning up..."
|
Write-Output "> Cleaning up..."
|
||||||
|
|
||||||
# Cleanup, remove Win11Debloat directory
|
# Cleanup, remove Win11Debloat directory
|
||||||
Get-ChildItem -Path "$env:TEMP/Win11Debloat" -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Win11Debloat-Run.log,Config,Logs | Remove-Item -Recurse -Force
|
Get-ChildItem -Path $tempWorkPath -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Win11Debloat-Run.log,Config,Logs,Backups | Remove-Item -Recurse -Force
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Output ""
|
Write-Output ""
|
||||||
|
|||||||
@@ -117,12 +117,16 @@ Write-Output "------------------------------------------------------------------
|
|||||||
Write-Output " Win11Debloat Script - Get"
|
Write-Output " Win11Debloat Script - Get"
|
||||||
Write-Output "-------------------------------------------------------------------------------------------"
|
Write-Output "-------------------------------------------------------------------------------------------"
|
||||||
|
|
||||||
|
$tempRootPath = $env:TEMP
|
||||||
|
$tempWorkPath = Join-Path $tempRootPath 'Win11Debloat'
|
||||||
|
$tempArchivePath = Join-Path $tempRootPath 'win11debloat.zip'
|
||||||
|
|
||||||
Write-Output "> Downloading Win11Debloat..."
|
Write-Output "> Downloading Win11Debloat..."
|
||||||
|
|
||||||
# Download latest version of Win11Debloat from GitHub as zip archive
|
# Download latest version of Win11Debloat from GitHub as zip archive
|
||||||
try {
|
try {
|
||||||
$LatestReleaseUri = (Invoke-RestMethod https://api.github.com/repos/Raphire/Win11Debloat/releases/latest).zipball_url
|
$LatestReleaseUri = (Invoke-RestMethod https://api.github.com/repos/Raphire/Win11Debloat/releases/latest).zipball_url
|
||||||
Invoke-RestMethod $LatestReleaseUri -OutFile "$env:TEMP/win11debloat.zip"
|
Invoke-RestMethod $LatestReleaseUri -OutFile $tempArchivePath
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
Write-Host "Error: Unable to fetch latest release from GitHub. Please check your internet connection and try again." -ForegroundColor Red
|
Write-Host "Error: Unable to fetch latest release from GitHub. Please check your internet connection and try again." -ForegroundColor Red
|
||||||
@@ -132,19 +136,22 @@ catch {
|
|||||||
Exit
|
Exit
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Output ""
|
# Remove old script folder if it exists, but keep configs, logs and backups
|
||||||
Write-Output "> Cleaning up old Win11Debloat folder..."
|
if (Test-Path $tempWorkPath) {
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output "> Cleaning up old Win11Debloat folder..."
|
||||||
|
|
||||||
# Remove old script folder if it exists, but keep config and log files
|
Get-ChildItem -Path $tempWorkPath -Exclude Config,Logs,Backups | Remove-Item -Recurse -Force
|
||||||
if (Test-Path "$env:TEMP/Win11Debloat") {
|
|
||||||
Get-ChildItem -Path "$env:TEMP/Win11Debloat" -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Config,Logs | Remove-Item -Recurse -Force
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$configDir = "$env:TEMP/Win11Debloat/Config"
|
$configDir = Join-Path $tempWorkPath 'Config'
|
||||||
$backupDir = "$env:TEMP/Win11Debloat/ConfigOld"
|
$backupDir = Join-Path $tempWorkPath 'ConfigOld'
|
||||||
|
|
||||||
# Temporarily move existing config files if they exist to prevent them from being overwritten by the new script files, will be moved back after the new script is unpacked
|
# Temporarily move existing config files if they exist to prevent them from being overwritten by the new script files, will be moved back after the new script is unpacked
|
||||||
if (Test-Path "$configDir") {
|
if (Test-Path "$configDir") {
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output "> Backing up existing config files..."
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null
|
New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null
|
||||||
|
|
||||||
$filesToKeep = @(
|
$filesToKeep = @(
|
||||||
@@ -161,13 +168,13 @@ Write-Output ""
|
|||||||
Write-Output "> Unpacking..."
|
Write-Output "> Unpacking..."
|
||||||
|
|
||||||
# Unzip archive to Win11Debloat folder
|
# Unzip archive to Win11Debloat folder
|
||||||
Expand-Archive "$env:TEMP/win11debloat.zip" "$env:TEMP/Win11Debloat"
|
Expand-Archive $tempArchivePath $tempWorkPath
|
||||||
|
|
||||||
# Remove archive
|
# Remove archive
|
||||||
Remove-Item "$env:TEMP/win11debloat.zip"
|
Remove-Item $tempArchivePath
|
||||||
|
|
||||||
# Move files
|
# Move files
|
||||||
Get-ChildItem -Path "$env:TEMP/Win11Debloat/Raphire-Win11Debloat-*" -Recurse | Move-Item -Destination "$env:TEMP/Win11Debloat"
|
Get-ChildItem -Path (Join-Path $tempWorkPath 'Raphire-Win11Debloat-*') -Recurse | Move-Item -Destination $tempWorkPath
|
||||||
|
|
||||||
# Add existing config files back to Config folder
|
# Add existing config files back to Config folder
|
||||||
if (Test-Path "$backupDir") {
|
if (Test-Path "$backupDir") {
|
||||||
@@ -175,6 +182,9 @@ if (Test-Path "$backupDir") {
|
|||||||
New-Item -ItemType Directory -Path "$configDir" -Force | Out-Null
|
New-Item -ItemType Directory -Path "$configDir" -Force | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output "> Restoring existing config files..."
|
||||||
|
|
||||||
Get-ChildItem -Path "$backupDir" -Recurse | Move-Item -Destination "$configDir"
|
Get-ChildItem -Path "$backupDir" -Recurse | Move-Item -Destination "$configDir"
|
||||||
Remove-Item "$backupDir" -Recurse -Force
|
Remove-Item "$backupDir" -Recurse -Force
|
||||||
}
|
}
|
||||||
@@ -207,20 +217,21 @@ if ($PSVersionTable.PSVersion.Major -ge 7) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Run Win11Debloat script with the provided arguments
|
# Run Win11Debloat script with the provided arguments
|
||||||
$debloatProcess = Start-Process powershell.exe -WindowStyle $windowStyle -PassThru -ArgumentList "-executionpolicy bypass -File $env:TEMP\Win11Debloat\Win11Debloat.ps1 $arguments" -Verb RunAs
|
$debloatScriptPath = Join-Path $tempWorkPath 'Win11Debloat.ps1'
|
||||||
|
$debloatProcess = Start-Process powershell.exe -WindowStyle $windowStyle -PassThru -ArgumentList "-executionpolicy bypass -File `"$debloatScriptPath`" $arguments" -Verb RunAs
|
||||||
|
|
||||||
# Wait for the process to finish before continuing
|
# Wait for the process to finish before continuing
|
||||||
if ($null -ne $debloatProcess) {
|
if ($null -ne $debloatProcess) {
|
||||||
$debloatProcess.WaitForExit()
|
$debloatProcess.WaitForExit()
|
||||||
}
|
}
|
||||||
|
|
||||||
# Remove all remaining script files, except for CustomAppsList and LastUsedSettings.json files
|
# Remove all remaining script files, except for configs, logs and backups
|
||||||
if (Test-Path "$env:TEMP/Win11Debloat") {
|
if (Test-Path $tempWorkPath) {
|
||||||
Write-Output ""
|
Write-Output ""
|
||||||
Write-Output "> Cleaning up..."
|
Write-Output "> Cleaning up..."
|
||||||
|
|
||||||
# Cleanup, remove Win11Debloat directory
|
# Cleanup, remove Win11Debloat directory
|
||||||
Get-ChildItem -Path "$env:TEMP/Win11Debloat" -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Win11Debloat-Run.log,Config,Logs | Remove-Item -Recurse -Force
|
Get-ChildItem -Path $tempWorkPath -Exclude Config,Logs,Backups | Remove-Item -Recurse -Force
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Output ""
|
Write-Output ""
|
||||||
|
|||||||
223
Scripts/Helpers/ApplyRegistryRegFile.ps1
Normal file
223
Scripts/Helpers/ApplyRegistryRegFile.ps1
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
function Get-NormalizedRegistryValueName {
|
||||||
|
param(
|
||||||
|
[AllowNull()]
|
||||||
|
$ValueName
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrEmpty([string]$ValueName)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return [string]$ValueName
|
||||||
|
}
|
||||||
|
|
||||||
|
function Convert-RegOperationToValueKind {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Operation
|
||||||
|
)
|
||||||
|
|
||||||
|
$valueName = if ([string]::IsNullOrEmpty([string]$Operation.ValueName)) { '' } else { [string]$Operation.ValueName }
|
||||||
|
$valueType = [string]$Operation.ValueType
|
||||||
|
$operationKeyPath = [string]$Operation.KeyPath
|
||||||
|
|
||||||
|
switch ($valueType) {
|
||||||
|
'DWord' {
|
||||||
|
$unsigned = [uint32]$Operation.ValueData
|
||||||
|
$value = [BitConverter]::ToInt32([BitConverter]::GetBytes($unsigned), 0)
|
||||||
|
return @{ Name = $valueName; Kind = [Microsoft.Win32.RegistryValueKind]::DWord; Value = $value }
|
||||||
|
}
|
||||||
|
'String' {
|
||||||
|
return @{ Name = $valueName; Kind = [Microsoft.Win32.RegistryValueKind]::String; Value = [string]$Operation.ValueData }
|
||||||
|
}
|
||||||
|
'Binary' {
|
||||||
|
return @{ Name = $valueName; Kind = [Microsoft.Win32.RegistryValueKind]::Binary; Value = [byte[]]$Operation.ValueData }
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
throw "Unsupported value type '$valueType' while applying reg operation for '$operationKeyPath'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Remove-RegistrySubKeyTreeIfExists {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[Microsoft.Win32.RegistryKey]$RootKey,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$SubKeyPath
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
$RootKey.DeleteSubKeyTree($SubKeyPath, $false)
|
||||||
|
}
|
||||||
|
catch [System.UnauthorizedAccessException], [System.Security.SecurityException] {
|
||||||
|
throw
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# Best-effort cleanup only; missing keys are fine.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RegistryKeyForOperation {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$RegistryPath,
|
||||||
|
[switch]$CreateIfMissing,
|
||||||
|
[bool]$OpenKey = $true
|
||||||
|
)
|
||||||
|
|
||||||
|
$parts = Split-RegistryPath -path $RegistryPath
|
||||||
|
if (-not $parts) {
|
||||||
|
throw "Unsupported registry path: $RegistryPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$rootKey = Get-RegistryRootKey -hiveName $parts.Hive
|
||||||
|
if (-not $rootKey) {
|
||||||
|
throw "Unsupported registry hive '$($parts.Hive)' in path '$RegistryPath'"
|
||||||
|
}
|
||||||
|
|
||||||
|
$subKeyPath = $parts.SubKey
|
||||||
|
if ([string]::IsNullOrWhiteSpace($subKeyPath)) {
|
||||||
|
return [PSCustomObject]@{ RootKey = $rootKey; SubKeyPath = $null; Key = $rootKey }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $OpenKey) {
|
||||||
|
return [PSCustomObject]@{ RootKey = $rootKey; SubKeyPath = $subKeyPath; Key = $null }
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = if ($CreateIfMissing) {
|
||||||
|
$rootKey.CreateSubKey($subKeyPath)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$rootKey.OpenSubKey($subKeyPath, $true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [PSCustomObject]@{ RootKey = $rootKey; SubKeyPath = $subKeyPath; Key = $key }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-RegistryDeleteValueOperation {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Operation,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$KeyInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $KeyInfo.Key) {
|
||||||
|
$valueName = Get-NormalizedRegistryValueName -ValueName $Operation.ValueName
|
||||||
|
$displayValueName = if ([string]::IsNullOrEmpty($valueName)) { '(Default)' } else { $valueName }
|
||||||
|
Write-Verbose "Unable to find or open key '$($Operation.KeyPath)' and value '$displayValueName'"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$valueName = Get-NormalizedRegistryValueName -ValueName $Operation.ValueName
|
||||||
|
$KeyInfo.Key.DeleteValue($valueName, $false)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$KeyInfo.Key.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-RegistrySetValueOperation {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Operation,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$KeyInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $KeyInfo.Key) {
|
||||||
|
throw [System.UnauthorizedAccessException]::new("Unable to open or create registry key '$($Operation.KeyPath)'")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$setArgs = Convert-RegOperationToValueKind -Operation $Operation
|
||||||
|
$KeyInfo.Key.SetValue($setArgs.Name, $setArgs.Value, $setArgs.Kind)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$KeyInfo.Key.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-RegistryOperationAccessDeniedWarning {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Operation,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$ExceptionMessage
|
||||||
|
)
|
||||||
|
|
||||||
|
$keyPath = [string]$Operation.KeyPath
|
||||||
|
$operationType = [string]$Operation.OperationType
|
||||||
|
|
||||||
|
if ($operationType -eq 'SetValue' -or $operationType -eq 'DeleteValue') {
|
||||||
|
$valueName = Get-NormalizedRegistryValueName -ValueName $Operation.ValueName
|
||||||
|
$displayValueName = if ([string]::IsNullOrEmpty($valueName)) { '(Default)' } else { $valueName }
|
||||||
|
Write-Warning "Skipping operation '$operationType' on key '$keyPath' value '$displayValueName' due to access restrictions: $ExceptionMessage"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Warning "Skipping operation '$operationType' on key '$keyPath' due to access restrictions: $ExceptionMessage"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-RegistryOperation {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Operation,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$RegFilePath
|
||||||
|
)
|
||||||
|
|
||||||
|
$operationType = [string]$Operation.OperationType
|
||||||
|
$isSetValueOperation = $operationType -eq 'SetValue'
|
||||||
|
$isDeleteKeyOperation = $operationType -eq 'DeleteKey'
|
||||||
|
|
||||||
|
$keyInfo = Get-RegistryKeyForOperation -RegistryPath $Operation.KeyPath -CreateIfMissing:$isSetValueOperation -OpenKey:(-not $isDeleteKeyOperation)
|
||||||
|
|
||||||
|
switch ($operationType) {
|
||||||
|
'DeleteKey' {
|
||||||
|
if ($null -ne $keyInfo.SubKeyPath) {
|
||||||
|
Remove-RegistrySubKeyTreeIfExists -RootKey $keyInfo.RootKey -SubKeyPath $keyInfo.SubKeyPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'DeleteValue' {
|
||||||
|
Invoke-RegistryDeleteValueOperation -Operation $Operation -KeyInfo $keyInfo
|
||||||
|
}
|
||||||
|
'SetValue' {
|
||||||
|
Invoke-RegistrySetValueOperation -Operation $Operation -KeyInfo $keyInfo
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
throw "Unsupported reg operation type '$($Operation.OperationType)' in '$RegFilePath'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-RegistryOperationsFromRegFile {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$RegFilePath
|
||||||
|
)
|
||||||
|
|
||||||
|
$accessDeniedCount = 0
|
||||||
|
$operations = @(Get-RegFileOperations -regFilePath $RegFilePath)
|
||||||
|
$totalOperations = $operations.Count
|
||||||
|
|
||||||
|
foreach ($operation in $operations) {
|
||||||
|
try {
|
||||||
|
Invoke-RegistryOperation -Operation $operation -RegFilePath $RegFilePath
|
||||||
|
}
|
||||||
|
catch [System.UnauthorizedAccessException], [System.Security.SecurityException] {
|
||||||
|
$accessDeniedCount++
|
||||||
|
Write-RegistryOperationAccessDeniedWarning -Operation $operation -ExceptionMessage $_.Exception.Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($totalOperations -gt 0 -and $accessDeniedCount -eq $totalOperations) {
|
||||||
|
throw "Registry fallback import could not apply any operations in '$RegFilePath' because all $accessDeniedCount operation(s) were blocked by access restrictions."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($accessDeniedCount -gt 0) {
|
||||||
|
Write-Warning "Registry fallback import completed with $accessDeniedCount access-restricted operation(s) skipped in '$RegFilePath'."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +1,41 @@
|
|||||||
function CheckIfUserExists {
|
function CheckIfUserExists {
|
||||||
param (
|
param (
|
||||||
$userName
|
[string]$userName
|
||||||
)
|
)
|
||||||
|
|
||||||
if ($userName -match '[<>:"|?*]') {
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($userName)) {
|
if ([string]::IsNullOrWhiteSpace($userName)) {
|
||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$lookupName = $userName.Trim()
|
||||||
|
|
||||||
|
# Validate special characters against the local username segment (user in DOMAIN\user or user@domain).
|
||||||
|
$localUserName = GetLocalUserNameSegment -UserName $lookupName
|
||||||
|
|
||||||
|
if ($localUserName.IndexOfAny([System.IO.Path]::GetInvalidFileNameChars()) -ge 0) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
# PowerShell treats [] as wildcard chars in non-literal paths; disallow them explicitly.
|
||||||
|
if ($localUserName -match '[\[\]]') {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$userExists = Test-Path "$env:SystemDrive\Users\$userName"
|
$userContext = ResolveUserProfileContext -UserName $lookupName
|
||||||
|
if (-not $userContext -or [string]::IsNullOrWhiteSpace($userContext.ProfilePath)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
if ($userExists) {
|
if ($lookupName -ieq 'Default') {
|
||||||
return $true
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
$userExists = Test-Path ($env:USERPROFILE -Replace ('\\' + $env:USERNAME + '$'), "\$userName")
|
return -not [string]::IsNullOrWhiteSpace($userContext.UserSid)
|
||||||
|
|
||||||
if ($userExists) {
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
Write-Error "Something went wrong when trying to find the user directory path for user $userName. Please ensure the user exists on this system"
|
Write-Error "Something went wrong when trying to find the user directory path for user $lookupName. Please ensure the user exists on this system"
|
||||||
}
|
}
|
||||||
|
|
||||||
return $false
|
return $false
|
||||||
|
|||||||
177
Scripts/Helpers/Get-RegFileOperations.ps1
Normal file
177
Scripts/Helpers/Get-RegFileOperations.ps1
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
function Get-RegFileOperations {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$regFilePath
|
||||||
|
)
|
||||||
|
|
||||||
|
$content = Get-Content -Path $regFilePath -Raw -ErrorAction Stop
|
||||||
|
$rawLines = $content -split "`r?`n"
|
||||||
|
|
||||||
|
# Join continuation lines (lines ending with \)
|
||||||
|
$lines = @()
|
||||||
|
$i = 0
|
||||||
|
while ($i -lt $rawLines.Count) {
|
||||||
|
$line = $rawLines[$i]
|
||||||
|
|
||||||
|
# Join lines that end with backslash to the next line(s)
|
||||||
|
while ($line.EndsWith("\") -and $i + 1 -lt $rawLines.Count) {
|
||||||
|
$line = $line.Substring(0, $line.Length - 1) + $rawLines[$i + 1]
|
||||||
|
$i++
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines += $line
|
||||||
|
$i++
|
||||||
|
}
|
||||||
|
|
||||||
|
$operations = @()
|
||||||
|
$currentKeyPath = $null
|
||||||
|
$isDeletedKey = $false
|
||||||
|
|
||||||
|
foreach ($rawLine in $lines) {
|
||||||
|
$line = $rawLine.Trim()
|
||||||
|
if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith(';')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($line -match '^Windows Registry Editor Version') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($line -match '^\[(?<deleted>-)?(?<keyPath>[^\]]+)\]$') {
|
||||||
|
$currentKeyPath = $matches.keyPath.Trim()
|
||||||
|
$isDeletedKey = $matches.deleted -eq '-'
|
||||||
|
|
||||||
|
if ($isDeletedKey) {
|
||||||
|
$operations += [PSCustomObject]@{
|
||||||
|
OperationType = 'DeleteKey'
|
||||||
|
KeyPath = $currentKeyPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $currentKeyPath -or $isDeletedKey) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($line -notmatch '^(?<valueName>@|"[^"]+")=(?<valueData>.*)$') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$valueNameToken = $matches.valueName
|
||||||
|
$valueName = if ($valueNameToken -eq '@') {
|
||||||
|
''
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$valueNameToken.Trim('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
$parsedValue = Convert-RegValueData -valueData $matches.valueData.Trim()
|
||||||
|
if (-not $parsedValue) { continue }
|
||||||
|
|
||||||
|
$operations += [PSCustomObject]@{
|
||||||
|
OperationType = $parsedValue.OperationType
|
||||||
|
KeyPath = $currentKeyPath
|
||||||
|
ValueName = $valueName
|
||||||
|
ValueType = $parsedValue.ValueType
|
||||||
|
ValueData = $parsedValue.ValueData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $operations
|
||||||
|
}
|
||||||
|
|
||||||
|
function Convert-RegValueData {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$valueData
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($valueData -eq '-') {
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
OperationType = 'DeleteValue'
|
||||||
|
ValueType = $null
|
||||||
|
ValueData = $null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($valueData -match '^dword:(?<value>[0-9a-fA-F]{1,8})$') {
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
OperationType = 'SetValue'
|
||||||
|
ValueType = 'DWord'
|
||||||
|
ValueData = [uint32]::Parse($matches.value, [System.Globalization.NumberStyles]::HexNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($valueData -match '^qword:(?<value>[0-9a-fA-F]{1,16})$') {
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
OperationType = 'SetValue'
|
||||||
|
ValueType = 'QWord'
|
||||||
|
ValueData = [uint64]::Parse($matches.value, [System.Globalization.NumberStyles]::HexNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($valueData -match '^hex(?:\((?<kind>[0-9a-fA-F]+)\))?:(?<bytes>[0-9a-fA-F,\s]+)$') {
|
||||||
|
$bytes = Convert-HexStringToByteArray -hexValue $matches.bytes
|
||||||
|
$valueType = if ($matches.kind) { "Hex$($matches.kind)" } else { 'Binary' }
|
||||||
|
$value = switch ($matches.kind) {
|
||||||
|
'2' { Convert-RegistryByteArrayToString -byteData $bytes }
|
||||||
|
'7' { Convert-RegistryByteArrayToMultiString -byteData $bytes }
|
||||||
|
default { $bytes }
|
||||||
|
}
|
||||||
|
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
OperationType = 'SetValue'
|
||||||
|
ValueType = $valueType
|
||||||
|
ValueData = $value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($valueData -match '^"(?<value>.*)"$') {
|
||||||
|
$stringValue = $matches.value
|
||||||
|
# Unescape registry string escape sequences
|
||||||
|
$stringValue = $stringValue -replace '\\"', '"' -replace '\\\\', '\'
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
OperationType = 'SetValue'
|
||||||
|
ValueType = 'String'
|
||||||
|
ValueData = $stringValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Convert-HexStringToByteArray {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$hexValue
|
||||||
|
)
|
||||||
|
|
||||||
|
$parts = $hexValue.Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
||||||
|
$bytes = New-Object byte[] $parts.Count
|
||||||
|
|
||||||
|
for ($i = 0; $i -lt $parts.Count; $i++) {
|
||||||
|
$bytes[$i] = [byte]::Parse($parts[$i], [System.Globalization.NumberStyles]::HexNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
function Convert-RegistryByteArrayToString {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[byte[]]$byteData
|
||||||
|
)
|
||||||
|
|
||||||
|
return ([System.Text.Encoding]::Unicode.GetString($byteData)).TrimEnd([char]0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Convert-RegistryByteArrayToMultiString {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[byte[]]$byteData
|
||||||
|
)
|
||||||
|
|
||||||
|
return @(([System.Text.Encoding]::Unicode.GetString($byteData)).TrimEnd([char]0) -split "`0" | Where-Object { $_ -ne '' })
|
||||||
|
}
|
||||||
43
Scripts/Helpers/GetFriendlyRegistryBackupTarget.ps1
Normal file
43
Scripts/Helpers/GetFriendlyRegistryBackupTarget.ps1
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
function GetFriendlyRegistryBackupTarget {
|
||||||
|
param(
|
||||||
|
[AllowNull()]
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[string]$Target
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Target)) {
|
||||||
|
return 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Target -eq 'DefaultUserProfile') {
|
||||||
|
return 'Default user profile'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Target -eq 'CurrentUser') {
|
||||||
|
return 'Current user'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Target -eq 'AllUsers') {
|
||||||
|
return 'All users'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Target -like 'CurrentUser:*') {
|
||||||
|
$userName = $Target.Substring(12)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($userName)) {
|
||||||
|
return 'Current user'
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Current user ($userName)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Target -like 'User:*') {
|
||||||
|
$userName = $Target.Substring(5)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($userName)) {
|
||||||
|
return 'User'
|
||||||
|
}
|
||||||
|
|
||||||
|
return "User ($userName)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return $Target
|
||||||
|
}
|
||||||
@@ -7,25 +7,43 @@ function GetUserDirectory {
|
|||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (-not (CheckIfUserExists -userName $userName) -and $userName -ne "*") {
|
if ($userName -eq "*") {
|
||||||
Write-Error "User $userName does not exist on this system"
|
$rootPaths = @(
|
||||||
AwaitKeyToExit
|
(Join-Path $env:SystemDrive 'Users')
|
||||||
|
(Split-Path -Path $env:USERPROFILE -Parent)
|
||||||
|
) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
|
||||||
|
|
||||||
|
foreach ($rootPath in $rootPaths) {
|
||||||
|
if (-not (Test-Path -LiteralPath $rootPath -PathType Container)) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
$userDirectoryExists = Test-Path "$env:SystemDrive\Users\$userName"
|
$wildcardPath = if ([string]::IsNullOrWhiteSpace($fileName)) {
|
||||||
$userPath = "$env:SystemDrive\Users\$userName\$fileName"
|
Join-Path $rootPath '*'
|
||||||
|
}
|
||||||
if ((Test-Path $userPath) -or ($userDirectoryExists -and (-not $exitIfPathNotFound))) {
|
else {
|
||||||
return $userPath
|
Join-Path (Join-Path $rootPath '*') $fileName
|
||||||
}
|
}
|
||||||
|
|
||||||
$userDirectoryExists = Test-Path ($env:USERPROFILE -Replace ('\\' + $env:USERNAME + '$'), "\$userName")
|
return $wildcardPath
|
||||||
$userPath = $env:USERPROFILE -Replace ('\\' + $env:USERNAME + '$'), "\$userName\$fileName"
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ((Test-Path $userPath) -or ($userDirectoryExists -and (-not $exitIfPathNotFound))) {
|
$userContext = ResolveUserProfileContext -UserName $userName
|
||||||
|
$resolvedUserDirectory = if ($userContext) { $userContext.ProfilePath } else { $null }
|
||||||
|
if ($resolvedUserDirectory) {
|
||||||
|
$userPath = if ([string]::IsNullOrWhiteSpace($fileName)) {
|
||||||
|
$resolvedUserDirectory
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Join-Path $resolvedUserDirectory $fileName
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((Test-Path -LiteralPath $userPath) -or ((Test-Path -LiteralPath $resolvedUserDirectory -PathType Container) -and (-not $exitIfPathNotFound))) {
|
||||||
return $userPath
|
return $userPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch {
|
catch {
|
||||||
Write-Error "Something went wrong when trying to find the user directory path for user $userName. Please ensure the user exists on this system"
|
Write-Error "Something went wrong when trying to find the user directory path for user $userName. Please ensure the user exists on this system"
|
||||||
AwaitKeyToExit
|
AwaitKeyToExit
|
||||||
|
|||||||
80
Scripts/Helpers/RegistryPathHelpers.ps1
Normal file
80
Scripts/Helpers/RegistryPathHelpers.ps1
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
function Split-RegistryPath {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$path
|
||||||
|
)
|
||||||
|
|
||||||
|
$normalizedPath = [string]$path
|
||||||
|
if ([string]::IsNullOrWhiteSpace($normalizedPath)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedPath = $normalizedPath.Trim().Replace('/', '\')
|
||||||
|
if ([string]::IsNullOrWhiteSpace($normalizedPath)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($normalizedPath -notmatch '^(?<hive>HKEY_[^\\]+)(?:\\(?<subKey>.*))?$') {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$hiveName = [string]$matches.hive
|
||||||
|
|
||||||
|
$normalizedSubKey = if ($null -ne $matches.subKey) {
|
||||||
|
([string]$matches.subKey).Trim('\\')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hiveName.Equals('HKEY_USERS', [System.StringComparison]::OrdinalIgnoreCase) -and -not [string]::IsNullOrWhiteSpace($normalizedSubKey)) {
|
||||||
|
if ($normalizedSubKey -match '^(?<mount>[^\\]+)(?:\\(?<rest>.*))?$') {
|
||||||
|
$mountName = [string]$matches.mount
|
||||||
|
if ($mountName.Equals('.DEFAULT', [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
$remainingSubKey = if ($matches.rest) { [string]$matches.rest } else { '' }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($remainingSubKey)) {
|
||||||
|
$normalizedSubKey = 'Default'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$normalizedSubKey = "Default\$remainingSubKey"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Hive = $hiveName
|
||||||
|
SubKey = $normalizedSubKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RegistryRootKey {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$hiveName
|
||||||
|
)
|
||||||
|
|
||||||
|
switch ($hiveName.ToUpperInvariant()) {
|
||||||
|
'HKEY_CURRENT_USER' { return [Microsoft.Win32.Registry]::CurrentUser }
|
||||||
|
'HKEY_LOCAL_MACHINE' { return [Microsoft.Win32.Registry]::LocalMachine }
|
||||||
|
'HKEY_CLASSES_ROOT' { return [Microsoft.Win32.Registry]::ClassesRoot }
|
||||||
|
'HKEY_USERS' { return [Microsoft.Win32.Registry]::Users }
|
||||||
|
'HKEY_CURRENT_CONFIG' { return [Microsoft.Win32.Registry]::CurrentConfig }
|
||||||
|
default { return $null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RegistryFilePathForFeature {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$RegistryKey,
|
||||||
|
[switch]$UseSysprepRegFiles
|
||||||
|
)
|
||||||
|
|
||||||
|
$useSysprepLayout = $UseSysprepRegFiles -or $script:Params.ContainsKey('Sysprep') -or $script:Params.ContainsKey('User')
|
||||||
|
if ($useSysprepLayout) {
|
||||||
|
return Join-Path (Join-Path $script:RegfilesPath 'Sysprep') $RegistryKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return Join-Path $script:RegfilesPath $RegistryKey
|
||||||
|
}
|
||||||
382
Scripts/Helpers/ResolveUserProfilePath.ps1
Normal file
382
Scripts/Helpers/ResolveUserProfilePath.ps1
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
function NormalizeUserLookupValue {
|
||||||
|
param(
|
||||||
|
[string]$Value
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Value)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove zero-width characters and normalize whitespace for robust comparisons.
|
||||||
|
$normalized = $Value -replace '[\u200B-\u200D\uFEFF]', ''
|
||||||
|
$normalized = $normalized.Trim() -replace '\s+', ' '
|
||||||
|
return $normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $script:ResolvedUserSidCache) {
|
||||||
|
$script:ResolvedUserSidCache = @{}
|
||||||
|
}
|
||||||
|
|
||||||
|
function GetUserLookupCacheKey {
|
||||||
|
param(
|
||||||
|
[string]$Value
|
||||||
|
)
|
||||||
|
|
||||||
|
$normalizedValue = NormalizeUserLookupValue -Value $Value
|
||||||
|
if ([string]::IsNullOrWhiteSpace($normalizedValue)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalizedValue.ToLowerInvariant()
|
||||||
|
}
|
||||||
|
|
||||||
|
function EscapeWqlString {
|
||||||
|
param(
|
||||||
|
[string]$Value
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $Value) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return $Value -replace "'", "''"
|
||||||
|
}
|
||||||
|
|
||||||
|
function GetLocalUserNameSegment {
|
||||||
|
param(
|
||||||
|
[string]$UserName
|
||||||
|
)
|
||||||
|
|
||||||
|
$normalizedName = NormalizeUserLookupValue -Value $UserName
|
||||||
|
if ([string]::IsNullOrWhiteSpace($normalizedName)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($normalizedName.Contains('\')) {
|
||||||
|
return NormalizeUserLookupValue -Value (($normalizedName -split '\\')[-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($normalizedName.Contains('@')) {
|
||||||
|
return NormalizeUserLookupValue -Value (($normalizedName -split '@')[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalizedName
|
||||||
|
}
|
||||||
|
|
||||||
|
function SetResolvedUserSidCache {
|
||||||
|
param(
|
||||||
|
[string[]]$Candidates,
|
||||||
|
[string]$Sid
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Sid)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($candidate in @($Candidates)) {
|
||||||
|
$cacheKey = GetUserLookupCacheKey -Value $candidate
|
||||||
|
if ($cacheKey) {
|
||||||
|
$script:ResolvedUserSidCache[$cacheKey] = $Sid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function GetCachedResolvedUserSid {
|
||||||
|
param(
|
||||||
|
[string[]]$Candidates
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($candidate in @($Candidates)) {
|
||||||
|
$cacheKey = GetUserLookupCacheKey -Value $candidate
|
||||||
|
if ($cacheKey -and $script:ResolvedUserSidCache.ContainsKey($cacheKey)) {
|
||||||
|
return $script:ResolvedUserSidCache[$cacheKey]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function TryResolveSidByNtAccount {
|
||||||
|
param(
|
||||||
|
[string]$UserName
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($UserName)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$ntAccount = [System.Security.Principal.NTAccount]::new($UserName)
|
||||||
|
$sid = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier])
|
||||||
|
if ($sid) {
|
||||||
|
return $sid.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# Fallback handled by caller.
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function TryResolveSidByLocalLookup {
|
||||||
|
param(
|
||||||
|
[string[]]$Candidates
|
||||||
|
)
|
||||||
|
|
||||||
|
$lookupCandidates = @($Candidates) | ForEach-Object { NormalizeUserLookupValue -Value $_ } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
|
||||||
|
if ($lookupCandidates.Count -eq 0) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Get-Command -Name Get-LocalUser -ErrorAction SilentlyContinue) {
|
||||||
|
foreach ($candidate in $lookupCandidates) {
|
||||||
|
try {
|
||||||
|
$matchingLocalUser = Get-LocalUser -Name $candidate -ErrorAction Stop | Select-Object -First 1
|
||||||
|
if ($matchingLocalUser -and $matchingLocalUser.SID) {
|
||||||
|
return $matchingLocalUser.SID.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# Continue to next lookup strategy.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($candidate in $lookupCandidates) {
|
||||||
|
try {
|
||||||
|
$escapedCandidate = EscapeWqlString -Value $candidate
|
||||||
|
$escapedComputerName = EscapeWqlString -Value $env:COMPUTERNAME
|
||||||
|
$filter = "LocalAccount=True AND (Name='$escapedCandidate' OR FullName='$escapedCandidate' OR Caption='$escapedComputerName\$escapedCandidate')"
|
||||||
|
$matchingAccount = Get-CimInstance -ClassName Win32_UserAccount -Filter $filter -ErrorAction Stop | Select-Object -First 1
|
||||||
|
|
||||||
|
if ($matchingAccount -and $matchingAccount.SID) {
|
||||||
|
return $matchingAccount.SID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# Continue to next lookup strategy.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function TryResolveSidFromProfileList {
|
||||||
|
param(
|
||||||
|
[string[]]$Candidates
|
||||||
|
)
|
||||||
|
|
||||||
|
$lookupCandidates = @($Candidates) | ForEach-Object { NormalizeUserLookupValue -Value $_ } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
|
||||||
|
if ($lookupCandidates.Count -eq 0) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$profileListPath = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList'
|
||||||
|
foreach ($sidKey in @(Get-ChildItem -LiteralPath $profileListPath -ErrorAction Stop)) {
|
||||||
|
try {
|
||||||
|
$imagePath = Get-ItemPropertyValue -LiteralPath $sidKey.PSPath -Name 'ProfileImagePath' -ErrorAction Stop
|
||||||
|
if ([string]::IsNullOrWhiteSpace($imagePath)) { continue }
|
||||||
|
|
||||||
|
$expandedPath = [System.Environment]::ExpandEnvironmentVariables($imagePath)
|
||||||
|
$leafName = NormalizeUserLookupValue -Value (Split-Path -Leaf $expandedPath)
|
||||||
|
|
||||||
|
foreach ($candidate in $lookupCandidates) {
|
||||||
|
if ($leafName -ieq $candidate) {
|
||||||
|
return $sidKey.PSChildName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# Fallback handled by caller.
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function NewResolvedUserContext {
|
||||||
|
param(
|
||||||
|
[string]$UserName,
|
||||||
|
[string]$UserSid,
|
||||||
|
[string]$ProfilePath
|
||||||
|
)
|
||||||
|
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
UserName = $UserName
|
||||||
|
UserSid = $UserSid
|
||||||
|
ProfilePath = $ProfilePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResolveUserSid {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$UserName
|
||||||
|
)
|
||||||
|
|
||||||
|
$candidateUserName = NormalizeUserLookupValue -Value $UserName
|
||||||
|
if ([string]::IsNullOrWhiteSpace($candidateUserName)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasQualifiedIdentity = $candidateUserName.Contains('\') -or $candidateUserName.Contains('@')
|
||||||
|
$localNameSegment = GetLocalUserNameSegment -UserName $candidateUserName
|
||||||
|
$leafNameCandidates = @()
|
||||||
|
if ($hasQualifiedIdentity -and -not [string]::IsNullOrWhiteSpace($localNameSegment) -and $localNameSegment -ine $candidateUserName) {
|
||||||
|
$leafNameCandidates = @($localNameSegment)
|
||||||
|
}
|
||||||
|
|
||||||
|
$cacheCandidates = if ($hasQualifiedIdentity) {
|
||||||
|
@($candidateUserName)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
@($candidateUserName) + $leafNameCandidates | Select-Object -Unique
|
||||||
|
}
|
||||||
|
|
||||||
|
$localLookupCandidates = if ($hasQualifiedIdentity) {
|
||||||
|
@()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
@($candidateUserName) + $leafNameCandidates | Select-Object -Unique
|
||||||
|
}
|
||||||
|
|
||||||
|
$profileHeuristicCandidates = if ($leafNameCandidates.Count -gt 0) {
|
||||||
|
$leafNameCandidates
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
@($candidateUserName)
|
||||||
|
}
|
||||||
|
|
||||||
|
$cachedSid = GetCachedResolvedUserSid -Candidates $cacheCandidates
|
||||||
|
if ($cachedSid) {
|
||||||
|
return $cachedSid
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve fully-qualified identities first to avoid accidentally matching a local leaf account.
|
||||||
|
if ($hasQualifiedIdentity) {
|
||||||
|
$resolvedSid = TryResolveSidByNtAccount -UserName $candidateUserName
|
||||||
|
if ($resolvedSid) {
|
||||||
|
SetResolvedUserSidCache -Candidates $cacheCandidates -Sid $resolvedSid
|
||||||
|
return $resolvedSid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedSid = TryResolveSidByLocalLookup -Candidates $localLookupCandidates
|
||||||
|
if ($resolvedSid) {
|
||||||
|
SetResolvedUserSidCache -Candidates $cacheCandidates -Sid $resolvedSid
|
||||||
|
return $resolvedSid
|
||||||
|
}
|
||||||
|
|
||||||
|
# Last-ditch NTAccount translation for non-qualified names.
|
||||||
|
if (-not $hasQualifiedIdentity) {
|
||||||
|
$resolvedSid = TryResolveSidByNtAccount -UserName $candidateUserName
|
||||||
|
if ($resolvedSid) {
|
||||||
|
SetResolvedUserSidCache -Candidates $cacheCandidates -Sid $resolvedSid
|
||||||
|
return $resolvedSid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedSid = TryResolveSidFromProfileList -Candidates $profileHeuristicCandidates
|
||||||
|
if ($resolvedSid) {
|
||||||
|
SetResolvedUserSidCache -Candidates $cacheCandidates -Sid $resolvedSid
|
||||||
|
return $resolvedSid
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResolveUserProfilePath {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$UserName
|
||||||
|
)
|
||||||
|
|
||||||
|
$userContext = ResolveUserProfileContext -UserName $UserName
|
||||||
|
if ($userContext) {
|
||||||
|
return $userContext.ProfilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResolveUserProfileContext {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$UserName
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($UserName)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidateUserName = NormalizeUserLookupValue -Value $UserName
|
||||||
|
$rootPaths = @(
|
||||||
|
(Join-Path $env:SystemDrive 'Users')
|
||||||
|
(Split-Path -Path $env:USERPROFILE -Parent)
|
||||||
|
) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
|
||||||
|
|
||||||
|
if ($candidateUserName -ieq 'Default') {
|
||||||
|
foreach ($rootPath in $rootPaths) {
|
||||||
|
if (-not (Test-Path -LiteralPath $rootPath -PathType Container)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaultProfilePath = Join-Path $rootPath 'Default'
|
||||||
|
if (Test-Path -LiteralPath $defaultProfilePath -PathType Container) {
|
||||||
|
return (NewResolvedUserContext -UserName $candidateUserName -UserSid $null -ProfilePath $defaultProfilePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$userSid = ResolveUserSid -UserName $candidateUserName
|
||||||
|
|
||||||
|
if ($userSid) {
|
||||||
|
$sidRegistryPath = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$userSid"
|
||||||
|
try {
|
||||||
|
if (Test-Path -LiteralPath $sidRegistryPath) {
|
||||||
|
$registryImagePath = Get-ItemPropertyValue -LiteralPath $sidRegistryPath -Name 'ProfileImagePath' -ErrorAction Stop
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($registryImagePath)) {
|
||||||
|
$expandedPath = [System.Environment]::ExpandEnvironmentVariables($registryImagePath)
|
||||||
|
if (Test-Path -LiteralPath $expandedPath -PathType Container) {
|
||||||
|
return (NewResolvedUserContext -UserName $candidateUserName -UserSid $userSid -ProfilePath $expandedPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# Try Win32_UserProfile fallback.
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$matchingProfiles = @(Get-CimInstance -ClassName Win32_UserProfile -Filter "SID='$userSid'" -ErrorAction Stop)
|
||||||
|
$resolvedProfile = $matchingProfiles | Where-Object { -not [string]::IsNullOrWhiteSpace($_.LocalPath) } | Select-Object -First 1
|
||||||
|
if ($resolvedProfile -and (Test-Path -LiteralPath $resolvedProfile.LocalPath -PathType Container)) {
|
||||||
|
return (NewResolvedUserContext -UserName $candidateUserName -UserSid $userSid -ProfilePath $resolvedProfile.LocalPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# Fall through to legacy path probing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rootPath in $rootPaths) {
|
||||||
|
if (-not (Test-Path -LiteralPath $rootPath -PathType Container)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidateUserPath = Join-Path $rootPath $candidateUserName
|
||||||
|
if (Test-Path -LiteralPath $candidateUserPath -PathType Container) {
|
||||||
|
return (NewResolvedUserContext -UserName $candidateUserName -UserSid $userSid -ProfilePath $candidateUserPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
47
Scripts/Helpers/Test-TargetUserName.ps1
Normal file
47
Scripts/Helpers/Test-TargetUserName.ps1
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
function Test-TargetUserName {
|
||||||
|
param(
|
||||||
|
[AllowNull()]
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[string]$UserName
|
||||||
|
)
|
||||||
|
|
||||||
|
$normalizedUserName = if ($null -ne $UserName) { $UserName.Trim() } else { '' }
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($normalizedUserName)) {
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
IsValid = $false
|
||||||
|
UserName = $normalizedUserName
|
||||||
|
Message = 'Please enter a username'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($normalizedUserName -eq $env:USERNAME) {
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
IsValid = $false
|
||||||
|
UserName = $normalizedUserName
|
||||||
|
Message = "Cannot enter your own username, use 'Current User' option instead"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (CheckIfUserExists -userName $normalizedUserName)) {
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
IsValid = $false
|
||||||
|
UserName = $normalizedUserName
|
||||||
|
Message = 'User not found, please enter a valid username'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TestIfUserIsLoggedIn -Username $normalizedUserName) {
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
IsValid = $false
|
||||||
|
UserName = $normalizedUserName
|
||||||
|
Message = "User '$normalizedUserName' is currently logged in. Please sign out that user first."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
IsValid = $true
|
||||||
|
UserName = $normalizedUserName
|
||||||
|
Message = "User found: $normalizedUserName"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -141,24 +141,31 @@ if (-not $isAdmin) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Define script-level variables & paths
|
# Define script-level variables & paths
|
||||||
$script:Version = "2026.04.26"
|
$script:Version = "2026.05.20"
|
||||||
$script:AppsListFilePath = "$PSScriptRoot/Config/Apps.json"
|
$configPath = Join-Path $PSScriptRoot 'Config'
|
||||||
$script:DefaultSettingsFilePath = "$PSScriptRoot/Config/DefaultSettings.json"
|
$logsPath = Join-Path $PSScriptRoot 'Logs'
|
||||||
$script:FeaturesFilePath = "$PSScriptRoot/Config/Features.json"
|
$schemasPath = Join-Path $PSScriptRoot 'Schemas'
|
||||||
$script:SavedSettingsFilePath = "$PSScriptRoot/Config/LastUsedSettings.json"
|
$scriptsPath = Join-Path $PSScriptRoot 'Scripts'
|
||||||
$script:CustomAppsListFilePath = "$PSScriptRoot/Config/CustomAppsList"
|
|
||||||
$script:DefaultLogPath = "$PSScriptRoot/Logs/Win11Debloat.log"
|
$script:AppsListFilePath = Join-Path $configPath 'Apps.json'
|
||||||
$script:RegfilesPath = "$PSScriptRoot/Regfiles"
|
$script:DefaultSettingsFilePath = Join-Path $configPath 'DefaultSettings.json'
|
||||||
$script:AssetsPath = "$PSScriptRoot/Assets"
|
$script:FeaturesFilePath = Join-Path $configPath 'Features.json'
|
||||||
$script:AppSelectionSchema = "$PSScriptRoot/Schemas/AppSelectionWindow.xaml"
|
$script:SavedSettingsFilePath = Join-Path $configPath 'LastUsedSettings.json'
|
||||||
$script:MainWindowSchema = "$PSScriptRoot/Schemas/MainWindow.xaml"
|
$script:CustomAppsListFilePath = Join-Path $configPath 'CustomAppsList'
|
||||||
$script:MessageBoxSchema = "$PSScriptRoot/Schemas/MessageBoxWindow.xaml"
|
$script:DefaultLogPath = Join-Path $logsPath 'Win11Debloat.log'
|
||||||
$script:AboutWindowSchema = "$PSScriptRoot/Schemas/AboutWindow.xaml"
|
$script:RegfilesPath = Join-Path $PSScriptRoot 'Regfiles'
|
||||||
$script:ApplyChangesWindowSchema = "$PSScriptRoot/Schemas/ApplyChangesWindow.xaml"
|
$script:RegistryBackupsPath = Join-Path $PSScriptRoot 'Backups'
|
||||||
$script:SharedStylesSchema = "$PSScriptRoot/Schemas/SharedStyles.xaml"
|
$script:AssetsPath = Join-Path $PSScriptRoot 'Assets'
|
||||||
$script:BubbleHintSchema = "$PSScriptRoot/Schemas/BubbleHint.xaml"
|
$script:AppSelectionSchema = Join-Path $schemasPath 'AppSelectionWindow.xaml'
|
||||||
$script:ImportExportConfigSchema = "$PSScriptRoot/Schemas/ImportExportConfigWindow.xaml"
|
$script:MainWindowSchema = Join-Path $schemasPath 'MainWindow.xaml'
|
||||||
$script:LoadAppsDetailsScriptPath = "$PSScriptRoot/Scripts/FileIO/LoadAppsDetailsFromJson.ps1"
|
$script:MessageBoxSchema = Join-Path $schemasPath 'MessageBoxWindow.xaml'
|
||||||
|
$script:AboutWindowSchema = Join-Path $schemasPath 'AboutWindow.xaml'
|
||||||
|
$script:ApplyChangesWindowSchema = Join-Path $schemasPath 'ApplyChangesWindow.xaml'
|
||||||
|
$script:SharedStylesSchema = Join-Path $schemasPath 'SharedStyles.xaml'
|
||||||
|
$script:BubbleHintSchema = Join-Path $schemasPath 'BubbleHint.xaml'
|
||||||
|
$script:ImportExportConfigSchema = Join-Path $schemasPath 'ImportExportConfigWindow.xaml'
|
||||||
|
$script:RestoreBackupWindowSchema = Join-Path $schemasPath 'RestoreBackupWindow.xaml'
|
||||||
|
$script:LoadAppsDetailsScriptPath = Join-Path (Join-Path $scriptsPath 'FileIO') 'LoadAppsDetailsFromJson.ps1'
|
||||||
|
|
||||||
$script:ControlParams = 'WhatIf', 'Confirm', 'Verbose', 'Debug', 'LogPath', 'Silent', 'Sysprep', 'User', 'NoRestartExplorer', 'RunDefaults', 'RunDefaultsLite', 'RunSavedSettings', 'Config', 'RunAppsListGenerator', 'CLI', 'AppRemovalTarget'
|
$script:ControlParams = 'WhatIf', 'Confirm', 'Verbose', 'Debug', 'LogPath', 'Silent', 'Sysprep', 'User', 'NoRestartExplorer', 'RunDefaults', 'RunDefaultsLite', 'RunSavedSettings', 'Config', 'RunAppsListGenerator', 'CLI', 'AppRemovalTarget'
|
||||||
|
|
||||||
@@ -209,14 +216,14 @@ Write-Host ""
|
|||||||
|
|
||||||
# Log script output to 'Win11Debloat.log' at the specified path
|
# Log script output to 'Win11Debloat.log' at the specified path
|
||||||
if ($LogPath -and (Test-Path $LogPath)) {
|
if ($LogPath -and (Test-Path $LogPath)) {
|
||||||
Start-Transcript -Path "$LogPath/Win11Debloat.log" -Append -IncludeInvocationHeader -Force | Out-Null
|
Start-Transcript -Path (Join-Path $LogPath 'Win11Debloat.log') -Append -IncludeInvocationHeader -Force | Out-Null
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Start-Transcript -Path $script:DefaultLogPath -Append -IncludeInvocationHeader -Force | Out-Null
|
Start-Transcript -Path $script:DefaultLogPath -Append -IncludeInvocationHeader -Force | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if script has all required files
|
# 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))) {
|
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:RestoreBackupWindowSchema) -and (Test-Path $script:FeaturesFilePath))) {
|
||||||
Write-Error "Win11Debloat is unable to find required files, please ensure all script files are present"
|
Write-Error "Win11Debloat is unable to find required files, please ensure all script files are present"
|
||||||
Write-Output ""
|
Write-Output ""
|
||||||
Write-Output "Press any key to exit..."
|
Write-Output "Press any key to exit..."
|
||||||
@@ -286,8 +293,15 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
|||||||
. "$PSScriptRoot/Scripts/CLI/PrintHeader.ps1"
|
. "$PSScriptRoot/Scripts/CLI/PrintHeader.ps1"
|
||||||
|
|
||||||
# Features functions
|
# Features functions
|
||||||
|
. "$PSScriptRoot/Scripts/Features/GetCurrentTweakState.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Features/ExecuteChanges.ps1"
|
. "$PSScriptRoot/Scripts/Features/ExecuteChanges.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Features/CreateSystemRestorePoint.ps1"
|
. "$PSScriptRoot/Scripts/Features/CreateSystemRestorePoint.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/Features/BackupRegistryFeatureSelection.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/Features/BackupRegistrySnapshotCapture.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/Features/BackupRegistryState.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/Features/RegistryBackupValidation.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/Features/RestoreRegistryApplyState.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/Features/RestoreRegistryBackup.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Features/DisableStoreSearchSuggestions.ps1"
|
. "$PSScriptRoot/Scripts/Features/DisableStoreSearchSuggestions.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Features/EnableWindowsFeature.ps1"
|
. "$PSScriptRoot/Scripts/Features/EnableWindowsFeature.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Features/ImportRegistryFile.ps1"
|
. "$PSScriptRoot/Scripts/Features/ImportRegistryFile.ps1"
|
||||||
@@ -314,20 +328,29 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
|||||||
. "$PSScriptRoot/Scripts/GUI/Show-ConfigWindow.ps1"
|
. "$PSScriptRoot/Scripts/GUI/Show-ConfigWindow.ps1"
|
||||||
. "$PSScriptRoot/Scripts/GUI/Show-ApplyModal.ps1"
|
. "$PSScriptRoot/Scripts/GUI/Show-ApplyModal.ps1"
|
||||||
. "$PSScriptRoot/Scripts/GUI/Show-AppSelectionWindow.ps1"
|
. "$PSScriptRoot/Scripts/GUI/Show-AppSelectionWindow.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/GUI/Show-RestoreBackupWindow.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/GUI/RestoreBackupDialogFeatureLists.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/GUI/Show-RestoreBackupDialog.ps1"
|
||||||
. "$PSScriptRoot/Scripts/GUI/Show-MainWindow.ps1"
|
. "$PSScriptRoot/Scripts/GUI/Show-MainWindow.ps1"
|
||||||
. "$PSScriptRoot/Scripts/GUI/Show-AboutDialog.ps1"
|
. "$PSScriptRoot/Scripts/GUI/Show-AboutDialog.ps1"
|
||||||
. "$PSScriptRoot/Scripts/GUI/Show-Bubble.ps1"
|
. "$PSScriptRoot/Scripts/GUI/Show-Bubble.ps1"
|
||||||
|
|
||||||
# Helper functions
|
# Helper functions
|
||||||
. "$PSScriptRoot/Scripts/Helpers/AddParameter.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/AddParameter.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/Helpers/ResolveUserProfilePath.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Helpers/CheckIfUserExists.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/CheckIfUserExists.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Helpers/CheckModernStandbySupport.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/CheckModernStandbySupport.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Helpers/GenerateAppsList.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/GenerateAppsList.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/Helpers/GetFriendlyRegistryBackupTarget.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Helpers/GetFriendlyTargetUserName.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/GetFriendlyTargetUserName.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Helpers/ImportConfigToParams.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/ImportConfigToParams.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Helpers/GetTargetUserForAppRemoval.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/GetTargetUserForAppRemoval.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/Helpers/Get-RegFileOperations.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/Helpers/Test-TargetUserName.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Helpers/GetUserDirectory.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/GetUserDirectory.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Helpers/GetUserName.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/GetUserName.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/Helpers/RegistryPathHelpers.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/Helpers/ApplyRegistryRegFile.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Helpers/TestIfUserIsLoggedIn.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/TestIfUserIsLoggedIn.ps1"
|
||||||
|
|
||||||
# Threading functions
|
# Threading functions
|
||||||
@@ -351,6 +374,7 @@ $WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\Current
|
|||||||
$script:ModernStandbySupported = CheckModernStandbySupport
|
$script:ModernStandbySupported = CheckModernStandbySupport
|
||||||
|
|
||||||
$script:Params = $PSBoundParameters
|
$script:Params = $PSBoundParameters
|
||||||
|
$script:UndoParams = @{}
|
||||||
|
|
||||||
# Add default Apps parameter when RemoveApps is requested and Apps was not explicitly provided
|
# Add default Apps parameter when RemoveApps is requested and Apps was not explicitly provided
|
||||||
if ((-not $script:Params.ContainsKey("Apps")) -and $script:Params.ContainsKey("RemoveApps")) {
|
if ((-not $script:Params.ContainsKey("Apps")) -and $script:Params.ContainsKey("RemoveApps")) {
|
||||||
@@ -380,7 +404,7 @@ else {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($script:Params.ContainsKey("Sysprep")) {
|
if ($script:Params.ContainsKey("Sysprep")) {
|
||||||
$defaultUserPath = GetUserDirectory -userName "Default"
|
GetUserDirectory -userName "Default" | Out-Null
|
||||||
|
|
||||||
# Exit script if run in Sysprep mode on Windows 10
|
# Exit script if run in Sysprep mode on Windows 10
|
||||||
if ($WinVersion -lt 22000) {
|
if ($WinVersion -lt 22000) {
|
||||||
@@ -391,10 +415,10 @@ if ($script:Params.ContainsKey("Sysprep")) {
|
|||||||
|
|
||||||
# Ensure that target user exists, if User or AppRemovalTarget parameter was provided
|
# Ensure that target user exists, if User or AppRemovalTarget parameter was provided
|
||||||
if ($script:Params.ContainsKey("User")) {
|
if ($script:Params.ContainsKey("User")) {
|
||||||
$userPath = GetUserDirectory -userName $script:Params.Item("User")
|
GetUserDirectory -userName $script:Params.Item("User") | Out-Null
|
||||||
}
|
}
|
||||||
if ($script:Params.ContainsKey("AppRemovalTarget")) {
|
if ($script:Params.ContainsKey("AppRemovalTarget")) {
|
||||||
$userPath = GetUserDirectory -userName $script:Params.Item("AppRemovalTarget")
|
GetUserDirectory -userName $script:Params.Item("AppRemovalTarget") | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
# Remove LastUsedSettings.json file if it exists and is empty
|
# Remove LastUsedSettings.json file if it exists and is empty
|
||||||
|
|||||||
Reference in New Issue
Block a user