mirror of
https://github.com/Raphire/Win11Debloat.git
synced 2026-05-18 11:46:18 +00:00
Compare commits
14 Commits
2026.04.26
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a5cb986c9 | ||
|
|
66982ada28 | ||
|
|
489af33a8b | ||
|
|
51aa288dfd | ||
|
|
24a6f1bcf8 | ||
|
|
8ac664e45f | ||
|
|
85aa67b5d2 | ||
|
|
c8b4563954 | ||
|
|
22f3144c0f | ||
|
|
2c360961e3 | ||
|
|
11a324365d | ||
|
|
5daa922148 | ||
|
|
1826d6d8be | ||
|
|
c15309bcf6 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ LastUsedSettings.json
|
||||
CustomAppsList
|
||||
Logs/*
|
||||
Win11Debloat.log
|
||||
Backups/*
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,18 +11,7 @@
|
||||
Topmost="False"
|
||||
ShowInTaskbar="False">
|
||||
|
||||
<Border BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Background="{DynamicResource CardBgColor}"
|
||||
Margin="25">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="Black"
|
||||
Opacity="0.15"
|
||||
BlurRadius="20"
|
||||
ShadowDepth="0"
|
||||
Direction="0"/>
|
||||
</Border.Effect>
|
||||
<Border Style="{DynamicResource ModalCardBorderStyle}">
|
||||
|
||||
<Grid Margin="0">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -32,13 +21,9 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 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"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center"
|
||||
Margin="20,0,0,0"/>
|
||||
Style="{DynamicResource ModalTitleTextStyle}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Message Content -->
|
||||
@@ -50,7 +35,7 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Message Text -->
|
||||
<Grid Grid.Row="0" Margin="24,12,24,20">
|
||||
<Grid Grid.Row="0" Margin="20,4,20,20">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
@@ -64,41 +49,29 @@
|
||||
<!-- Version -->
|
||||
<TextBlock Grid.Row="0" Grid.Column="0"
|
||||
Text="Version:"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
FontWeight="SemiBold"
|
||||
Margin="0,0,16,8"/>
|
||||
Style="{DynamicResource ModalInfoLabelTextStyle}"/>
|
||||
<TextBlock x:Name="VersionText"
|
||||
Grid.Row="0" Grid.Column="1"
|
||||
Text="0.0.0"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
Margin="0,0,0,8"/>
|
||||
Style="{DynamicResource ModalInfoValueTextStyle}"/>
|
||||
|
||||
<!-- Author -->
|
||||
<TextBlock Grid.Row="1" Grid.Column="0"
|
||||
Text="Author:"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
FontWeight="SemiBold"
|
||||
Margin="0,0,16,8"/>
|
||||
Style="{DynamicResource ModalInfoLabelTextStyle}"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="1"
|
||||
Text="Raphire"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
Margin="0,0,0,8"/>
|
||||
Style="{DynamicResource ModalInfoValueTextStyle}"/>
|
||||
|
||||
<!-- Project Link -->
|
||||
<TextBlock Grid.Row="2" Grid.Column="0"
|
||||
Text="Project:"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
FontWeight="SemiBold"
|
||||
Style="{DynamicResource ModalInfoLabelTextStyle}"
|
||||
Margin="0,0,16,0"/>
|
||||
<TextBlock x:Name="ProjectLink"
|
||||
Grid.Row="2" Grid.Column="1"
|
||||
Text="https://github.com/Raphire/Win11Debloat"
|
||||
FontSize="14"
|
||||
FontSize="13"
|
||||
Style="{DynamicResource HyperlinkStyle}"
|
||||
Margin="0,0,0,0"/>
|
||||
</Grid>
|
||||
@@ -107,10 +80,10 @@
|
||||
<Border Grid.Row="1"
|
||||
Height="1"
|
||||
Background="{DynamicResource BorderColor}"
|
||||
Margin="10,0"/>
|
||||
Margin="20,0"/>
|
||||
|
||||
<!-- Content -->
|
||||
<StackPanel Grid.Row="2" Margin="24,20">
|
||||
<StackPanel Grid.Row="2" Margin="20,18,20,20">
|
||||
<!-- 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!"
|
||||
FontSize="14"
|
||||
@@ -150,19 +123,11 @@
|
||||
</Grid>
|
||||
|
||||
<!-- Button Panel -->
|
||||
<Border Grid.Row="2"
|
||||
Background="{DynamicResource BgColor}"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="16,12"
|
||||
CornerRadius="0,0,8,8">
|
||||
<StackPanel x:Name="ButtonPanel"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right">
|
||||
<Border Grid.Row="2" Style="{DynamicResource ModalFooterBorderStyle}">
|
||||
<StackPanel x:Name="ButtonPanel" Style="{DynamicResource ModalFooterButtonsRightStyle}">
|
||||
<Button x:Name="CloseButton"
|
||||
Content="Close"
|
||||
Height="32" MinWidth="80" Margin="4,0"
|
||||
Style="{DynamicResource SecondaryButtonStyle}"/>
|
||||
Style="{DynamicResource ModalSecondaryActionButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
@@ -11,18 +11,7 @@
|
||||
Topmost="False"
|
||||
ShowInTaskbar="False">
|
||||
|
||||
<Border BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Background="{DynamicResource CardBgColor}"
|
||||
Margin="25">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="Black"
|
||||
Opacity="0.15"
|
||||
BlurRadius="20"
|
||||
ShadowDepth="0"
|
||||
Direction="0"/>
|
||||
</Border.Effect>
|
||||
<Border Style="{DynamicResource ModalCardBorderStyle}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
@@ -100,11 +89,7 @@
|
||||
MaxWidth="430"/>
|
||||
</StackPanel>
|
||||
|
||||
<Border Background="{DynamicResource BgColor}"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="16,12"
|
||||
CornerRadius="0,0,8,8">
|
||||
<Border Style="{DynamicResource ModalFooterBorderStyle}">
|
||||
<StackPanel>
|
||||
|
||||
<!-- Progress bar -->
|
||||
@@ -172,29 +157,22 @@
|
||||
</Border>
|
||||
|
||||
<!-- Button Panel -->
|
||||
<Border Background="{DynamicResource BgColor}"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="16,12"
|
||||
CornerRadius="0,0,8,8">
|
||||
<StackPanel x:Name="ButtonPanel"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Center">
|
||||
<Button x:Name="ApplyKofiBtn" Width="210" Height="32"
|
||||
Style="{DynamicResource SecondaryButtonStyle}"
|
||||
Margin="0,0,12,0"
|
||||
<Border Style="{DynamicResource ModalFooterBorderStyle}">
|
||||
<UniformGrid x:Name="ButtonPanel" Rows="1">
|
||||
<Button x:Name="ApplyKofiBtn"
|
||||
Style="{DynamicResource ModalSecondaryStretchedButtonStyle}"
|
||||
AutomationProperties.Name="Support the creator">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<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"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button x:Name="ApplyCloseBtn" Width="100" Height="32"
|
||||
Style="{DynamicResource PrimaryButtonStyle}"
|
||||
<Button x:Name="ApplyCloseBtn"
|
||||
Style="{DynamicResource ModalPrimaryStretchedButtonStyle}"
|
||||
AutomationProperties.Name="Close">
|
||||
<TextBlock Text="Close" VerticalAlignment="Center" FontSize="14" Margin="0,0,0,1"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</UniformGrid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
@@ -12,18 +12,7 @@
|
||||
Topmost="False"
|
||||
ShowInTaskbar="False">
|
||||
|
||||
<Border BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Background="{DynamicResource CardBgColor}"
|
||||
Margin="25">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="Black"
|
||||
Opacity="0.15"
|
||||
BlurRadius="20"
|
||||
ShadowDepth="0"
|
||||
Direction="0"/>
|
||||
</Border.Effect>
|
||||
<Border Style="{DynamicResource ModalCardBorderStyle}">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
@@ -33,18 +22,14 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 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"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center"
|
||||
Margin="16,0,0,0"/>
|
||||
Style="{DynamicResource ModalTitleTextStyle}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Content -->
|
||||
<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"
|
||||
TextWrapping="Wrap"
|
||||
FontSize="14"
|
||||
@@ -57,22 +42,15 @@
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Button Footer -->
|
||||
<Border Grid.Row="2"
|
||||
Background="{DynamicResource BgColor}"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="16,12"
|
||||
CornerRadius="0,0,8,8">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Border Grid.Row="2" Style="{DynamicResource ModalFooterBorderStyle}">
|
||||
<UniformGrid Rows="1">
|
||||
<Button x:Name="OkButton"
|
||||
Content="OK"
|
||||
Height="32" MinWidth="80" Margin="4,0"
|
||||
Style="{DynamicResource PrimaryButtonStyle}"/>
|
||||
Style="{DynamicResource ModalPrimaryStretchedButtonStyle}"/>
|
||||
<Button x:Name="CancelButton"
|
||||
Content="Cancel"
|
||||
Height="32" MinWidth="80" Margin="4,0"
|
||||
Style="{DynamicResource SecondaryButtonStyle}"/>
|
||||
</StackPanel>
|
||||
Style="{DynamicResource ModalSecondaryStretchedButtonStyle}"/>
|
||||
</UniformGrid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
|
||||
Title="Win11Debloat"
|
||||
MinWidth="1130" MinHeight="600"
|
||||
MinWidth="860" MinHeight="600"
|
||||
ResizeMode="CanResize"
|
||||
SnapsToDevicePixels="True"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
@@ -28,177 +28,6 @@
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</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 -->
|
||||
<Style x:Key="LabelStyle" TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
@@ -582,6 +411,11 @@
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
|
||||
</MenuItem.Icon>
|
||||
</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 />
|
||||
<MenuItem x:Name="MenuDocumentation" Header="Documentation" AutomationProperties.Name="Documentation">
|
||||
<MenuItem.Icon>
|
||||
|
||||
@@ -44,8 +44,7 @@
|
||||
</Grid>
|
||||
|
||||
<!-- Message Content -->
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" MaxHeight="500" Padding="0" Margin="20,12,1,20">
|
||||
<Grid Margin="0,0,20,0">
|
||||
<Grid Grid.Row="1" Margin="20,12,1,20">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
@@ -58,20 +57,21 @@
|
||||
FontSize="24"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="4,2,14,0"
|
||||
Margin="4,0,14,0"
|
||||
Visibility="Collapsed"/>
|
||||
|
||||
<!-- Message Text -->
|
||||
<ScrollViewer Grid.Column="1" VerticalScrollBarVisibility="Auto" MaxHeight="300" Padding="0">
|
||||
<TextBlock x:Name="MessageText"
|
||||
Grid.Column="1"
|
||||
Text="Message content goes here"
|
||||
TextWrapping="Wrap"
|
||||
FontSize="14"
|
||||
LineHeight="20"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,20,0"/>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<!-- Button Panel -->
|
||||
<Border Grid.Row="2"
|
||||
|
||||
396
Schemas/RestoreBackupWindow.xaml
Normal file
396
Schemas/RestoreBackupWindow.xaml
Normal file
@@ -0,0 +1,396 @@
|
||||
<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"/>
|
||||
</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="NonRevertibleSeparator" Grid.Row="3" Height="1" Background="{DynamicResource BorderColor}" Margin="0,12,0,12" Visibility="Collapsed"/>
|
||||
|
||||
<Grid x:Name="NonRevertiblePanel" Grid.Row="4" 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}"
|
||||
CornerRadius="4"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,0,0,1"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
@@ -125,6 +125,255 @@
|
||||
<Setter Property="TextAlignment" Value="Center"/>
|
||||
</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 -->
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
|
||||
@@ -46,9 +46,8 @@ function PrintPendingChanges {
|
||||
}
|
||||
default {
|
||||
if ($script:Features -and $script:Features.ContainsKey($parameterName)) {
|
||||
$action = $script:Features[$parameterName].Action
|
||||
$message = $script:Features[$parameterName].Label
|
||||
Write-Output "- $action $message"
|
||||
Write-Output "- $message"
|
||||
}
|
||||
else {
|
||||
# Fallback: show the parameter name if no feature description is available
|
||||
|
||||
22
Scripts/Features/BackupRegistryFeatureSelection.ps1
Normal file
22
Scripts/Features/BackupRegistryFeatureSelection.ps1
Normal file
@@ -0,0 +1,22 @@
|
||||
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(
|
||||
[Parameter(Mandatory)]
|
||||
[object[]]$Features
|
||||
)
|
||||
|
||||
return @($Features | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey) })
|
||||
}
|
||||
264
Scripts/Features/BackupRegistrySnapshotCapture.ps1
Normal file
264
Scripts/Features/BackupRegistrySnapshotCapture.ps1
Normal file
@@ -0,0 +1,264 @@
|
||||
function Get-RegistryBackupCapturePlans {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[object[]]$SelectedRegistryFeatures,
|
||||
[switch]$UseSysprepRegFiles
|
||||
)
|
||||
|
||||
$planMap = @{}
|
||||
foreach ($feature in $SelectedRegistryFeatures) {
|
||||
$regFilePath = Get-RegistryFilePathForFeature -Feature $feature -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 }
|
||||
|
||||
$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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return @(
|
||||
foreach ($entry in $planMap.Values) {
|
||||
[PSCustomObject]@{
|
||||
Path = $entry.Path
|
||||
IncludeSubKeys = [bool]$entry.IncludeSubKeys
|
||||
CaptureAllValues = [bool]$entry.CaptureAllValues
|
||||
ValueNames = @($entry.ValueNames)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function Get-RegistrySnapshotsForBackup {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[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"
|
||||
}
|
||||
88
Scripts/Features/BackupRegistryState.ps1
Normal file
88
Scripts/Features/BackupRegistryState.ps1
Normal file
@@ -0,0 +1,88 @@
|
||||
function New-RegistrySettingsBackup {
|
||||
param(
|
||||
[string[]]$ActionableKeys
|
||||
)
|
||||
|
||||
$ActionableKeys = @($ActionableKeys)
|
||||
$selectedFeatures = Get-SelectedFeatures -ActionableKeys $ActionableKeys
|
||||
if (@($selectedFeatures | 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 -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(
|
||||
[Parameter(Mandatory)]
|
||||
[object[]]$SelectedFeatures,
|
||||
[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)
|
||||
}
|
||||
}
|
||||
|
||||
$selectedRegistryFeatures = Get-RegistryBackedFeatures -Features $SelectedFeatures
|
||||
$capturePlans = Get-RegistryBackupCapturePlans -SelectedRegistryFeatures $SelectedRegistryFeatures
|
||||
$registryKeys = @(Get-RegistrySnapshotsForBackup -CapturePlans $capturePlans)
|
||||
|
||||
return @{
|
||||
Version = '1.0'
|
||||
BackupType = 'RegistryState'
|
||||
CreatedAt = $CreatedAt.ToString('o')
|
||||
CreatedBy = 'Win11Debloat'
|
||||
Target = (Get-RegistryBackupTargetDescription)
|
||||
ComputerName = $env:COMPUTERNAME
|
||||
SelectedFeatures = @($selectedFeatureIds)
|
||||
RegistryKeys = @($registryKeys)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ function CreateSystemRestorePoint {
|
||||
# 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') {
|
||||
try {
|
||||
$enableResult = Invoke-NonBlocking -TimeoutSeconds 20 -ScriptBlock {
|
||||
$enableResult = Invoke-NonBlocking -TimeoutSeconds 90 -ScriptBlock {
|
||||
try {
|
||||
Enable-ComputerRestore -Drive "$env:SystemDrive"
|
||||
return $null
|
||||
@@ -33,7 +33,7 @@ function CreateSystemRestorePoint {
|
||||
|
||||
if (-not $failed) {
|
||||
try {
|
||||
$result = Invoke-NonBlocking -TimeoutSeconds 20 -ScriptBlock {
|
||||
$result = Invoke-NonBlocking -TimeoutSeconds 90 -ScriptBlock {
|
||||
try {
|
||||
$recentRestorePoints = Get-ComputerRestorePoint | Where-Object { (Get-Date) - [System.Management.ManagementDateTimeConverter]::ToDateTime($_.CreationTime) -le (New-TimeSpan -Hours 24) }
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
function DisableStoreSearchSuggestionsForAllUsers {
|
||||
# Get path to Store app database for all users
|
||||
$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
|
||||
ForEach ($storeDbPath in $usersStoreDbPaths) {
|
||||
|
||||
@@ -147,17 +147,44 @@ function ExecuteAllChanges {
|
||||
$actionableKeys += $paramKey
|
||||
}
|
||||
|
||||
$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
|
||||
}
|
||||
}
|
||||
|
||||
$totalSteps = $actionableKeys.Count
|
||||
if ($hasRegistryBackedFeature) { $totalSteps++ }
|
||||
if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ }
|
||||
$currentStep = 0
|
||||
|
||||
if ($hasRegistryBackedFeature) {
|
||||
$currentStep++
|
||||
if ($script:ApplyProgressCallback) {
|
||||
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating registry backup..."
|
||||
}
|
||||
|
||||
Write-Host "> Creating registry backup..."
|
||||
try {
|
||||
New-RegistrySettingsBackup -ActionableKeys $actionableKeys | Out-Null
|
||||
}
|
||||
catch {
|
||||
throw "Registry backup failed before applying changes. $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
# Create restore point if requested (CLI only - GUI handles this separately)
|
||||
if ($script:Params.ContainsKey("CreateRestorePoint")) {
|
||||
$currentStep++
|
||||
if ($script:ApplyProgressCallback) {
|
||||
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating system restore point"
|
||||
& $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
|
||||
Write-Host ""
|
||||
}
|
||||
@@ -178,14 +205,10 @@ function ExecuteAllChanges {
|
||||
# Prefer explicit ApplyText when provided
|
||||
$stepName = $feature.ApplyText
|
||||
} elseif ($feature.Label) {
|
||||
# Fallback: construct a name from Action and Label, or just Label
|
||||
if ($feature.Action) {
|
||||
$stepName = "$($feature.Action) $($feature.Label)"
|
||||
} else {
|
||||
# Fallback: use label from Features.json
|
||||
$stepName = $feature.Label
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($script:ApplyProgressCallback) {
|
||||
& $script:ApplyProgressCallback $currentStep $totalSteps $stepName
|
||||
|
||||
@@ -71,7 +71,7 @@ function ImportRegistryFile {
|
||||
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.Error = "Failed to unload registry hive HKU\Default (exit code: $unloadExitCode)"
|
||||
$result.ExitCode = $unloadExitCode
|
||||
}
|
||||
}
|
||||
|
||||
385
Scripts/Features/RegistryBackupValidation.ps1
Normal file
385
Scripts/Features/RegistryBackupValidation.ps1
Normal file
@@ -0,0 +1,385 @@
|
||||
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.')
|
||||
}
|
||||
|
||||
if ($selectedFeatures.Count -eq 0) {
|
||||
$errors.Add('SelectedFeatures must contain at least one feature ID.')
|
||||
}
|
||||
|
||||
return [PSCustomObject]@{
|
||||
SelectedFeatures = $selectedFeatures.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)]
|
||||
[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) -Errors $errors)
|
||||
$useSysprepRegFiles = ($Target -eq 'DefaultUserProfile') -or ($Target -like 'User:*')
|
||||
|
||||
$capturePlans = @()
|
||||
if ($errors.Count -eq 0 -and $selectedRegistryFeatures.Count -gt 0) {
|
||||
$capturePlans = @(Get-RegistryBackupCapturePlans -SelectedRegistryFeatures @($selectedRegistryFeatures) -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 SelectedFeatures.')
|
||||
}
|
||||
|
||||
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)]
|
||||
[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 ($feature -and -not [string]::IsNullOrWhiteSpace([string]$feature.RegistryKey)) {
|
||||
$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/
|
||||
function ReplaceStartMenuForAllUsers {
|
||||
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..."
|
||||
@@ -16,7 +16,7 @@ function ReplaceStartMenuForAllUsers {
|
||||
|
||||
# Get path to start menu file for all users
|
||||
$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
|
||||
ForEach ($startMenuPath in $usersStartMenuPaths) {
|
||||
@@ -43,13 +43,13 @@ function ReplaceStartMenuForAllUsers {
|
||||
# Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/
|
||||
function ReplaceStartMenu {
|
||||
param (
|
||||
$startMenuTemplate = "$script:AssetsPath/Start/start2.bin",
|
||||
$startMenuTemplate = "$script:AssetsPath\Start\start2.bin",
|
||||
$startMenuBinFile = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
|
||||
)
|
||||
|
||||
# Change path to correct user if a user was specified
|
||||
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
|
||||
@@ -63,7 +63,7 @@ function ReplaceStartMenu {
|
||||
return
|
||||
}
|
||||
|
||||
$userName = [regex]::Match($startMenuBinFile, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value
|
||||
$userName = GetStartMenuUserNameFromPath -StartMenuBinFile $startMenuBinFile
|
||||
|
||||
$backupBinFile = $startMenuBinFile + ".bak"
|
||||
|
||||
@@ -81,3 +81,141 @@ function ReplaceStartMenu {
|
||||
|
||||
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) {
|
||||
if ($script:Features.ContainsKey($paramKey) -and $script:Features[$paramKey].RequiresReboot -eq $true) {
|
||||
$feature = $script:Features[$paramKey]
|
||||
Write-Host "Warning: '$($feature.Action) $($feature.Label)' requires a reboot to take full effect" -ForegroundColor Yellow
|
||||
Write-Host "Warning: '$($feature.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)
|
||||
}
|
||||
}
|
||||
145
Scripts/Features/RestoreRegistryBackup.ps1
Normal file
145
Scripts/Features/RestoreRegistryBackup.ps1
Normal file
@@ -0,0 +1,145 @@
|
||||
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)
|
||||
}
|
||||
|
||||
$allowListValidationErrors = @(Test-RegistryBackupMatchesSelectedFeatures -SelectedFeatureIds @($selectedFeatures) -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)
|
||||
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,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$FilePath
|
||||
[string]$FilePath,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[int]$MaxDepth = 10
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
catch {
|
||||
|
||||
121
Scripts/GUI/RestoreBackupDialogFeatureLists.ps1
Normal file
121
Scripts/GUI/RestoreBackupDialogFeatureLists.ps1
Normal file
@@ -0,0 +1,121 @@
|
||||
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-SelectedFeatureIdsFromBackup {
|
||||
param($SelectedBackup)
|
||||
|
||||
return @(
|
||||
foreach ($featureId in @($SelectedBackup.SelectedFeatures)) {
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$featureId)) {
|
||||
[string]$featureId
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,7 @@ function SetWindowThemeResources {
|
||||
$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("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("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")))
|
||||
|
||||
@@ -162,7 +162,7 @@ function Show-ApplyModal {
|
||||
foreach ($paramKey in $script:Params.Keys) {
|
||||
if ($script:Features.ContainsKey($paramKey) -and $script:Features[$paramKey].RequiresReboot -eq $true) {
|
||||
$feature = $script:Features[$paramKey]
|
||||
$rebootFeatures += "$($feature.Action) $($feature.Label)"
|
||||
$rebootFeatures += "$($feature.Label)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ function Show-ImportExportConfigWindow {
|
||||
[string]$Prompt,
|
||||
[string[]]$Categories = @('Applications', 'System Tweaks', 'Deployment Settings'),
|
||||
[string[]]$DisabledCategories = @(),
|
||||
[hashtable]$CategoryDetails = @()
|
||||
[hashtable]$CategoryDetails = @(),
|
||||
[string]$ActionLabel = 'OK'
|
||||
)
|
||||
|
||||
# Show overlay on owner window
|
||||
@@ -105,6 +106,7 @@ function Show-ImportExportConfigWindow {
|
||||
|
||||
$okBtn = $dlg.FindName('OkButton')
|
||||
$cancelBtn = $dlg.FindName('CancelButton')
|
||||
$okBtn.Content = $ActionLabel
|
||||
$okBtn.Add_Click({ $dlg.Tag = 'OK'; $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
|
||||
$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
|
||||
if (-not $categories) { return }
|
||||
$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) {
|
||||
Write-Host 'Export canceled.'
|
||||
return
|
||||
}
|
||||
|
||||
$config = @{ Version = '1.0' }
|
||||
|
||||
@@ -401,12 +406,19 @@ function Export-Configuration {
|
||||
$saveDialog.DefaultExt = '.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) {
|
||||
Write-Host "Configuration exported successfully: $($saveDialog.FileName)"
|
||||
Show-MessageBox -Message "Configuration exported successfully." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -425,36 +437,49 @@ function Import-Configuration {
|
||||
|
||||
# Show native open-file dialog
|
||||
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
|
||||
$openDialog.Title = 'Import Configuration'
|
||||
$openDialog.Title = 'Select Configuration File'
|
||||
$openDialog.Filter = 'JSON files (*.json)|*.json|All files (*.*)|*.*'
|
||||
$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'
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
$availableCategories = Get-AvailableImportExportCategories -Config $config
|
||||
|
||||
if ($availableCategories.Count -eq 0) {
|
||||
Show-MessageBox -Message "The configuration file contains no importable data." -Title 'Import Configuration' -Button 'OK' -Icon 'Information' | Out-Null
|
||||
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
|
||||
}
|
||||
|
||||
Write-Host "Available categories in config: $($availableCategories -join ', ')"
|
||||
|
||||
$appCount = @($config.Apps | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) }).Count
|
||||
$tweakCount = @($config.Tweaks | Where-Object { $_ -and $_.Name -and $_.Value -eq $true }).Count
|
||||
$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
|
||||
if (-not $categories) { return }
|
||||
$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) {
|
||||
Write-Host 'Import canceled.'
|
||||
return
|
||||
}
|
||||
|
||||
if ($categories -contains 'Applications' -and $config.Apps) {
|
||||
$appIds = @(
|
||||
@@ -464,6 +489,7 @@ function Import-Configuration {
|
||||
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
|
||||
)
|
||||
|
||||
Write-Host "Importing $($appIds.Count) app selection(s)."
|
||||
Apply-ImportedApplications -AppsPanel $AppsPanel -AppIds $appIds
|
||||
|
||||
if ($OnAppsImported) {
|
||||
@@ -471,12 +497,16 @@ function Import-Configuration {
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
if ($categories -contains 'Deployment Settings' -and $config.Deployment) {
|
||||
Write-Host 'Importing deployment settings.'
|
||||
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
|
||||
|
||||
if ($OnImportCompleted) {
|
||||
|
||||
@@ -96,6 +96,7 @@ function Show-MainWindow {
|
||||
$menuAbout = $window.FindName('MenuAbout')
|
||||
$importConfigBtn = $window.FindName('ImportConfigBtn')
|
||||
$exportConfigBtn = $window.FindName('ExportConfigBtn')
|
||||
$restoreBackupBtn = $window.FindName('RestoreBackupBtn')
|
||||
|
||||
$windowStateNormal = [System.Windows.WindowState]::Normal
|
||||
$windowStateMaximized = [System.Windows.WindowState]::Maximized
|
||||
@@ -198,7 +199,10 @@ function Show-MainWindow {
|
||||
}
|
||||
}
|
||||
|
||||
$window.Add_SizeChanged({ & $updateContentMargin })
|
||||
$window.Add_SizeChanged({
|
||||
& $updateContentMargin
|
||||
UpdateTweaksResponsiveColumns
|
||||
})
|
||||
|
||||
$window.Add_StateChanged({
|
||||
& $updateWindowChrome
|
||||
@@ -231,7 +235,7 @@ function Show-MainWindow {
|
||||
})
|
||||
|
||||
$menuLogs.Add_Click({
|
||||
$logsFolder = Join-Path $PSScriptRoot "../../Logs"
|
||||
$logsFolder = Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) 'Logs'
|
||||
if (Test-Path $logsFolder) {
|
||||
Start-Process "explorer.exe" -ArgumentList $logsFolder
|
||||
}
|
||||
@@ -244,12 +248,18 @@ function Show-MainWindow {
|
||||
Show-AboutDialog -Owner $window
|
||||
})
|
||||
|
||||
# --- Import/Export Configuration ---
|
||||
$exportConfigBtn.Add_Click({
|
||||
try {
|
||||
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({
|
||||
try {
|
||||
Import-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -OnAppsImported { UpdateAppSelectionStatus; UpdatePresetStates } -OnImportCompleted {
|
||||
$tabControl.SelectedIndex = 3
|
||||
UpdateNavigationButtons
|
||||
@@ -258,8 +268,25 @@ function Show-MainWindow {
|
||||
Show-Bubble -TargetControl $reviewChangesBtn -Message 'View the selected changes here'
|
||||
}) | 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 {
|
||||
Show-RestoreBackupWindow -Owner $window
|
||||
}
|
||||
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({
|
||||
$window.Close()
|
||||
})
|
||||
@@ -349,6 +376,20 @@ 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
|
||||
}
|
||||
elseif ($control -is [System.Windows.Controls.ComboBox]) {
|
||||
$control.SelectedIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function AnimateDropdownArrow {
|
||||
param(
|
||||
[System.Windows.Controls.TextBlock]$arrow,
|
||||
@@ -504,6 +545,19 @@ function Show-MainWindow {
|
||||
}
|
||||
}
|
||||
$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
|
||||
@@ -582,6 +636,8 @@ function Show-MainWindow {
|
||||
|
||||
$script:UiControlMappings = @{}
|
||||
$script:CategoryCardMap = @{}
|
||||
$script:TweaksCompactMode = $null
|
||||
$script:TweaksCardsMovedFromCol2 = @()
|
||||
|
||||
function CreateLabeledCombo($parent, $labelText, $comboName, $items) {
|
||||
# If only 2 items (No Change + one option), use a checkbox instead
|
||||
@@ -825,7 +881,7 @@ function Show-MainWindow {
|
||||
if ($feature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($feature.FeatureId -match '^Enable') { $opt = 'Enable' }
|
||||
$items = @('No Change', $opt)
|
||||
$comboName = ("Feature_{0}_Combo" -f $feature.FeatureId) -replace '[^a-zA-Z0-9_]',''
|
||||
$combo = CreateLabeledCombo -parent $panel -labelText ($feature.Action + ' ' + $feature.Label) -comboName $comboName -items $items
|
||||
$combo = CreateLabeledCombo -parent $panel -labelText $feature.Label -comboName $comboName -items $items
|
||||
# attach tooltip from Features.json if present
|
||||
if ($feature.ToolTip) {
|
||||
$tipBlock = New-Object System.Windows.Controls.TextBlock
|
||||
@@ -837,7 +893,7 @@ function Show-MainWindow {
|
||||
try { $lblBorderObj = $window.FindName("$comboName`_LabelBorder") } catch {}
|
||||
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
|
||||
}
|
||||
$script:UiControlMappings[$comboName] = @{ Type='feature'; FeatureId = $feature.FeatureId; Action = $feature.Action; Label = $feature.Label; Category = $categoryName }
|
||||
$script:UiControlMappings[$comboName] = @{ Type='feature'; FeatureId = $feature.FeatureId; Label = $feature.Label; Category = $categoryName }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -845,7 +901,7 @@ function Show-MainWindow {
|
||||
# Build a feature-label lookup so GenerateOverview can resolve feature IDs without reloading JSON
|
||||
$script:FeatureLabelLookup = @{}
|
||||
foreach ($f in $featuresJson.Features) {
|
||||
$script:FeatureLabelLookup[$f.FeatureId] = $f.Action + ' ' + $f.Label
|
||||
$script:FeatureLabelLookup[$f.FeatureId] = $f.Label
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1243,6 +1299,52 @@ function Show-MainWindow {
|
||||
$col1 = $window.FindName('Column1Panel')
|
||||
$col2 = $window.FindName('Column2Panel')
|
||||
|
||||
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
|
||||
$tweaksScrollViewer.Add_ScrollChanged({
|
||||
if ($tweaksScrollViewer.ScrollableHeight -gt 0) {
|
||||
@@ -1444,8 +1546,6 @@ function Show-MainWindow {
|
||||
# Show "Current user only" option, hide "Target user only" option
|
||||
$appRemovalScopeCurrentUser.Visibility = 'Visible'
|
||||
$appRemovalScopeTargetUser.Visibility = 'Collapsed'
|
||||
# Enable app removal scope selection for current user
|
||||
$appRemovalScopeCombo.IsEnabled = $true
|
||||
$appRemovalScopeCombo.SelectedIndex = 0
|
||||
}
|
||||
1 {
|
||||
@@ -1455,8 +1555,6 @@ function Show-MainWindow {
|
||||
# Hide "Current user only" option, show "Target user only" option
|
||||
$appRemovalScopeCurrentUser.Visibility = 'Collapsed'
|
||||
$appRemovalScopeTargetUser.Visibility = 'Visible'
|
||||
# Enable app removal scope selection for other user
|
||||
$appRemovalScopeCombo.IsEnabled = $true
|
||||
$appRemovalScopeCombo.SelectedIndex = 0
|
||||
}
|
||||
2 {
|
||||
@@ -1467,10 +1565,12 @@ function Show-MainWindow {
|
||||
$appRemovalScopeCurrentUser.Visibility = 'Collapsed'
|
||||
$appRemovalScopeTargetUser.Visibility = 'Collapsed'
|
||||
# Lock app removal scope to "All users" when applying to sysprep
|
||||
$appRemovalScopeCombo.IsEnabled = $false
|
||||
$appRemovalScopeCombo.SelectedIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
# Keep enabled/disabled state in sync with both app selection and user mode.
|
||||
UpdateAppSelectionStatus
|
||||
})
|
||||
|
||||
# Helper function to update app removal scope description
|
||||
@@ -1513,38 +1613,16 @@ function Show-MainWindow {
|
||||
return $true
|
||||
}
|
||||
|
||||
$username = $otherUsernameTextBox.Text.Trim()
|
||||
|
||||
$errorBrush = $window.Resources['ValidationErrorColor']
|
||||
$successBrush = $window.Resources['ValidationSuccessColor']
|
||||
$validationResult = Test-TargetUserName -UserName $otherUsernameTextBox.Text
|
||||
|
||||
if ($username.Length -eq 0) {
|
||||
$usernameValidationMessage.Text = "Please enter a username"
|
||||
$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.Text = $validationResult.Message
|
||||
if ($validationResult.IsValid) {
|
||||
$usernameValidationMessage.Foreground = $successBrush
|
||||
return $true
|
||||
}
|
||||
|
||||
$usernameValidationMessage.Text = "User not found, please enter a valid username"
|
||||
$usernameValidationMessage.Foreground = $errorBrush
|
||||
return $false
|
||||
}
|
||||
@@ -1563,21 +1641,7 @@ function Show-MainWindow {
|
||||
$changesList += "Remove $selectedAppsCount application(s)"
|
||||
}
|
||||
|
||||
# Update app removal scope section based on whether apps are selected
|
||||
if ($selectedAppsCount -gt 0) {
|
||||
# Enable app removal scope selection (unless locked by sysprep mode)
|
||||
if ($userSelectionCombo.SelectedIndex -ne 2) {
|
||||
$appRemovalScopeCombo.IsEnabled = $true
|
||||
}
|
||||
$appRemovalScopeSection.Opacity = 1.0
|
||||
UpdateAppRemovalScopeDescription
|
||||
}
|
||||
else {
|
||||
# Disable app removal scope selection when no apps selected
|
||||
$appRemovalScopeCombo.IsEnabled = $false
|
||||
$appRemovalScopeSection.Opacity = 0.5
|
||||
$appRemovalScopeDescription.Text = "No apps selected for removal."
|
||||
}
|
||||
UpdateAppSelectionStatus
|
||||
|
||||
# Collect all ComboBox/CheckBox selections from dynamically created controls
|
||||
if ($script:UiControlMappings) {
|
||||
@@ -1605,7 +1669,7 @@ function Show-MainWindow {
|
||||
}
|
||||
elseif ($mapping.Type -eq 'feature') {
|
||||
$label = $script:FeatureLabelLookup[$mapping.FeatureId]
|
||||
if (-not $label) { $label = $mapping.Action + ' ' + $mapping.Label }
|
||||
if (-not $label) { $label = $mapping.Label }
|
||||
$changesList += $label
|
||||
}
|
||||
}
|
||||
@@ -1830,6 +1894,7 @@ function Show-MainWindow {
|
||||
# Initialize UI elements on window load
|
||||
$window.Add_Loaded({
|
||||
BuildDynamicTweaks
|
||||
UpdateTweaksResponsiveColumns
|
||||
RefreshTweakPresetSources -defaultSettingsJson $defaultsJson -lastUsedSettingsJson $lastUsedSettingsJson
|
||||
RegisterTweakPresetControlStateHandlers
|
||||
UpdateTweakPresetStates
|
||||
@@ -2159,18 +2224,7 @@ function Show-MainWindow {
|
||||
# Clear All Tweaks button
|
||||
$clearAllTweaksBtn = $window.FindName('ClearAllTweaksBtn')
|
||||
$clearAllTweaksBtn.Add_Click({
|
||||
# Reset all ComboBoxes to index 0 (No Change) and uncheck all CheckBoxes
|
||||
if ($script:UiControlMappings) {
|
||||
foreach ($comboName in $script:UiControlMappings.Keys) {
|
||||
$control = $window.FindName($comboName)
|
||||
if ($control -is [System.Windows.Controls.CheckBox]) {
|
||||
$control.IsChecked = $false
|
||||
}
|
||||
elseif ($control -is [System.Windows.Controls.ComboBox]) {
|
||||
$control.SelectedIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
ClearTweakSelections
|
||||
UpdateTweakPresetStates
|
||||
})
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ function Show-MessageBox {
|
||||
[string]$Button = 'OK',
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet('None', 'Information', 'Warning', 'Error', 'Question')]
|
||||
[ValidateSet('None', 'Information', 'Success', 'Warning', 'Error', 'Question')]
|
||||
[string]$Icon = 'None',
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
@@ -90,6 +90,11 @@ function Show-MessageBox {
|
||||
$iconText.Foreground = $msgWindow.FindResource('InformationIconColor')
|
||||
$iconText.Visibility = 'Visible'
|
||||
}
|
||||
'Success' {
|
||||
$iconText.Text = [char]0xE73E
|
||||
$iconText.Foreground = $msgWindow.FindResource('SuccessIconColor')
|
||||
$iconText.Visibility = 'Visible'
|
||||
}
|
||||
'Warning' {
|
||||
$iconText.Text = [char]0xE7BA
|
||||
$iconText.Foreground = $msgWindow.FindResource('WarningIconColor')
|
||||
|
||||
403
Scripts/GUI/Show-RestoreBackupDialog.ps1
Normal file
403
Scripts/GUI/Show-RestoreBackupDialog.ps1
Normal file
@@ -0,0 +1,403 @@
|
||||
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')
|
||||
$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'
|
||||
$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
|
||||
}
|
||||
}
|
||||
|
||||
$selectedFeatureIds = Get-SelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup
|
||||
$featureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds $selectedFeatureIds -Features $script:Features
|
||||
$revertibleFeaturesList = @($featureLists.Revertible)
|
||||
$nonRevertibleFeaturesList = @($featureLists.NonRevertible)
|
||||
Write-Host "Backup overview prepared. Revertible=$($revertibleFeaturesList.Count), NonRevertible=$($nonRevertibleFeaturesList.Count)"
|
||||
|
||||
if ($revertibleFeaturesList.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 = 'Visible'
|
||||
$overviewSummaryText.Visibility = 'Collapsed'
|
||||
$nonRevertibleFeaturesItemsControl.ItemsSource = $nonRevertibleFeaturesList
|
||||
|
||||
$hasNonRevertibleItems = ($nonRevertibleFeaturesList.Count -gt 0)
|
||||
if ($hasNonRevertibleItems) { $nonRevertiblePanel.Visibility = 'Visible' } else { $nonRevertiblePanel.Visibility = 'Collapsed' }
|
||||
if ($hasNonRevertibleItems) { $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
|
||||
}
|
||||
88
Scripts/GUI/Show-RestoreBackupWindow.ps1
Normal file
88
Scripts/GUI/Show-RestoreBackupWindow.ps1
Normal file
@@ -0,0 +1,88 @@
|
||||
function Show-RestoreBackupWindow {
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[System.Windows.Window]$Owner = $null
|
||||
)
|
||||
|
||||
try {
|
||||
Write-Host 'Opening restore backup dialog.'
|
||||
|
||||
$dialogResult = Show-RestoreBackupDialog -Owner $Owner
|
||||
if (-not $dialogResult -or $dialogResult.Result -eq 'Cancel') {
|
||||
Write-Host 'Restore canceled by user.'
|
||||
return
|
||||
}
|
||||
|
||||
$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
|
||||
$successMessage = 'Registry backup restored successfully. Please restart your computer for all changes 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -117,11 +117,15 @@ Write-Output "------------------------------------------------------------------
|
||||
Write-Output " Win11Debloat Script - Get Dev"
|
||||
Write-Output "-------------------------------------------------------------------------------------------"
|
||||
|
||||
$tempRootPath = $env:TEMP
|
||||
$tempWorkPath = Join-Path $tempRootPath 'Win11Debloat'
|
||||
$tempArchivePath = Join-Path $tempRootPath 'win11debloat.zip'
|
||||
|
||||
Write-Output "> Downloading Win11Debloat for development..."
|
||||
|
||||
# Download latest version of Win11Debloat from GitHub master branch as zip archive
|
||||
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 {
|
||||
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..."
|
||||
|
||||
# Remove old script folder if it exists, but keep config and log files
|
||||
if (Test-Path "$env:TEMP/Win11Debloat") {
|
||||
Get-ChildItem -Path "$env:TEMP/Win11Debloat" -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Config,Logs | Remove-Item -Recurse -Force
|
||||
if (Test-Path $tempWorkPath) {
|
||||
Get-ChildItem -Path $tempWorkPath -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Config,Logs,Backups | Remove-Item -Recurse -Force
|
||||
}
|
||||
|
||||
$configDir = "$env:TEMP/Win11Debloat/Config"
|
||||
$backupDir = "$env:TEMP/Win11Debloat/ConfigOld"
|
||||
$configDir = Join-Path $tempWorkPath 'Config'
|
||||
$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
|
||||
if (Test-Path "$configDir") {
|
||||
@@ -160,13 +164,13 @@ Write-Output ""
|
||||
Write-Output "> Unpacking..."
|
||||
|
||||
# Unzip archive to Win11Debloat folder
|
||||
Expand-Archive "$env:TEMP/win11debloat.zip" "$env:TEMP/Win11Debloat"
|
||||
Expand-Archive $tempArchivePath $tempWorkPath
|
||||
|
||||
# Remove archive
|
||||
Remove-Item "$env:TEMP/win11debloat.zip"
|
||||
Remove-Item $tempArchivePath
|
||||
|
||||
# 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
|
||||
if (Test-Path "$backupDir") {
|
||||
@@ -206,7 +210,8 @@ if ($PSVersionTable.PSVersion.Major -ge 7) {
|
||||
}
|
||||
|
||||
# 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
|
||||
if ($null -ne $debloatProcess) {
|
||||
@@ -214,12 +219,12 @@ if ($null -ne $debloatProcess) {
|
||||
}
|
||||
|
||||
# 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 "> Cleaning up..."
|
||||
|
||||
# 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 ""
|
||||
|
||||
@@ -117,12 +117,16 @@ Write-Output "------------------------------------------------------------------
|
||||
Write-Output " Win11Debloat Script - Get"
|
||||
Write-Output "-------------------------------------------------------------------------------------------"
|
||||
|
||||
$tempRootPath = $env:TEMP
|
||||
$tempWorkPath = Join-Path $tempRootPath 'Win11Debloat'
|
||||
$tempArchivePath = Join-Path $tempRootPath 'win11debloat.zip'
|
||||
|
||||
Write-Output "> Downloading Win11Debloat..."
|
||||
|
||||
# Download latest version of Win11Debloat from GitHub as zip archive
|
||||
try {
|
||||
$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 {
|
||||
Write-Host "Error: Unable to fetch latest release from GitHub. Please check your internet connection and try again." -ForegroundColor Red
|
||||
@@ -136,12 +140,12 @@ Write-Output ""
|
||||
Write-Output "> Cleaning up old Win11Debloat folder..."
|
||||
|
||||
# Remove old script folder if it exists, but keep config and log files
|
||||
if (Test-Path "$env:TEMP/Win11Debloat") {
|
||||
Get-ChildItem -Path "$env:TEMP/Win11Debloat" -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Config,Logs | Remove-Item -Recurse -Force
|
||||
if (Test-Path $tempWorkPath) {
|
||||
Get-ChildItem -Path $tempWorkPath -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Config,Logs,Backups | Remove-Item -Recurse -Force
|
||||
}
|
||||
|
||||
$configDir = "$env:TEMP/Win11Debloat/Config"
|
||||
$backupDir = "$env:TEMP/Win11Debloat/ConfigOld"
|
||||
$configDir = Join-Path $tempWorkPath 'Config'
|
||||
$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
|
||||
if (Test-Path "$configDir") {
|
||||
@@ -161,13 +165,13 @@ Write-Output ""
|
||||
Write-Output "> Unpacking..."
|
||||
|
||||
# Unzip archive to Win11Debloat folder
|
||||
Expand-Archive "$env:TEMP/win11debloat.zip" "$env:TEMP/Win11Debloat"
|
||||
Expand-Archive $tempArchivePath $tempWorkPath
|
||||
|
||||
# Remove archive
|
||||
Remove-Item "$env:TEMP/win11debloat.zip"
|
||||
Remove-Item $tempArchivePath
|
||||
|
||||
# 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
|
||||
if (Test-Path "$backupDir") {
|
||||
@@ -207,7 +211,8 @@ if ($PSVersionTable.PSVersion.Major -ge 7) {
|
||||
}
|
||||
|
||||
# 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
|
||||
if ($null -ne $debloatProcess) {
|
||||
@@ -215,12 +220,12 @@ if ($null -ne $debloatProcess) {
|
||||
}
|
||||
|
||||
# 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 "> Cleaning up..."
|
||||
|
||||
# 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 ""
|
||||
|
||||
@@ -1,31 +1,41 @@
|
||||
function CheckIfUserExists {
|
||||
param (
|
||||
$userName
|
||||
[string]$userName
|
||||
)
|
||||
|
||||
if ($userName -match '[<>:"|?*]') {
|
||||
return $false
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($userName)) {
|
||||
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 {
|
||||
$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
|
||||
}
|
||||
|
||||
$userExists = Test-Path ($env:USERPROFILE -Replace ('\\' + $env:USERNAME + '$'), "\$userName")
|
||||
return -not [string]::IsNullOrWhiteSpace($userContext.UserSid)
|
||||
|
||||
if ($userExists) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Error "Something went wrong when trying to find the user directory path for user $userName. Please ensure the user exists on this system"
|
||||
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
|
||||
|
||||
174
Scripts/Helpers/Get-RegFileOperations.ps1
Normal file
174
Scripts/Helpers/Get-RegFileOperations.ps1
Normal file
@@ -0,0 +1,174 @@
|
||||
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>.*)"$') {
|
||||
return [PSCustomObject]@{
|
||||
OperationType = 'SetValue'
|
||||
ValueType = 'String'
|
||||
ValueData = $matches.value
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
if (-not (CheckIfUserExists -userName $userName) -and $userName -ne "*") {
|
||||
Write-Error "User $userName does not exist on this system"
|
||||
AwaitKeyToExit
|
||||
if ($userName -eq "*") {
|
||||
$rootPaths = @(
|
||||
(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"
|
||||
$userPath = "$env:SystemDrive\Users\$userName\$fileName"
|
||||
|
||||
if ((Test-Path $userPath) -or ($userDirectoryExists -and (-not $exitIfPathNotFound))) {
|
||||
return $userPath
|
||||
$wildcardPath = if ([string]::IsNullOrWhiteSpace($fileName)) {
|
||||
Join-Path $rootPath '*'
|
||||
}
|
||||
else {
|
||||
Join-Path (Join-Path $rootPath '*') $fileName
|
||||
}
|
||||
|
||||
$userDirectoryExists = Test-Path ($env:USERPROFILE -Replace ('\\' + $env:USERNAME + '$'), "\$userName")
|
||||
$userPath = $env:USERPROFILE -Replace ('\\' + $env:USERNAME + '$'), "\$userName\$fileName"
|
||||
return $wildcardPath
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Error "Something went wrong when trying to find the user directory path for user $userName. Please ensure the user exists on this system"
|
||||
AwaitKeyToExit
|
||||
|
||||
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)]
|
||||
$Feature,
|
||||
[switch]$UseSysprepRegFiles
|
||||
)
|
||||
|
||||
$useSysprepLayout = $UseSysprepRegFiles -or $script:Params.ContainsKey('Sysprep') -or $script:Params.ContainsKey('User')
|
||||
if ($useSysprepLayout) {
|
||||
return Join-Path (Join-Path $script:RegfilesPath 'Sysprep') $Feature.RegistryKey
|
||||
}
|
||||
|
||||
return Join-Path $script:RegfilesPath $Feature.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
|
||||
$script:Version = "2026.04.26"
|
||||
$script:AppsListFilePath = "$PSScriptRoot/Config/Apps.json"
|
||||
$script:DefaultSettingsFilePath = "$PSScriptRoot/Config/DefaultSettings.json"
|
||||
$script:FeaturesFilePath = "$PSScriptRoot/Config/Features.json"
|
||||
$script:SavedSettingsFilePath = "$PSScriptRoot/Config/LastUsedSettings.json"
|
||||
$script:CustomAppsListFilePath = "$PSScriptRoot/Config/CustomAppsList"
|
||||
$script:DefaultLogPath = "$PSScriptRoot/Logs/Win11Debloat.log"
|
||||
$script:RegfilesPath = "$PSScriptRoot/Regfiles"
|
||||
$script:AssetsPath = "$PSScriptRoot/Assets"
|
||||
$script:AppSelectionSchema = "$PSScriptRoot/Schemas/AppSelectionWindow.xaml"
|
||||
$script:MainWindowSchema = "$PSScriptRoot/Schemas/MainWindow.xaml"
|
||||
$script:MessageBoxSchema = "$PSScriptRoot/Schemas/MessageBoxWindow.xaml"
|
||||
$script:AboutWindowSchema = "$PSScriptRoot/Schemas/AboutWindow.xaml"
|
||||
$script:ApplyChangesWindowSchema = "$PSScriptRoot/Schemas/ApplyChangesWindow.xaml"
|
||||
$script:SharedStylesSchema = "$PSScriptRoot/Schemas/SharedStyles.xaml"
|
||||
$script:BubbleHintSchema = "$PSScriptRoot/Schemas/BubbleHint.xaml"
|
||||
$script:ImportExportConfigSchema = "$PSScriptRoot/Schemas/ImportExportConfigWindow.xaml"
|
||||
$script:LoadAppsDetailsScriptPath = "$PSScriptRoot/Scripts/FileIO/LoadAppsDetailsFromJson.ps1"
|
||||
$script:Version = "2026.05.11"
|
||||
$configPath = Join-Path $PSScriptRoot 'Config'
|
||||
$logsPath = Join-Path $PSScriptRoot 'Logs'
|
||||
$schemasPath = Join-Path $PSScriptRoot 'Schemas'
|
||||
$scriptsPath = Join-Path $PSScriptRoot 'Scripts'
|
||||
|
||||
$script:AppsListFilePath = Join-Path $configPath 'Apps.json'
|
||||
$script:DefaultSettingsFilePath = Join-Path $configPath 'DefaultSettings.json'
|
||||
$script:FeaturesFilePath = Join-Path $configPath 'Features.json'
|
||||
$script:SavedSettingsFilePath = Join-Path $configPath 'LastUsedSettings.json'
|
||||
$script:CustomAppsListFilePath = Join-Path $configPath 'CustomAppsList'
|
||||
$script:DefaultLogPath = Join-Path $logsPath 'Win11Debloat.log'
|
||||
$script:RegfilesPath = Join-Path $PSScriptRoot 'Regfiles'
|
||||
$script:RegistryBackupsPath = Join-Path $PSScriptRoot 'Backups'
|
||||
$script:AssetsPath = Join-Path $PSScriptRoot 'Assets'
|
||||
$script:AppSelectionSchema = Join-Path $schemasPath 'AppSelectionWindow.xaml'
|
||||
$script:MainWindowSchema = Join-Path $schemasPath 'MainWindow.xaml'
|
||||
$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'
|
||||
|
||||
@@ -209,14 +216,14 @@ Write-Host ""
|
||||
|
||||
# Log script output to 'Win11Debloat.log' at the specified path
|
||||
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 {
|
||||
Start-Transcript -Path $script:DefaultLogPath -Append -IncludeInvocationHeader -Force | Out-Null
|
||||
}
|
||||
|
||||
# Check if script has all required files
|
||||
if (-not ((Test-Path $script:DefaultSettingsFilePath) -and (Test-Path $script:AppsListFilePath) -and (Test-Path $script:RegfilesPath) -and (Test-Path $script:AssetsPath) -and (Test-Path $script:AppSelectionSchema) -and (Test-Path $script:ApplyChangesWindowSchema) -and (Test-Path $script:SharedStylesSchema) -and (Test-Path $script:BubbleHintSchema) -and (Test-Path $script:FeaturesFilePath))) {
|
||||
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-Output ""
|
||||
Write-Output "Press any key to exit..."
|
||||
@@ -288,6 +295,12 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
||||
# Features functions
|
||||
. "$PSScriptRoot/Scripts/Features/ExecuteChanges.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/EnableWindowsFeature.ps1"
|
||||
. "$PSScriptRoot/Scripts/Features/ImportRegistryFile.ps1"
|
||||
@@ -314,20 +327,28 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
||||
. "$PSScriptRoot/Scripts/GUI/Show-ConfigWindow.ps1"
|
||||
. "$PSScriptRoot/Scripts/GUI/Show-ApplyModal.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-AboutDialog.ps1"
|
||||
. "$PSScriptRoot/Scripts/GUI/Show-Bubble.ps1"
|
||||
|
||||
# Helper functions
|
||||
. "$PSScriptRoot/Scripts/Helpers/AddParameter.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/ResolveUserProfilePath.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/CheckIfUserExists.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/CheckModernStandbySupport.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/GenerateAppsList.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/GetFriendlyRegistryBackupTarget.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/GetFriendlyTargetUserName.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/ImportConfigToParams.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/GetUserName.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/RegistryPathHelpers.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/TestIfUserIsLoggedIn.ps1"
|
||||
|
||||
# Threading functions
|
||||
|
||||
Reference in New Issue
Block a user