mirror of
https://github.com/Raphire/Win11Debloat.git
synced 2026-06-15 14:28:24 +00:00
Compare commits
18 Commits
2026.05.11
...
2026.06.11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2193591448 | ||
|
|
e9269c5501 | ||
|
|
fdac0a6d14 | ||
|
|
2aa9afaa2c | ||
|
|
67c9cc6ba3 | ||
|
|
157d26bb22 | ||
|
|
53ca51dffd | ||
|
|
db24865051 | ||
|
|
33b77f19a0 | ||
|
|
37872b2030 | ||
|
|
abfc5db2c3 | ||
|
|
1d828d6a78 | ||
|
|
4d9da4749b | ||
|
|
5cf9ac4082 | ||
|
|
924c192ca5 | ||
|
|
2a5cb986c9 | ||
|
|
66982ada28 | ||
|
|
489af33a8b |
40
.github/CONTRIBUTING.md
vendored
40
.github/CONTRIBUTING.md
vendored
@@ -67,7 +67,7 @@ Win11Debloat/
|
|||||||
│ ├── DefaultSettings.json # Default configuration preset
|
│ ├── DefaultSettings.json # Default configuration preset
|
||||||
│ ├── Features.json # All features with metadata
|
│ ├── Features.json # All features with metadata
|
||||||
│ └── LastUsedSettings.json # Last used configuration (generated during use)
|
│ └── LastUsedSettings.json # Last used configuration (generated during use)
|
||||||
├── Regfiles/ # Registry files for each feature
|
├── Regfiles/ # Registry files for all features
|
||||||
└── Schemas/ # XAML Schemas for GUI elements
|
└── Schemas/ # XAML Schemas for GUI elements
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -98,18 +98,18 @@ Avoid these common mistakes when contributing:
|
|||||||
|
|
||||||
1. **Forgetting Get.ps1**: When adding a new command-line parameter, contributors often remember to add it to `Win11Debloat.ps1` but forget to add the same parameter to `Scripts/Get.ps1`. Both files **must** have matching parameters.
|
1. **Forgetting Get.ps1**: When adding a new command-line parameter, contributors often remember to add it to `Win11Debloat.ps1` but forget to add the same parameter to `Scripts/Get.ps1`. Both files **must** have matching parameters.
|
||||||
|
|
||||||
2. **Missing Registry Files**: Always create an `Undo` registry file for reversibility, aswell as a `Sysprep` registry file for Sysprep mode.
|
2. **Missing Registry Files**: Always create an `Undo` registry file for reversibility, aswell as a `Sysprep` registry file for applying changes to other users and Sysprep mode.
|
||||||
|
|
||||||
3. **Incorrect Registry Hives for Sysprep**: Sysprep registry files apply changes to Windows' default user, registry keys in the `HKEY_CURRENT_USER` hive must use `hkey_users\default` instead. Ensure you update **all** registry keys in the file.
|
3. **Incorrect Registry Hives for Sysprep**: Sysprep registry files are meant to apply changes to a different user. Registry keys in the `HKEY_CURRENT_USER` hive must use `hkey_users\default` instead. Ensure you update **all** registry keys in the file.
|
||||||
|
|
||||||
4. **Wrong Registry File Location**:
|
4. **Wrong Registry File Location**:
|
||||||
- Main action files go in `Regfiles/`
|
- Main action files go in `Regfiles/`
|
||||||
- Undo files go in `Regfiles/Undo/`
|
- Undo files go in `Regfiles/Undo/`
|
||||||
- Sysprep files go in `Regfiles/Sysprep/`
|
- Sysprep files go in `Regfiles/Sysprep/`
|
||||||
|
|
||||||
Placing files in the wrong directory will cause the script to fail when trying to apply or undo changes.
|
Placing files in the wrong directory may cause the script to fail when trying to apply or undo changes.
|
||||||
|
|
||||||
6. **Not Testing Undo Functionality**: Always test that your undo registry file properly reverts all changes. A feature that can't be undone will frustrate users.
|
6. **Not Testing Undo Functionality**: Always test that your undo registry file properly reverts all changes.
|
||||||
|
|
||||||
7. **Not Testing User/Sysprep Functionality**: Always test that your feature works when applied to another user or to the Windows default user with Sysprep. Sysprep changes can be tested by creating new users after running the script.
|
7. **Not Testing User/Sysprep Functionality**: Always test that your feature works when applied to another user or to the Windows default user with Sysprep. Sysprep changes can be tested by creating new users after running the script.
|
||||||
|
|
||||||
@@ -192,31 +192,33 @@ Add your feature to the `"Features"` array in `Config/Features.json`:
|
|||||||
"ToolTip": "Detailed explanation of what this feature does and its impact.",
|
"ToolTip": "Detailed explanation of what this feature does and its impact.",
|
||||||
"Category": "Privacy & Suggested Content",
|
"Category": "Privacy & Suggested Content",
|
||||||
"Priority": 1,
|
"Priority": 1,
|
||||||
"Action": "Disable",
|
|
||||||
"RegistryKey": "Disable_YourFeature.reg",
|
"RegistryKey": "Disable_YourFeature.reg",
|
||||||
"ApplyText": "Disabling your feature...",
|
"ApplyText": "Disabling your feature",
|
||||||
"UndoAction": "Enable",
|
"UndoLabel": "Short description for the undo",
|
||||||
|
"ApplyUndoText": "Enabling your feature",
|
||||||
"RegistryUndoKey": "Enable_YourFeature.reg",
|
"RegistryUndoKey": "Enable_YourFeature.reg",
|
||||||
"RequiresReboot": false,
|
"RequiresReboot": false,
|
||||||
|
"DisableWhenApplied": false,
|
||||||
"MinVersion": null,
|
"MinVersion": null,
|
||||||
"MaxVersion": null
|
"MaxVersion": null
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Field Descriptions**:
|
**Field Descriptions**:
|
||||||
- `FeatureId`: Unique identifier (must match parameter name in Win11Debloat.ps1 and Get.ps1)
|
- `FeatureId`: Unique identifier, this must match parameter name in the Win11Debloat.ps1 and Get.ps1 files.
|
||||||
- `Label`: Short description shown in the UI, written in a way to fit with the Action or UndoAction prefixed
|
- `Label`: Short description shown in the UI and wiki documentation.
|
||||||
- `ToolTip`: Detailed explanation of what the feature does, used for tooltips in the GUI
|
- `ToolTip`: Detailed explanation of what the feature does, used for tooltips in the GUI.
|
||||||
- `Category`: One of the predefined categories (see Categories array in Features.json), features without a category won't be loaded into the GUI.
|
- `Category`: One of the predefined categories (see Categories array in Features.json), features without a category won't be loaded into the GUI.
|
||||||
- `Priority`: Optional. The priority value (int) is used to sort features within a category. If this field is omitted the feature will be sorted based on the order in the Features.json file.
|
- `Priority`: Optional. The priority value (int) is used to sort features within a category. If this field is omitted the feature will be sorted based on the order in the Features.json file.
|
||||||
- `Action`: Action word for the feature (e.g., "Disable", "Enable", "Hide", "Show")
|
- `RegistryKey`: Filename of the registry file to apply (in Regfiles/ directory) or null if feature does not require registry changes.
|
||||||
- `RegistryKey`: Filename of the registry file to apply (in Regfiles/ directory) or null if feature does not require registry changes
|
- `ApplyText`: Message shown when applying the feature.
|
||||||
- `ApplyText`: Message shown when applying the feature
|
- `UndoLabel`: Short description for the undo shown in the UI.
|
||||||
- `UndoAction`: Action word for reverting (e.g., "Enable", "Show")
|
- `ApplyUndoText`: Message shown when undoing the feature.
|
||||||
- `RegistryUndoKey`: Filename of the registry file to revert changes or null if feature does not require registry changes
|
- `RegistryUndoKey`: Filename of the registry file to revert changes or null if feature does not require registry changes.
|
||||||
- `RequiresReboot`: Optional boolean. Set to `true` if the feature requires a system reboot to take effect
|
- `RequiresReboot`: Optional boolean. Set to `true` if the feature requires a system reboot to take effect.
|
||||||
- `MinVersion`: Minimum Windows build version (e.g., "22000") or null
|
- `DisableWhenApplied`: Optional boolean. Set to `true` if the feature has no supported undo method.
|
||||||
- `MaxVersion`: Maximum Windows version or null
|
- `MinVersion`: Minimum Windows build version (e.g., "22000") or null.
|
||||||
|
- `MaxVersion`: Maximum Windows version or null.
|
||||||
|
|
||||||
#### 3. Add Command-Line Parameter
|
#### 3. Add Command-Line Parameter
|
||||||
|
|
||||||
|
|||||||
@@ -781,9 +781,9 @@
|
|||||||
{
|
{
|
||||||
"FriendlyName": "Windows Terminal",
|
"FriendlyName": "Windows Terminal",
|
||||||
"AppId": "Microsoft.WindowsTerminal",
|
"AppId": "Microsoft.WindowsTerminal",
|
||||||
"Description": "Default terminal app in windows 11 (Command Prompt, PowerShell, WSL)",
|
"Description": "Default terminal app in windows 11 (Command Prompt, PowerShell, WSL), WARNING: Do not remove if you launched Win11Debloat from Windows Terminal, as this will cause the script to fail.",
|
||||||
"SelectedByDefault": false,
|
"SelectedByDefault": false,
|
||||||
"Recommendation": "optional"
|
"Recommendation": "unsafe"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"FriendlyName": "Xbox TCUI Framework",
|
"FriendlyName": "Xbox TCUI Framework",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,9 +4,9 @@
|
|||||||
[](https://github.com/Raphire/Win11Debloat/discussions)
|
[](https://github.com/Raphire/Win11Debloat/discussions)
|
||||||
[](https://github.com/Raphire/Win11Debloat/wiki/)
|
[](https://github.com/Raphire/Win11Debloat/wiki/)
|
||||||
|
|
||||||
Win11Debloat is a lightweight, easy to use PowerShell script that allows you to quickly declutter and customize your Windows experience. It can remove pre-installed bloatware apps, disable telemetry, remove intrusive interface elements and much more. No need to painstakingly go through all the settings yourself or remove apps one by one. Win11Debloat makes the process quick and easy!
|
Win11Debloat is a lightweight, easy to use PowerShell script that allows you to quickly declutter and customize your Windows experience, no installation required! You can use it to remove pre-installed apps, disable telemetry, remove intrusive interface elements and much more. No need to painstakingly go through all the settings yourself or remove apps one by one. Win11Debloat makes the process quick and easy!
|
||||||
|
|
||||||
The script also includes many features that system administrators and power users will enjoy. Such as a powerful command-line interface, support for Windows Audit mode and the option to make changes to other Windows users. Please refer to our [wiki](https://github.com/Raphire/Win11Debloat/wiki/) for more details.
|
The script also includes many features that system administrators and power users will enjoy. Such as a powerful command-line interface, support for Windows Audit mode and the ability to make changes to other Windows users. Please refer to our [wiki](https://github.com/Raphire/Win11Debloat/wiki/) for more details.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
7
Regfiles/Start_AllApps_Category.reg
Normal file
7
Regfiles/Start_AllApps_Category.reg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Windows Registry Editor Version 5.00
|
||||||
|
|
||||||
|
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Start]
|
||||||
|
"AllAppsViewMode"=dword:00000000
|
||||||
|
|
||||||
|
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer]
|
||||||
|
"NoStartMenuMorePrograms"=-
|
||||||
7
Regfiles/Start_AllApps_Grid.reg
Normal file
7
Regfiles/Start_AllApps_Grid.reg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Windows Registry Editor Version 5.00
|
||||||
|
|
||||||
|
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Start]
|
||||||
|
"AllAppsViewMode"=dword:00000001
|
||||||
|
|
||||||
|
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer]
|
||||||
|
"NoStartMenuMorePrograms"=-
|
||||||
7
Regfiles/Start_AllApps_List.reg
Normal file
7
Regfiles/Start_AllApps_List.reg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Windows Registry Editor Version 5.00
|
||||||
|
|
||||||
|
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Start]
|
||||||
|
"AllAppsViewMode"=dword:00000002
|
||||||
|
|
||||||
|
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer]
|
||||||
|
"NoStartMenuMorePrograms"=-
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
7
Regfiles/Sysprep/Start_AllApps_Category.reg
Normal file
7
Regfiles/Sysprep/Start_AllApps_Category.reg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Windows Registry Editor Version 5.00
|
||||||
|
|
||||||
|
[hkey_users\default\Software\Microsoft\Windows\CurrentVersion\Start]
|
||||||
|
"AllAppsViewMode"=dword:00000000
|
||||||
|
|
||||||
|
[hkey_users\default\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer]
|
||||||
|
"NoStartMenuMorePrograms"=-
|
||||||
7
Regfiles/Sysprep/Start_AllApps_Grid.reg
Normal file
7
Regfiles/Sysprep/Start_AllApps_Grid.reg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Windows Registry Editor Version 5.00
|
||||||
|
|
||||||
|
[hkey_users\default\Software\Microsoft\Windows\CurrentVersion\Start]
|
||||||
|
"AllAppsViewMode"=dword:00000001
|
||||||
|
|
||||||
|
[hkey_users\default\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer]
|
||||||
|
"NoStartMenuMorePrograms"=-
|
||||||
7
Regfiles/Sysprep/Start_AllApps_List.reg
Normal file
7
Regfiles/Sysprep/Start_AllApps_List.reg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Windows Registry Editor Version 5.00
|
||||||
|
|
||||||
|
[hkey_users\default\Software\Microsoft\Windows\CurrentVersion\Start]
|
||||||
|
"AllAppsViewMode"=dword:00000002
|
||||||
|
|
||||||
|
[hkey_users\default\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer]
|
||||||
|
"NoStartMenuMorePrograms"=-
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
6
Run.bat
6
Run.bat
@@ -19,15 +19,17 @@ if exist "%wtDefaultPath%" (
|
|||||||
set "wtPath="
|
set "wtPath="
|
||||||
)
|
)
|
||||||
|
|
||||||
|
set "SCRIPT_PATH=\"%~dp0Win11Debloat.ps1\""
|
||||||
|
|
||||||
:: Launch script
|
:: Launch script
|
||||||
if defined wtPath (
|
if defined wtPath (
|
||||||
call :Log Launching Win11Debloat.ps1 with Windows Terminal...
|
call :Log Launching Win11Debloat.ps1 with Windows Terminal...
|
||||||
PowerShell -Command "Start-Process -FilePath '%wtPath%' -ArgumentList 'PowerShell -NoProfile -ExecutionPolicy Bypass -File ""%~dp0Win11Debloat.ps1""' -Verb RunAs" >> "%logFile%" || call :Error "PowerShell command failed"
|
PowerShell -Command "Start-Process -FilePath '%wtPath%' -ArgumentList 'PowerShell -NoProfile -ExecutionPolicy Bypass -File %SCRIPT_PATH%' -Verb RunAs" >> "%logFile%" || call :Error "PowerShell command failed"
|
||||||
call :Log Script execution passed successfully to Win11Debloat.ps1
|
call :Log Script execution passed successfully to Win11Debloat.ps1
|
||||||
) else (
|
) else (
|
||||||
echo Windows Terminal not found. Using default PowerShell instead...
|
echo Windows Terminal not found. Using default PowerShell instead...
|
||||||
call :Log Windows Terminal not found. Using default PowerShell to launch Win11Debloat.ps1...
|
call :Log Windows Terminal not found. Using default PowerShell to launch Win11Debloat.ps1...
|
||||||
PowerShell -ExecutionPolicy Bypass -Command "& {Start-Process PowerShell -ArgumentList '-NoProfile -ExecutionPolicy Bypass -File ""%~dp0Win11Debloat.ps1""' -Verb RunAs}" >> "%logFile%" || call :Error "PowerShell command failed"
|
PowerShell -ExecutionPolicy Bypass -Command "& {Start-Process PowerShell -ArgumentList '-NoProfile -ExecutionPolicy Bypass -File %SCRIPT_PATH%' -Verb RunAs}" >> "%logFile%" || call :Error "PowerShell command failed"
|
||||||
call :Log Script execution passed successfully to Win11Debloat.ps1
|
call :Log Script execution passed successfully to Win11Debloat.ps1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
<Style x:Key="CategoryHeaderTextBlock" TargetType="TextBlock">
|
<Style x:Key="CategoryHeaderTextBlock" TargetType="TextBlock">
|
||||||
<Setter Property="FontWeight" Value="Bold"/>
|
<Setter Property="FontWeight" Value="Bold"/>
|
||||||
<Setter Property="FontSize" Value="16"/>
|
<Setter Property="FontSize" Value="16"/>
|
||||||
<Setter Property="Margin" Value="0,0,0,13"/>
|
<Setter Property="Margin" Value="0,0,0,12"/>
|
||||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
@@ -485,49 +485,82 @@
|
|||||||
<!-- Home Tab -->
|
<!-- Home Tab -->
|
||||||
<TabItem Header="Home" x:Name="HomeTab">
|
<TabItem Header="Home" x:Name="HomeTab">
|
||||||
<Grid>
|
<Grid>
|
||||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Width="600">
|
<StackPanel x:Name="HomeContentPanel" HorizontalAlignment="Center" VerticalAlignment="Top">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<Viewbox Width="220" Height="220" Margin="0,32,0,10" HorizontalAlignment="Center">
|
<Viewbox Width="250" Height="250" Margin="0,0,0,10" HorizontalAlignment="Center">
|
||||||
<Grid Width="220" Height="220">
|
<Grid Width="250" Height="250">
|
||||||
<!-- Windows logo style icon -->
|
<!-- Windows logo style icon -->
|
||||||
<Path x:Name="LogoFallback" Data="M0,0 L80,0 L80,80 L0,80 Z M90,0 L170,0 L170,80 L90,80 Z M0,90 L80,90 L80,170 L0,170 Z M90,90 L170,90 L170,170 L90,170 Z"
|
<Path x:Name="LogoFallback" Data="M0,0 L80,0 L80,80 L0,80 Z M90,0 L170,0 L170,80 L90,80 Z M0,90 L80,90 L80,170 L0,170 Z M90,90 L170,90 L170,170 L90,170 Z"
|
||||||
Fill="{DynamicResource ButtonBg}" Stretch="Uniform" Margin="10"/>
|
Fill="{DynamicResource ButtonBg}" Stretch="Uniform" Margin="10"/>
|
||||||
<!-- Sparkle effects -->
|
<!-- Sparkle effects -->
|
||||||
<Canvas HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="45" Height="45" Margin="0,0,2,2">
|
<Canvas HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="50" Height="50" Margin="0,0,2,2">
|
||||||
<Path Canvas.Left="10" Canvas.Top="16" Data="M12,0 L14,10 L24,12 L14,14 L12,24 L10,14 L0,12 L10,10 Z"
|
<Path Canvas.Left="10" Canvas.Top="16" Data="M12,0 L14,10 L24,12 L14,14 L12,24 L10,14 L0,12 L10,10 Z"
|
||||||
Fill="{DynamicResource AccentColor}" Width="38" Height="38" Stretch="Uniform"/>
|
Fill="{DynamicResource AccentColor}" Width="40" Height="40" Stretch="Uniform"/>
|
||||||
<Path Canvas.Left="0" Canvas.Top="0" Data="M6,0 L7,5 L12,6 L7,7 L6,12 L5,7 L0,6 L5,5 Z"
|
<Path Canvas.Left="0" Canvas.Top="0" Data="M6,0 L7,5 L12,6 L7,7 L6,12 L5,7 L0,6 L5,5 Z"
|
||||||
Fill="{DynamicResource AccentColor}" Width="20" Height="20" Stretch="Uniform"/>
|
Fill="{DynamicResource AccentColor}" Width="22" Height="22" Stretch="Uniform"/>
|
||||||
<Path Canvas.Left="35" Canvas.Top="8" Data="M4,0 L5,3 L8,4 L5,5 L4,8 L3,5 L0,4 L3,3 Z"
|
<Path Canvas.Left="35" Canvas.Top="8" Data="M4,0 L5,3 L8,4 L5,5 L4,8 L3,5 L0,4 L3,3 Z"
|
||||||
Fill="{DynamicResource AccentColor}" Width="15" Height="15" Stretch="Uniform"/>
|
Fill="{DynamicResource AccentColor}" Width="17" Height="17" Stretch="Uniform"/>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
<!-- Actual logo image if available -->
|
|
||||||
<Image x:Name="LogoImage" Stretch="Uniform"/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Viewbox>
|
</Viewbox>
|
||||||
|
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<TextBlock Text="Welcome to Win11Debloat" FontSize="28" FontWeight="Bold" Foreground="{DynamicResource FgColor}" HorizontalAlignment="Center" Margin="0,0,0,10"/>
|
<TextBlock Text="Welcome to Win11Debloat" FontSize="30" FontWeight="Bold" Foreground="{DynamicResource FgColor}" HorizontalAlignment="Center" Margin="0,0,0,10"/>
|
||||||
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgColor}" FontSize="16" LineHeight="22" HorizontalAlignment="Center" Margin="0,0,0,30">
|
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgColor}" FontSize="18" LineHeight="22" HorizontalAlignment="Center" Margin="0,0,0,60">
|
||||||
<Run Text="Your clean Windows experience is just a few clicks away!"/>
|
<Run Text="Your clean Windows experience is just a few clicks away!"/>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<Border HorizontalAlignment="Center" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" CornerRadius="4" Background="{DynamicResource CardBgColor}" Padding="16,12" Width="500">
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,20,0,0">
|
<StackPanel>
|
||||||
<Button x:Name="HomeDefaultModeBtn" Width="180" Height="50" Style="{DynamicResource PrimaryButtonStyle}" Margin="0,0,12,0" AutomationProperties.Name="Default Mode">
|
<TextBlock Text="What user do you want to apply changes to?" Style="{StaticResource CategoryHeaderTextBlock}"/>
|
||||||
|
<ComboBox x:Name="UserSelectionCombo" Margin="0,0,0,6" AutomationProperties.Name="Apply Changes To">
|
||||||
|
<ComboBoxItem Content="Current User" IsSelected="True"/>
|
||||||
|
<ComboBoxItem Content="Other User"/>
|
||||||
|
<ComboBoxItem Content="Windows Default User (Sysprep)"/>
|
||||||
|
</ComboBox>
|
||||||
|
<StackPanel x:Name="OtherUserPanel" Visibility="Collapsed" Margin="0,0,0,6">
|
||||||
|
<TextBlock x:Name="UsernameValidationMessage" Text="" FontStyle="Italic" Foreground="{DynamicResource CloseHover}" FontSize="11" Margin="3,0,0,4" TextWrapping="Wrap"/>
|
||||||
|
<Border IsEnabled="{Binding ElementName=OtherUsernameTextBox, Path=IsEnabled}">
|
||||||
|
<Border.Style>
|
||||||
|
<Style TargetType="Border" BasedOn="{StaticResource UserTextBoxBorderStyle}">
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding ElementName=OtherUsernameTextBox, Path=IsFocused}" Value="True">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource InputFocusColor}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Border.Style>
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0" Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="4,0,8,0" Foreground="{DynamicResource FgColor}"/>
|
||||||
|
<TextBlock x:Name="UsernameTextBoxPlaceholder" Grid.Column="1" Text="Enter username" Foreground="{DynamicResource FgColor}" Opacity="0.5" FontSize="13" Margin="3,0,0,1" VerticalAlignment="Center" IsHitTestVisible="False"/>
|
||||||
|
<TextBox x:Name="OtherUsernameTextBox" Grid.Column="1" Style="{StaticResource UserTextBoxStyle}" Text="" AutomationProperties.Name="Enter username"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Separator Margin="0,10,0,8" Background="{DynamicResource BorderColor}"/>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,8,0,0">
|
||||||
|
<Button x:Name="HomeDefaultModeBtn" Width="227" Height="50" Style="{DynamicResource PrimaryButtonStyle}" Margin="0,0,12,0" AutomationProperties.Name="Default Mode">
|
||||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" VerticalAlignment="Center" Margin="0,0,8,-1"/>
|
||||||
<TextBlock Text="Default Mode" ToolTip="Quickly select the recommended settings" FontWeight="SemiBold" VerticalAlignment="Center" FontSize="17" Margin="0,0,0,2"/>
|
<TextBlock Text="Default Mode" ToolTip="Quickly select the recommended settings" FontWeight="SemiBold" VerticalAlignment="Center" FontSize="17" Margin="0,0,0,1"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
<Button x:Name="HomeStartBtn" Width="180" Height="50" Style="{DynamicResource SecondaryButtonStyle}" AutomationProperties.Name="Custom Setup">
|
<Button x:Name="HomeStartBtn" Width="227" Height="50" Style="{DynamicResource SecondaryButtonStyle}" AutomationProperties.Name="Custom Setup">
|
||||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,0,8,-1"/>
|
||||||
<TextBlock Text="Custom Setup" ToolTip="Manually select your preferred settings" FontWeight="SemiBold" VerticalAlignment="Center" FontSize="17" Margin="0,0,0,2"/>
|
<TextBlock Text="Custom Setup" ToolTip="Manually select your preferred settings" FontWeight="SemiBold" VerticalAlignment="Center" FontSize="17" Margin="0,0,0,1"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
@@ -732,6 +765,8 @@
|
|||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="10"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<StackPanel Grid.Column="0" Orientation="Horizontal">
|
<StackPanel Grid.Column="0" Orientation="Horizontal">
|
||||||
@@ -798,7 +833,9 @@
|
|||||||
</Popup>
|
</Popup>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Border x:Name="TweakSearchBorder" Grid.Column="2">
|
<CheckBox x:Name="ShowCurrentlyAppliedTweaksCheckBox" Grid.Column="2" Content="Detect applied tweaks" IsChecked="True" Foreground="{DynamicResource FgColor}" VerticalAlignment="Center" AutomationProperties.Name="Detect applied tweaks" ToolTip="Detect all tweaks currently applied for the current user."/>
|
||||||
|
|
||||||
|
<Border x:Name="TweakSearchBorder" Grid.Column="4">
|
||||||
<Border.Style>
|
<Border.Style>
|
||||||
<Style TargetType="Border" BasedOn="{StaticResource SearchBoxBorderStyle}">
|
<Style TargetType="Border" BasedOn="{StaticResource SearchBoxBorderStyle}">
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
@@ -891,43 +928,15 @@
|
|||||||
<!-- Apply Changes To -->
|
<!-- Apply Changes To -->
|
||||||
<Border Grid.Row="0" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" CornerRadius="4" Background="{DynamicResource CardBgColor}" Padding="16,12" Margin="0,0,0,16">
|
<Border Grid.Row="0" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" CornerRadius="4" Background="{DynamicResource CardBgColor}" Padding="16,12" Margin="0,0,0,16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="Apply Changes To" Style="{StaticResource CategoryHeaderTextBlock}"/>
|
<TextBlock Text="Changes will be applied to" Style="{StaticResource CategoryHeaderTextBlock}"/>
|
||||||
<ComboBox x:Name="UserSelectionCombo" Margin="0,0,0,6" AutomationProperties.Name="Apply Changes To">
|
<TextBlock x:Name="UserSelectionDescription" Text="The currently logged-in user profile." Foreground="{DynamicResource FgColor}" FontSize="12" TextWrapping="Wrap" Margin="0,0,0,3"/>
|
||||||
<ComboBoxItem Content="Current User" IsSelected="True"/>
|
|
||||||
<ComboBoxItem Content="Other User"/>
|
|
||||||
<ComboBoxItem Content="Windows Default User (Sysprep)"/>
|
|
||||||
</ComboBox>
|
|
||||||
<StackPanel x:Name="OtherUserPanel" Visibility="Collapsed" Margin="0,0,0,6">
|
|
||||||
<TextBlock x:Name="UsernameValidationMessage" Text="" FontStyle="Italic" Foreground="{DynamicResource CloseHover}" FontSize="11" Margin="3,0,0,4" TextWrapping="Wrap"/>
|
|
||||||
<Border IsEnabled="{Binding ElementName=OtherUsernameTextBox, Path=IsEnabled}">
|
|
||||||
<Border.Style>
|
|
||||||
<Style TargetType="Border" BasedOn="{StaticResource UserTextBoxBorderStyle}">
|
|
||||||
<Style.Triggers>
|
|
||||||
<DataTrigger Binding="{Binding ElementName=OtherUsernameTextBox, Path=IsFocused}" Value="True">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource InputFocusColor}"/>
|
|
||||||
</DataTrigger>
|
|
||||||
</Style.Triggers>
|
|
||||||
</Style>
|
|
||||||
</Border.Style>
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Grid.Column="0" Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="4,0,8,0" Foreground="{DynamicResource FgColor}"/>
|
|
||||||
<TextBlock x:Name="UsernameTextBoxPlaceholder" Grid.Column="1" Text="Enter username" Foreground="{DynamicResource FgColor}" Opacity="0.5" FontSize="13" Margin="3,0,0,1" VerticalAlignment="Center" IsHitTestVisible="False"/>
|
|
||||||
<TextBox x:Name="OtherUsernameTextBox" Grid.Column="1" Style="{StaticResource UserTextBoxStyle}" Text="" AutomationProperties.Name="Enter username"/>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
|
||||||
<TextBlock x:Name="UserSelectionDescription" Text="Changes will be applied to the currently logged-in user profile." Foreground="{DynamicResource FgColor}" FontSize="12" TextWrapping="Wrap" Margin="0,6,0,3"/>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- App Removal Scope -->
|
<!-- App Removal Scope -->
|
||||||
<Border Grid.Row="1" x:Name="AppRemovalScopeSection" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" CornerRadius="4" Background="{DynamicResource CardBgColor}" Padding="16,12" Margin="0,0,0,16">
|
<Border Grid.Row="1" x:Name="AppRemovalScopeSection" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" CornerRadius="4" Background="{DynamicResource CardBgColor}" Padding="16,12" Margin="0,0,0,16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="Remove Apps For" Style="{StaticResource CategoryHeaderTextBlock}"/>
|
<TextBlock Text="Apps will be removed for" Style="{StaticResource CategoryHeaderTextBlock}"/>
|
||||||
<ComboBox x:Name="AppRemovalScopeCombo" Margin="0,0,0,6" AutomationProperties.Name="App Removal Scope">
|
<ComboBox x:Name="AppRemovalScopeCombo" Margin="0,0,0,6" AutomationProperties.Name="App Removal Scope">
|
||||||
<ComboBoxItem x:Name="AppRemovalScopeAllUsers" Content="All users" IsSelected="True"/>
|
<ComboBoxItem x:Name="AppRemovalScopeAllUsers" Content="All users" IsSelected="True"/>
|
||||||
<ComboBoxItem x:Name="AppRemovalScopeCurrentUser" Content="Current user only"/>
|
<ComboBoxItem x:Name="AppRemovalScopeCurrentUser" Content="Current user only"/>
|
||||||
|
|||||||
@@ -222,6 +222,8 @@
|
|||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<Grid Grid.Row="0" Margin="0,0,0,16">
|
<Grid Grid.Row="0" Margin="0,0,0,16">
|
||||||
@@ -282,9 +284,38 @@
|
|||||||
Visibility="Collapsed"
|
Visibility="Collapsed"
|
||||||
Text="This will restore the Start Menu pinned apps layout for the current user."/>
|
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"/>
|
<Border x:Name="ReappliedSeparator" 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 x:Name="ReappliedPanel" Grid.Row="4" Visibility="Collapsed">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
Text="The following changes will be re-applied:"
|
||||||
|
FontSize="13"
|
||||||
|
Foreground="{DynamicResource FgColor}"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Margin="0,0,0,8"/>
|
||||||
|
|
||||||
|
<ItemsControl x:Name="ReappliedFeaturesItemsControl" Grid.Row="1">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel Orientation="Vertical" IsItemsHost="True"/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding DisplayText}" FontSize="12" Foreground="{DynamicResource FgColor}" TextWrapping="Wrap" Margin="0,0,0,6"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Border x:Name="NonRevertibleSeparator" Grid.Row="5" Height="1" Background="{DynamicResource BorderColor}" Margin="0,12,0,12" Visibility="Collapsed"/>
|
||||||
|
|
||||||
|
<Grid x:Name="NonRevertiblePanel" Grid.Row="6" Visibility="Collapsed">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="*"/>
|
<RowDefinition Height="*"/>
|
||||||
@@ -343,7 +374,7 @@
|
|||||||
LineHeight="20"
|
LineHeight="20"
|
||||||
Margin="0,0,0,12"
|
Margin="0,0,0,12"
|
||||||
Foreground="{DynamicResource FgColor}"
|
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."/>
|
Text="This will restore the Start Menu pinned apps layout for the selected user(s) using a backup that is automatically created by Win11Debloat. Manually created backups can also be used."/>
|
||||||
|
|
||||||
<ComboBox x:Name="StartMenuScopeCombo"
|
<ComboBox x:Name="StartMenuScopeCombo"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function RemoveApps {
|
|||||||
& $script:ApplySubStepCallback "Removing apps ($appIndex/$appCount)" $appIndex $appCount
|
& $script:ApplySubStepCallback "Removing apps ($appIndex/$appCount)" $appIndex $appCount
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Attempting to remove $app..."
|
Write-Host "Removing $app"
|
||||||
|
|
||||||
# Use WinGet only to remove OneDrive and Edge
|
# Use WinGet only to remove OneDrive and Edge
|
||||||
if (($app -eq "Microsoft.OneDrive") -or ($edgeIds -contains $app)) {
|
if (($app -eq "Microsoft.OneDrive") -or ($edgeIds -contains $app)) {
|
||||||
@@ -124,10 +124,7 @@ function RemoveApps {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
if ($DebugPreference -ne "SilentlyContinue") {
|
Write-Verbose "Something went wrong while trying to remove $($app): $_"
|
||||||
Write-Host "Something went wrong while trying to remove $app" -ForegroundColor Yellow
|
|
||||||
Write-Host $psitem.Exception.StackTrace -ForegroundColor Gray
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ function Get-FeatureId {
|
|||||||
|
|
||||||
function Get-RegistryBackedFeatures {
|
function Get-RegistryBackedFeatures {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[object[]]$Features = @()
|
||||||
[object[]]$Features
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return @($Features | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey) })
|
return @($Features | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey) })
|
||||||
|
|||||||
@@ -1,47 +1,44 @@
|
|||||||
function Get-RegistryBackupCapturePlans {
|
function Get-RegistryBackupCapturePlans {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[object[]]$SelectedRegistryFeatures = @(),
|
||||||
[object[]]$SelectedRegistryFeatures,
|
[object[]]$UndoRegistryFeatures = @(),
|
||||||
[switch]$UseSysprepRegFiles
|
[switch]$UseSysprepRegFiles
|
||||||
)
|
)
|
||||||
|
|
||||||
$planMap = @{}
|
$planMap = @{}
|
||||||
|
|
||||||
foreach ($feature in $SelectedRegistryFeatures) {
|
foreach ($feature in $SelectedRegistryFeatures) {
|
||||||
$regFilePath = Get-RegistryFilePathForFeature -Feature $feature -UseSysprepRegFiles:$UseSysprepRegFiles
|
$regFilePath = Get-RegistryFilePathForFeature -RegistryKey $feature.RegistryKey -UseSysprepRegFiles:$UseSysprepRegFiles
|
||||||
if (-not (Test-Path $regFilePath)) {
|
if (-not (Test-Path $regFilePath)) {
|
||||||
throw "Unable to find registry file for backup: $($feature.RegistryKey) ($regFilePath)"
|
throw "Unable to find registry file for backup: $($feature.RegistryKey) ($regFilePath)"
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($operation in @(Get-RegFileOperations -regFilePath $regFilePath)) {
|
foreach ($operation in @(Get-RegFileOperations -regFilePath $regFilePath)) {
|
||||||
if (-not $operation.KeyPath) { continue }
|
if (-not $operation.KeyPath) { continue }
|
||||||
|
Add-RegistryPlanOperation -PlanMap $planMap -Operation $operation
|
||||||
$mapKey = $operation.KeyPath.ToLowerInvariant()
|
|
||||||
if (-not $planMap.ContainsKey($mapKey)) {
|
|
||||||
$planMap[$mapKey] = [PSCustomObject]@{
|
|
||||||
Path = $operation.KeyPath
|
|
||||||
IncludeSubKeys = $false
|
|
||||||
CaptureAllValues = $false
|
|
||||||
ValueNames = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$plan = $planMap[$mapKey]
|
foreach ($feature in $UndoRegistryFeatures) {
|
||||||
switch ($operation.OperationType) {
|
$regFilePath = Resolve-RegistryBackupUndoFilePath -Feature $feature
|
||||||
'DeleteKey' {
|
if ([string]::IsNullOrWhiteSpace($regFilePath)) {
|
||||||
$plan.IncludeSubKeys = $true
|
continue
|
||||||
$plan.CaptureAllValues = $true
|
|
||||||
}
|
}
|
||||||
'SetValue' {
|
|
||||||
if (-not $plan.CaptureAllValues) {
|
if (-not (Test-Path $regFilePath)) {
|
||||||
$null = $plan.ValueNames.Add([string]$operation.ValueName)
|
$undoKeyDescription = if (-not [string]::IsNullOrWhiteSpace([string]$feature.RegistryUndoKey)) {
|
||||||
}
|
[string]$feature.RegistryUndoKey
|
||||||
}
|
|
||||||
'DeleteValue' {
|
|
||||||
if (-not $plan.CaptureAllValues) {
|
|
||||||
$null = $plan.ValueNames.Add([string]$operation.ValueName)
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
[string]$feature.RegistryKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw "Unable to find registry undo file for backup: $undoKeyDescription ($regFilePath)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ($operation in @(Get-RegFileOperations -regFilePath $regFilePath)) {
|
||||||
|
if (-not $operation.KeyPath) { continue }
|
||||||
|
Add-RegistryPlanOperation -PlanMap $planMap -Operation $operation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,10 +54,68 @@ function Get-RegistryBackupCapturePlans {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-RegistrySnapshotsForBackup {
|
function Add-RegistryPlanOperation {
|
||||||
|
param(
|
||||||
|
[hashtable]$PlanMap,
|
||||||
|
[PSCustomObject]$Operation
|
||||||
|
)
|
||||||
|
|
||||||
|
$mapKey = $Operation.KeyPath.ToLowerInvariant()
|
||||||
|
if (-not $PlanMap.ContainsKey($mapKey)) {
|
||||||
|
$PlanMap[$mapKey] = [PSCustomObject]@{
|
||||||
|
Path = $Operation.KeyPath
|
||||||
|
IncludeSubKeys = $false
|
||||||
|
CaptureAllValues = $false
|
||||||
|
ValueNames = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$plan = $PlanMap[$mapKey]
|
||||||
|
switch ($Operation.OperationType) {
|
||||||
|
'DeleteKey' {
|
||||||
|
$plan.IncludeSubKeys = $true
|
||||||
|
$plan.CaptureAllValues = $true
|
||||||
|
}
|
||||||
|
'SetValue' {
|
||||||
|
if (-not $plan.CaptureAllValues) {
|
||||||
|
$null = $plan.ValueNames.Add([string]$Operation.ValueName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'DeleteValue' {
|
||||||
|
if (-not $plan.CaptureAllValues) {
|
||||||
|
$null = $plan.ValueNames.Add([string]$Operation.ValueName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-RegistryBackupUndoFilePath {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
[object[]]$CapturePlans
|
$Feature
|
||||||
|
)
|
||||||
|
|
||||||
|
$undoRegistryKey = [string]$Feature.RegistryUndoKey
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($undoRegistryKey)) {
|
||||||
|
$resolvedUndoPath = Resolve-UndoRegFilePath -FileName $undoRegistryKey
|
||||||
|
return Join-Path $script:RegfilesPath $resolvedUndoPath
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedRegistryKey = [string]$Feature.RegistryKey
|
||||||
|
if ([string]::IsNullOrWhiteSpace($resolvedRegistryKey)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([System.IO.Path]::IsPathRooted($resolvedRegistryKey)) {
|
||||||
|
return $resolvedRegistryKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return Join-Path $script:RegfilesPath $resolvedRegistryKey
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RegistrySnapshotsForBackup {
|
||||||
|
param(
|
||||||
|
[object[]]$CapturePlans = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
if ($CapturePlans.Count -eq 0) {
|
if ($CapturePlans.Count -eq 0) {
|
||||||
@@ -92,31 +147,14 @@ function Invoke-WithLoadedBackupHive {
|
|||||||
$ArgumentObject = $null
|
$ArgumentObject = $null
|
||||||
)
|
)
|
||||||
|
|
||||||
$hiveDatPath = if ($script:Params.ContainsKey('Sysprep')) {
|
$targetUserName = if ($script:Params.ContainsKey('Sysprep')) {
|
||||||
GetUserDirectory -userName 'Default' -fileName 'NTUSER.DAT'
|
'Default'
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
GetUserDirectory -userName $script:Params.Item('User') -fileName 'NTUSER.DAT'
|
$script:Params.Item('User')
|
||||||
}
|
}
|
||||||
|
|
||||||
$global:LASTEXITCODE = 0
|
return Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $ScriptBlock -ArgumentObject $ArgumentObject
|
||||||
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 {
|
function Get-RegistryKeySnapshot {
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
function New-RegistrySettingsBackup {
|
function New-RegistrySettingsBackup {
|
||||||
param(
|
param(
|
||||||
[string[]]$ActionableKeys
|
[string[]]$ActionableKeys,
|
||||||
|
[object[]]$ExtraFeatures = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$ActionableKeys = @($ActionableKeys)
|
$ActionableKeys = @($ActionableKeys)
|
||||||
$selectedFeatures = Get-SelectedFeatures -ActionableKeys $ActionableKeys
|
$selectedFeatures = @(Get-SelectedFeatures -ActionableKeys $ActionableKeys)
|
||||||
if (@($selectedFeatures | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey) }).Count -eq 0) {
|
$undoFeatures = @($ExtraFeatures | Where-Object { $_ -ne $null })
|
||||||
|
$allFeatures = @($selectedFeatures) + @($undoFeatures)
|
||||||
|
if (@($allFeatures | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey) }).Count -eq 0) {
|
||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,7 +21,7 @@ function New-RegistrySettingsBackup {
|
|||||||
$backupFileName = 'Win11Debloat-RegistryBackup-{0}.json' -f $timestamp.ToString('yyyyMMdd_HHmmss')
|
$backupFileName = 'Win11Debloat-RegistryBackup-{0}.json' -f $timestamp.ToString('yyyyMMdd_HHmmss')
|
||||||
$backupFilePath = Join-Path $backupDirectory $backupFileName
|
$backupFilePath = Join-Path $backupDirectory $backupFileName
|
||||||
|
|
||||||
$backupConfig = Get-RegistryBackupPayload -SelectedFeatures $selectedFeatures -CreatedAt $timestamp
|
$backupConfig = Get-RegistryBackupPayload -SelectedFeatures $selectedFeatures -UndoFeatures $undoFeatures -CreatedAt $timestamp
|
||||||
if (-not (SaveToFile -Config $backupConfig -FilePath $backupFilePath -MaxDepth 25)) {
|
if (-not (SaveToFile -Config $backupConfig -FilePath $backupFilePath -MaxDepth 25)) {
|
||||||
throw "Failed to save registry backup to '$backupFilePath'"
|
throw "Failed to save registry backup to '$backupFilePath'"
|
||||||
}
|
}
|
||||||
@@ -55,8 +58,8 @@ function Get-SelectedFeatures {
|
|||||||
|
|
||||||
function Get-RegistryBackupPayload {
|
function Get-RegistryBackupPayload {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[object[]]$SelectedFeatures = @(),
|
||||||
[object[]]$SelectedFeatures,
|
[object[]]$UndoFeatures = @(),
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
[datetime]$CreatedAt
|
[datetime]$CreatedAt
|
||||||
)
|
)
|
||||||
@@ -71,11 +74,24 @@ function Get-RegistryBackupPayload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$selectedRegistryFeatures = Get-RegistryBackedFeatures -Features $SelectedFeatures
|
$selectedUndoFeatureIds = New-Object System.Collections.Generic.List[string]
|
||||||
$capturePlans = Get-RegistryBackupCapturePlans -SelectedRegistryFeatures $SelectedRegistryFeatures
|
$seenUndoFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
foreach ($feature in $UndoFeatures) {
|
||||||
|
$featureId = Get-FeatureId -Feature $feature
|
||||||
|
|
||||||
|
if ($seenUndoFeatureIds.Add($featureId)) {
|
||||||
|
$selectedUndoFeatureIds.Add($featureId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedRegistryFeatures = @(Get-RegistryBackedFeatures -Features $SelectedFeatures)
|
||||||
|
$undoRegistryFeatures = @($UndoFeatures | Where-Object {
|
||||||
|
-not [string]::IsNullOrWhiteSpace([string]$_.RegistryUndoKey) -or -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey)
|
||||||
|
})
|
||||||
|
$capturePlans = @(Get-RegistryBackupCapturePlans -SelectedRegistryFeatures $selectedRegistryFeatures -UndoRegistryFeatures $undoRegistryFeatures)
|
||||||
$registryKeys = @(Get-RegistrySnapshotsForBackup -CapturePlans $capturePlans)
|
$registryKeys = @(Get-RegistrySnapshotsForBackup -CapturePlans $capturePlans)
|
||||||
|
|
||||||
return @{
|
$backupPayload = @{
|
||||||
Version = '1.0'
|
Version = '1.0'
|
||||||
BackupType = 'RegistryState'
|
BackupType = 'RegistryState'
|
||||||
CreatedAt = $CreatedAt.ToString('o')
|
CreatedAt = $CreatedAt.ToString('o')
|
||||||
@@ -85,4 +101,10 @@ function Get-RegistryBackupPayload {
|
|||||||
SelectedFeatures = @($selectedFeatureIds)
|
SelectedFeatures = @($selectedFeatureIds)
|
||||||
RegistryKeys = @($registryKeys)
|
RegistryKeys = @($registryKeys)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($selectedUndoFeatureIds.Count -gt 0) {
|
||||||
|
$backupPayload['SelectedUndoFeatures'] = @($selectedUndoFeatureIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $backupPayload
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ function CreateSystemRestorePoint {
|
|||||||
# In GUI mode, skip the prompt and just try to enable it
|
# In GUI mode, skip the prompt and just try to enable it
|
||||||
if ($script:GuiWindow -or $Silent -or $( Read-Host -Prompt "System restore is disabled, would you like to enable it and create a restore point? (y/n)") -eq 'y') {
|
if ($script:GuiWindow -or $Silent -or $( Read-Host -Prompt "System restore is disabled, would you like to enable it and create a restore point? (y/n)") -eq 'y') {
|
||||||
try {
|
try {
|
||||||
$enableResult = Invoke-NonBlocking -TimeoutSeconds 20 -ScriptBlock {
|
$enableResult = Invoke-NonBlocking -TimeoutSeconds 90 -ScriptBlock {
|
||||||
try {
|
try {
|
||||||
Enable-ComputerRestore -Drive "$env:SystemDrive"
|
Enable-ComputerRestore -Drive "$env:SystemDrive"
|
||||||
return $null
|
return $null
|
||||||
@@ -33,7 +33,7 @@ function CreateSystemRestorePoint {
|
|||||||
|
|
||||||
if (-not $failed) {
|
if (-not $failed) {
|
||||||
try {
|
try {
|
||||||
$result = Invoke-NonBlocking -TimeoutSeconds 20 -ScriptBlock {
|
$result = Invoke-NonBlocking -TimeoutSeconds 90 -ScriptBlock {
|
||||||
try {
|
try {
|
||||||
$recentRestorePoints = Get-ComputerRestorePoint | Where-Object { (Get-Date) - [System.Management.ManagementDateTimeConverter]::ToDateTime($_.CreationTime) -le (New-TimeSpan -Hours 24) }
|
$recentRestorePoints = Get-ComputerRestorePoint | Where-Object { (Get-Date) - [System.Management.ManagementDateTimeConverter]::ToDateTime($_.CreationTime) -le (New-TimeSpan -Hours 24) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,3 +50,136 @@ function DisableStoreSearchSuggestions {
|
|||||||
|
|
||||||
Write-Host "Disabled Microsoft Store search suggestions for user $userName"
|
Write-Host "Disabled Microsoft Store search suggestions for user $userName"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function EnableStoreSearchSuggestionsForAllUsers {
|
||||||
|
# Get path to Store app database for all users
|
||||||
|
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages"
|
||||||
|
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
# Go through all users and re-enable start search suggestions
|
||||||
|
ForEach ($storeDbPath in $usersStoreDbPaths) {
|
||||||
|
EnableStoreSearchSuggestions ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Also re-enable for the default user profile
|
||||||
|
$defaultStoreDbPath = GetUserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false
|
||||||
|
EnableStoreSearchSuggestions $defaultStoreDbPath
|
||||||
|
}
|
||||||
|
|
||||||
|
function EnableStoreSearchSuggestions {
|
||||||
|
param (
|
||||||
|
$StoreAppsDatabase = "$env:LocalAppData\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Change path to correct user if a user was specified
|
||||||
|
if ($script:Params.ContainsKey("User")) {
|
||||||
|
$StoreAppsDatabase = GetUserDirectory -userName "$(GetUserName)" -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$userName = [regex]::Match($StoreAppsDatabase, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value
|
||||||
|
if (-not $userName) { $userName = '<unknown>' }
|
||||||
|
|
||||||
|
if (-not (Test-Path -Path $StoreAppsDatabase)) {
|
||||||
|
Write-Host "Store app database not found for user $userName, nothing to undo"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure we can modify/delete the file even if restrictive ACLs were set.
|
||||||
|
$global:LASTEXITCODE = 0
|
||||||
|
takeown /F "$StoreAppsDatabase" /A | Out-Null
|
||||||
|
icacls "$StoreAppsDatabase" /grant *S-1-5-32-544:F /C | Out-Null
|
||||||
|
|
||||||
|
$everyoneSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0') # 'EVERYONE' group
|
||||||
|
|
||||||
|
try {
|
||||||
|
$acl = Get-Acl -Path $StoreAppsDatabase
|
||||||
|
$denyRules = @(
|
||||||
|
$acl.Access | Where-Object {
|
||||||
|
$_.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Deny -and
|
||||||
|
(($_.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne 0) -and
|
||||||
|
(try { $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $everyoneSid } catch { $false })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($denyRule in $denyRules) {
|
||||||
|
$null = $acl.RemoveAccessRuleSpecific($denyRule)
|
||||||
|
}
|
||||||
|
|
||||||
|
Set-Acl -Path $StoreAppsDatabase -AclObject $acl | Out-Null
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Failed to normalize ACL for store database '$StoreAppsDatabase': $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Remove-Item -Path $StoreAppsDatabase -Force -ErrorAction Stop
|
||||||
|
Write-Host "Re-enabled Microsoft Store search suggestions for user $userName"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
throw "Failed to remove '$StoreAppsDatabase' while undoing Microsoft Store search suggestions for user $userName. $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-StoreSearchSuggestionsDisabled {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$StoreAppsDatabase
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-Path -Path $StoreAppsDatabase)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$acl = Get-Acl -Path $StoreAppsDatabase
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$everyoneSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0')
|
||||||
|
|
||||||
|
foreach ($accessRule in @($acl.Access)) {
|
||||||
|
$isDenyFullControl = $accessRule.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Deny -and
|
||||||
|
(($accessRule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne 0)
|
||||||
|
if (-not $isDenyFullControl) { continue }
|
||||||
|
|
||||||
|
$isEveryone = $false
|
||||||
|
try {
|
||||||
|
$isEveryone = $accessRule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $everyoneSid
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
if ($isEveryone) {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-StoreSearchSuggestionsDisabledForAllUsers {
|
||||||
|
$paths = @()
|
||||||
|
|
||||||
|
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages"
|
||||||
|
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
|
||||||
|
foreach ($storeDbPath in $usersStoreDbPaths) {
|
||||||
|
$paths += ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaultStoreDbPath = GetUserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false
|
||||||
|
if ($defaultStoreDbPath) {
|
||||||
|
$paths += $defaultStoreDbPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($paths.Count -eq 0) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($path in $paths) {
|
||||||
|
if (-not (Test-StoreSearchSuggestionsDisabled -StoreAppsDatabase $path)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $true
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# Enables a Windows optional feature and pipes its output to the console
|
|
||||||
function EnableWindowsFeature {
|
|
||||||
param (
|
|
||||||
[string]$FeatureName
|
|
||||||
)
|
|
||||||
|
|
||||||
$result = Invoke-NonBlocking -ScriptBlock {
|
|
||||||
param($name)
|
|
||||||
Enable-WindowsOptionalFeature -Online -FeatureName $name -All -NoRestart
|
|
||||||
} -ArgumentList $FeatureName
|
|
||||||
|
|
||||||
$dismResult = @($result) | Where-Object { $_ -is [Microsoft.Dism.Commands.ImageObject] }
|
|
||||||
if ($dismResult) {
|
|
||||||
Write-Host ($dismResult | Out-String).Trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,21 +14,17 @@ function ExecuteParameter {
|
|||||||
|
|
||||||
# If feature has RegistryKey and ApplyText, use dynamic ImportRegistryFile
|
# If feature has RegistryKey and ApplyText, use dynamic ImportRegistryFile
|
||||||
if ($feature -and $feature.RegistryKey -and $feature.ApplyText) {
|
if ($feature -and $feature.RegistryKey -and $feature.ApplyText) {
|
||||||
ImportRegistryFile "> $($feature.ApplyText)" $feature.RegistryKey
|
ImportRegistryFile "> $($feature.ApplyText)..." $feature.RegistryKey
|
||||||
|
|
||||||
# Handle special cases that have additional logic after ImportRegistryFile
|
# Handle special cases that have additional logic after ImportRegistryFile
|
||||||
switch ($paramKey) {
|
switch ($paramKey) {
|
||||||
'DisableBing' {
|
'DisableBing' {
|
||||||
# Also remove the app package for Bing search
|
# Also remove the app package for Bing search
|
||||||
RemoveApps 'Microsoft.BingSearch'
|
RemoveApps @('Microsoft.BingSearch')
|
||||||
}
|
}
|
||||||
'DisableCopilot' {
|
'DisableCopilot' {
|
||||||
# Also remove the app package for Copilot
|
# Also remove the app package for Copilot
|
||||||
RemoveApps 'Microsoft.Copilot'
|
RemoveApps @('Microsoft.Copilot')
|
||||||
}
|
|
||||||
'DisableWidgets' {
|
|
||||||
# Also remove the app packages for Widgets
|
|
||||||
RemoveApps 'Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -37,7 +33,7 @@ function ExecuteParameter {
|
|||||||
# Handle features without RegistryKey or with special logic
|
# Handle features without RegistryKey or with special logic
|
||||||
switch ($paramKey) {
|
switch ($paramKey) {
|
||||||
'RemoveApps' {
|
'RemoveApps' {
|
||||||
Write-Host "> Removing selected apps for $(GetFriendlyTargetUserName)..."
|
Write-Host "> $($feature.ApplyText) for $(GetFriendlyTargetUserName)..."
|
||||||
$appsList = GenerateAppsList
|
$appsList = GenerateAppsList
|
||||||
|
|
||||||
if ($appsList.Count -eq 0) {
|
if ($appsList.Count -eq 0) {
|
||||||
@@ -50,7 +46,7 @@ function ExecuteParameter {
|
|||||||
RemoveApps $appsList
|
RemoveApps $appsList
|
||||||
}
|
}
|
||||||
'RemoveAppsCustom' {
|
'RemoveAppsCustom' {
|
||||||
Write-Host "> Removing selected apps..."
|
Write-Host "> $($feature.ApplyText)..."
|
||||||
$appsList = LoadAppsFromFile $script:CustomAppsListFilePath
|
$appsList = LoadAppsFromFile $script:CustomAppsListFilePath
|
||||||
|
|
||||||
if ($appsList.Count -eq 0) {
|
if ($appsList.Count -eq 0) {
|
||||||
@@ -62,51 +58,46 @@ function ExecuteParameter {
|
|||||||
Write-Host "$($appsList.Count) apps selected for removal"
|
Write-Host "$($appsList.Count) apps selected for removal"
|
||||||
RemoveApps $appsList
|
RemoveApps $appsList
|
||||||
}
|
}
|
||||||
'RemoveCommApps' {
|
|
||||||
$appsList = 'Microsoft.windowscommunicationsapps', 'Microsoft.People'
|
|
||||||
Write-Host "> Removing Mail, Calendar and People apps..."
|
|
||||||
RemoveApps $appsList
|
|
||||||
return
|
|
||||||
}
|
|
||||||
'RemoveW11Outlook' {
|
|
||||||
$appsList = 'Microsoft.OutlookForWindows'
|
|
||||||
Write-Host "> Removing new Outlook for Windows app..."
|
|
||||||
RemoveApps $appsList
|
|
||||||
return
|
|
||||||
}
|
|
||||||
'RemoveGamingApps' {
|
'RemoveGamingApps' {
|
||||||
$appsList = 'Microsoft.GamingApp', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay'
|
$appsList = @('Microsoft.GamingApp', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay')
|
||||||
Write-Host "> Removing gaming related apps..."
|
Write-Host "> $($feature.ApplyText)..."
|
||||||
RemoveApps $appsList
|
RemoveApps $appsList
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
'RemoveHPApps' {
|
'RemoveHPApps' {
|
||||||
$appsList = 'AD2F1837.HPAIExperienceCenter', 'AD2F1837.HPJumpStarts', 'AD2F1837.HPPCHardwareDiagnosticsWindows', 'AD2F1837.HPPowerManager', 'AD2F1837.HPPrivacySettings', 'AD2F1837.HPSupportAssistant', 'AD2F1837.HPSureShieldAI', 'AD2F1837.HPSystemInformation', 'AD2F1837.HPQuickDrop', 'AD2F1837.HPWorkWell', 'AD2F1837.myHP', 'AD2F1837.HPDesktopSupportUtilities', 'AD2F1837.HPQuickTouch', 'AD2F1837.HPEasyClean', 'AD2F1837.HPConnectedMusic', 'AD2F1837.HPFileViewer', 'AD2F1837.HPRegistration', 'AD2F1837.HPWelcome', 'AD2F1837.HPConnectedPhotopoweredbySnapfish', 'AD2F1837.HPPrinterControl'
|
$appsList = @('AD2F1837.HPAIExperienceCenter', 'AD2F1837.HPJumpStarts', 'AD2F1837.HPPCHardwareDiagnosticsWindows', 'AD2F1837.HPPowerManager', 'AD2F1837.HPPrivacySettings', 'AD2F1837.HPSupportAssistant', 'AD2F1837.HPSureShieldAI', 'AD2F1837.HPSystemInformation', 'AD2F1837.HPQuickDrop', 'AD2F1837.HPWorkWell', 'AD2F1837.myHP', 'AD2F1837.HPDesktopSupportUtilities', 'AD2F1837.HPQuickTouch', 'AD2F1837.HPEasyClean', 'AD2F1837.HPConnectedMusic', 'AD2F1837.HPFileViewer', 'AD2F1837.HPRegistration', 'AD2F1837.HPWelcome', 'AD2F1837.HPConnectedPhotopoweredbySnapfish', 'AD2F1837.HPPrinterControl')
|
||||||
Write-Host "> Removing HP apps..."
|
Write-Host "> $($feature.ApplyText)..."
|
||||||
RemoveApps $appsList
|
RemoveApps $appsList
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
"EnableWindowsSandbox" {
|
'DisableWidgets' {
|
||||||
Write-Host "> Enabling Windows Sandbox..."
|
Write-Host "> $($feature.ApplyText)..."
|
||||||
|
# Stop widgets related processes before removing the app packages to prevent potential issues
|
||||||
|
Get-Process *Widget* -ErrorAction SilentlyContinue | Stop-Process
|
||||||
|
|
||||||
|
RemoveApps @('Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime')
|
||||||
|
}
|
||||||
|
'EnableWindowsSandbox' {
|
||||||
|
Write-Host "> $($feature.ApplyText)..."
|
||||||
EnableWindowsFeature "Containers-DisposableClientVM"
|
EnableWindowsFeature "Containers-DisposableClientVM"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
"EnableWindowsSubsystemForLinux" {
|
'EnableWindowsSubsystemForLinux' {
|
||||||
Write-Host "> Enabling Windows Subsystem for Linux..."
|
Write-Host "> $($feature.ApplyText)..."
|
||||||
EnableWindowsFeature "VirtualMachinePlatform"
|
EnableWindowsFeature "VirtualMachinePlatform"
|
||||||
EnableWindowsFeature "Microsoft-Windows-Subsystem-Linux"
|
EnableWindowsFeature "Microsoft-Windows-Subsystem-Linux"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
'ClearStart' {
|
'ClearStart' {
|
||||||
Write-Host "> Removing all pinned apps from the start menu for user $(GetUserName)..."
|
Write-Host "> $($feature.ApplyText) for user $(GetUserName)..."
|
||||||
ReplaceStartMenu
|
ReplaceStartMenu
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
'ReplaceStart' {
|
'ReplaceStart' {
|
||||||
Write-Host "> Replacing the start menu for user $(GetUserName)..."
|
Write-Host "> $($feature.ApplyText) for user $(GetUserName)..."
|
||||||
ReplaceStartMenu $script:Params.Item("ReplaceStart")
|
ReplaceStartMenu $script:Params.Item("ReplaceStart")
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
return
|
return
|
||||||
@@ -138,6 +129,15 @@ function ExecuteParameter {
|
|||||||
|
|
||||||
# Executes all selected parameters/features
|
# Executes all selected parameters/features
|
||||||
function ExecuteAllChanges {
|
function ExecuteAllChanges {
|
||||||
|
# When running as SYSTEM, require -User or -Sysprep to prevent applying
|
||||||
|
# changes to the SYSTEM profile instead of a real user.
|
||||||
|
$isSystem = ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value -eq 'S-1-5-18')
|
||||||
|
if ($isSystem -and -not $script:Params.ContainsKey("User") -and -not $script:Params.ContainsKey("Sysprep")) {
|
||||||
|
throw "Win11Debloat is running as the SYSTEM account. Use the '-User' or '-Sysprep' parameter to target a specific user."
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:RegistryImportFailures = 0
|
||||||
|
|
||||||
# Build list of actionable parameters (skip control params and data-only params)
|
# Build list of actionable parameters (skip control params and data-only params)
|
||||||
$actionableKeys = @()
|
$actionableKeys = @()
|
||||||
foreach ($paramKey in $script:Params.Keys) {
|
foreach ($paramKey in $script:Params.Keys) {
|
||||||
@@ -157,8 +157,15 @@ function ExecuteAllChanges {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
# Undo operations that write registry values also require a backup
|
||||||
|
if (-not $hasRegistryBackedFeature) {
|
||||||
|
foreach ($featureId in $script:UndoParams.Keys) {
|
||||||
|
$f = if ($script:Features.ContainsKey($featureId)) { $script:Features[$featureId] } else { $null }
|
||||||
|
if ($f -and $f.RegistryUndoKey) { $hasRegistryBackedFeature = $true; break }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$totalSteps = $actionableKeys.Count
|
$totalSteps = $actionableKeys.Count + $script:UndoParams.Count
|
||||||
if ($hasRegistryBackedFeature) { $totalSteps++ }
|
if ($hasRegistryBackedFeature) { $totalSteps++ }
|
||||||
if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ }
|
if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ }
|
||||||
$currentStep = 0
|
$currentStep = 0
|
||||||
@@ -166,12 +173,18 @@ function ExecuteAllChanges {
|
|||||||
if ($hasRegistryBackedFeature) {
|
if ($hasRegistryBackedFeature) {
|
||||||
$currentStep++
|
$currentStep++
|
||||||
if ($script:ApplyProgressCallback) {
|
if ($script:ApplyProgressCallback) {
|
||||||
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating registry backup"
|
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating registry backup..."
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "> Creating registry backup..."
|
Write-Host "> Creating registry backup..."
|
||||||
try {
|
try {
|
||||||
New-RegistrySettingsBackup -ActionableKeys $actionableKeys | Out-Null
|
$undoSyntheticFeatures = @($script:UndoParams.Keys | ForEach-Object {
|
||||||
|
$f = if ($script:Features.ContainsKey($_)) { $script:Features[$_] } else { $null }
|
||||||
|
if ($f -and $f.RegistryUndoKey) {
|
||||||
|
[PSCustomObject]@{ FeatureId = $_; RegistryKey = (Resolve-UndoRegFilePath $f.RegistryUndoKey) }
|
||||||
|
}
|
||||||
|
} | Where-Object { $_ })
|
||||||
|
New-RegistrySettingsBackup -ActionableKeys $actionableKeys -ExtraFeatures $undoSyntheticFeatures | Out-Null
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
throw "Registry backup failed before applying changes. $($_.Exception.Message)"
|
throw "Registry backup failed before applying changes. $($_.Exception.Message)"
|
||||||
@@ -182,18 +195,16 @@ function ExecuteAllChanges {
|
|||||||
if ($script:Params.ContainsKey("CreateRestorePoint")) {
|
if ($script:Params.ContainsKey("CreateRestorePoint")) {
|
||||||
$currentStep++
|
$currentStep++
|
||||||
if ($script:ApplyProgressCallback) {
|
if ($script:ApplyProgressCallback) {
|
||||||
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating system restore point"
|
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating system restore point, this may take a moment..."
|
||||||
}
|
}
|
||||||
Write-Host "> Attempting to create a system restore point..."
|
Write-Host "> Creating a system restore point..."
|
||||||
CreateSystemRestorePoint
|
CreateSystemRestorePoint
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
}
|
}
|
||||||
|
|
||||||
# Execute all parameters
|
# Execute all parameters
|
||||||
foreach ($paramKey in $actionableKeys) {
|
foreach ($paramKey in $actionableKeys) {
|
||||||
if ($script:CancelRequested) {
|
if ($script:CancelRequested) { return }
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$currentStep++
|
$currentStep++
|
||||||
|
|
||||||
@@ -216,4 +227,83 @@ function ExecuteAllChanges {
|
|||||||
|
|
||||||
ExecuteParameter -paramKey $paramKey
|
ExecuteParameter -paramKey $paramKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Execute all undo operations
|
||||||
|
foreach ($featureId in $script:UndoParams.Keys) {
|
||||||
|
if ($script:CancelRequested) { return }
|
||||||
|
|
||||||
|
$f = if ($script:Features.ContainsKey($featureId)) { $script:Features[$featureId] } else { $null }
|
||||||
|
$undoLabel = if ($f -and $f.UndoLabel) { $f.UndoLabel } else { $featureId }
|
||||||
|
$applyUndoText = if ($f -and $f.ApplyUndoText) { $f.ApplyUndoText } else { $undoLabel }
|
||||||
|
|
||||||
|
$currentStep++
|
||||||
|
if ($script:ApplyProgressCallback) {
|
||||||
|
& $script:ApplyProgressCallback $currentStep $totalSteps $applyUndoText
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($f -and $f.RegistryUndoKey) {
|
||||||
|
ImportRegistryFile "> $applyUndoText" (Resolve-UndoRegFilePath $f.RegistryUndoKey)
|
||||||
|
} else {
|
||||||
|
Invoke-UndoFeatureAction -FeatureId $featureId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($script:RegistryImportFailures -gt 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "$($script:RegistryImportFailures) registry import change(s) failed. See output above for details." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolves the path of an undo reg file relative to $script:RegfilesPath.
|
||||||
|
# Checks the Undo/ subfolder first, then falls back to the root Regfiles/ folder.
|
||||||
|
function Resolve-UndoRegFilePath {
|
||||||
|
param ([string]$FileName)
|
||||||
|
$undoSubPath = Join-Path 'Undo' $FileName
|
||||||
|
if (Test-Path (Join-Path $script:RegfilesPath $undoSubPath)) {
|
||||||
|
return $undoSubPath
|
||||||
|
}
|
||||||
|
return $FileName
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-UndoFeatureAction {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$FeatureId
|
||||||
|
)
|
||||||
|
|
||||||
|
$feature = if ($script:Features.ContainsKey($FeatureId)) { $script:Features[$FeatureId] } else { $null }
|
||||||
|
|
||||||
|
switch ($FeatureId) {
|
||||||
|
'DisableStoreSearchSuggestions' {
|
||||||
|
if ($script:Params.ContainsKey('Sysprep')) {
|
||||||
|
Write-Host "> Re-enabling Microsoft Store search suggestions in the start menu for all users..."
|
||||||
|
EnableStoreSearchSuggestionsForAllUsers
|
||||||
|
Write-Host ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "> Re-enabling Microsoft Store search suggestions for user $(GetUserName)..."
|
||||||
|
EnableStoreSearchSuggestions
|
||||||
|
Write-Host ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
'EnableWindowsSandbox' {
|
||||||
|
Write-Host "> $($feature.ApplyUndoText)..."
|
||||||
|
DisableWindowsFeature 'Containers-DisposableClientVM'
|
||||||
|
Write-Host ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
'EnableWindowsSubsystemForLinux' {
|
||||||
|
Write-Host "> $($feature.ApplyUndoText)..."
|
||||||
|
DisableWindowsFeature 'Microsoft-Windows-Subsystem-Linux'
|
||||||
|
DisableWindowsFeature 'VirtualMachinePlatform'
|
||||||
|
Write-Host ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
Write-Host "> No undo action defined for $FeatureId, skipping..." -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
175
Scripts/Features/GetCurrentTweakState.ps1
Normal file
175
Scripts/Features/GetCurrentTweakState.ps1
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# Tests whether the registry operations in a feature's .reg file currently match the live registry.
|
||||||
|
# Returns $true if ALL operations in the apply reg file match current system state.
|
||||||
|
# Returns $false if the feature has no RegistryKey, the file is missing, or any operation mismatches.
|
||||||
|
function Get-ExpectedRegistryValueKind {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Operation
|
||||||
|
)
|
||||||
|
|
||||||
|
switch ([string]$Operation.ValueType) {
|
||||||
|
'DWord' { return [Microsoft.Win32.RegistryValueKind]::DWord }
|
||||||
|
'QWord' { return [Microsoft.Win32.RegistryValueKind]::QWord }
|
||||||
|
'String' { return [Microsoft.Win32.RegistryValueKind]::String }
|
||||||
|
'Binary' { return [Microsoft.Win32.RegistryValueKind]::Binary }
|
||||||
|
'Hex2' { return [Microsoft.Win32.RegistryValueKind]::ExpandString }
|
||||||
|
'Hex7' { return [Microsoft.Win32.RegistryValueKind]::MultiString }
|
||||||
|
default { return $null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-FeatureApplied {
|
||||||
|
param (
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$FeatureId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $script:Features.ContainsKey($FeatureId)) { return $false }
|
||||||
|
$feature = $script:Features[$FeatureId]
|
||||||
|
|
||||||
|
switch ($FeatureId) {
|
||||||
|
'DisableWidgets' {
|
||||||
|
# Widgets packages cannot be reinstalled automatically, so we treat their
|
||||||
|
# absence as the applied state (checked) and presence as not-yet-applied.
|
||||||
|
$widgetAppIds = @(
|
||||||
|
'Microsoft.StartExperiencesApp',
|
||||||
|
'MicrosoftWindows.Client.WebExperience',
|
||||||
|
'Microsoft.WidgetsPlatformRuntime'
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($appId in $widgetAppIds) {
|
||||||
|
if (Get-AppxPackage -Name $appId -AllUsers -ErrorAction SilentlyContinue) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
'DisableStoreSearchSuggestions' {
|
||||||
|
if ($script:Params.ContainsKey('Sysprep')) {
|
||||||
|
return (Test-StoreSearchSuggestionsDisabledForAllUsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
$storeDbPath = "$env:LocalAppData\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
|
||||||
|
if ($script:Params.ContainsKey('User')) {
|
||||||
|
$storeDbPath = GetUserDirectory -userName "$(GetUserName)" -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Test-StoreSearchSuggestionsDisabled -StoreAppsDatabase $storeDbPath)
|
||||||
|
}
|
||||||
|
'EnableWindowsSandbox' {
|
||||||
|
return (Test-WindowsOptionalFeatureEnabled -FeatureName 'Containers-DisposableClientVM')
|
||||||
|
}
|
||||||
|
'EnableWindowsSubsystemForLinux' {
|
||||||
|
$wslEnabled = Test-WindowsOptionalFeatureEnabled -FeatureName 'Microsoft-Windows-Subsystem-Linux'
|
||||||
|
$vmpEnabled = Test-WindowsOptionalFeatureEnabled -FeatureName 'VirtualMachinePlatform'
|
||||||
|
return ($wslEnabled -and $vmpEnabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $feature.RegistryKey) { return $false }
|
||||||
|
|
||||||
|
$regFilePath = Join-Path $script:RegfilesPath $feature.RegistryKey
|
||||||
|
if (-not (Test-Path $regFilePath)) { return $false }
|
||||||
|
|
||||||
|
try {
|
||||||
|
$operations = @(Get-RegFileOperations -regFilePath $regFilePath)
|
||||||
|
}
|
||||||
|
catch { return $false }
|
||||||
|
|
||||||
|
if ($operations.Count -eq 0) { return $false }
|
||||||
|
|
||||||
|
foreach ($op in $operations) {
|
||||||
|
$parts = Split-RegistryPath -path $op.KeyPath
|
||||||
|
if (-not $parts) { return $false }
|
||||||
|
|
||||||
|
$rootKey = Get-RegistryRootKey -hiveName $parts.Hive
|
||||||
|
if (-not $rootKey) { return $false }
|
||||||
|
|
||||||
|
$key = $null
|
||||||
|
try {
|
||||||
|
$key = $rootKey.OpenSubKey($parts.SubKey, $false)
|
||||||
|
|
||||||
|
switch ($op.OperationType) {
|
||||||
|
'DeleteKey' {
|
||||||
|
if ($null -ne $key) { return $false }
|
||||||
|
}
|
||||||
|
'DeleteValue' {
|
||||||
|
if ($null -ne $key) {
|
||||||
|
$names = @($key.GetValueNames())
|
||||||
|
if ($names -icontains $op.ValueName) { return $false }
|
||||||
|
}
|
||||||
|
# key missing = value also gone = operation matches
|
||||||
|
}
|
||||||
|
'SetValue' {
|
||||||
|
if ($null -eq $key) { return $false }
|
||||||
|
$names = @($key.GetValueNames())
|
||||||
|
if (-not ($names -icontains $op.ValueName)) { return $false }
|
||||||
|
|
||||||
|
$actualKind = $key.GetValueKind($op.ValueName)
|
||||||
|
$expectedKind = Get-ExpectedRegistryValueKind -Operation $op
|
||||||
|
if ($null -eq $expectedKind -or $actualKind -ne $expectedKind) { return $false }
|
||||||
|
$actualRaw = $key.GetValue($op.ValueName, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
|
||||||
|
|
||||||
|
$actual = switch ($actualKind) {
|
||||||
|
([Microsoft.Win32.RegistryValueKind]::DWord) {
|
||||||
|
[BitConverter]::ToUInt32([BitConverter]::GetBytes([int32]$actualRaw), 0)
|
||||||
|
}
|
||||||
|
([Microsoft.Win32.RegistryValueKind]::QWord) {
|
||||||
|
[BitConverter]::ToUInt64([BitConverter]::GetBytes([int64]$actualRaw), 0)
|
||||||
|
}
|
||||||
|
([Microsoft.Win32.RegistryValueKind]::Binary) {
|
||||||
|
@($actualRaw | ForEach-Object { [int]$_ })
|
||||||
|
}
|
||||||
|
([Microsoft.Win32.RegistryValueKind]::MultiString) {
|
||||||
|
@($actualRaw)
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
if ($null -ne $actualRaw) { [string]$actualRaw } else { $null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$expected = $op.ValueData
|
||||||
|
|
||||||
|
$match = if (($actual -is [array]) -and ($expected -is [array])) {
|
||||||
|
(Compare-Object $actual $expected).Count -eq 0
|
||||||
|
} else {
|
||||||
|
$actual -eq $expected
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $match) { return $false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { return $false }
|
||||||
|
finally {
|
||||||
|
if ($null -ne $key) { $key.Close() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns the 1-based index of the UiGroup option whose features all match current system state,
|
||||||
|
# or 0 if no option fully matches (meaning the current state is unknown / "No Change").
|
||||||
|
function Get-CurrentGroupActiveIndex {
|
||||||
|
param (
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[object]$Group
|
||||||
|
)
|
||||||
|
|
||||||
|
$i = 1
|
||||||
|
foreach ($val in $Group.Values) {
|
||||||
|
$allApplied = $true
|
||||||
|
foreach ($fid in $val.FeatureIds) {
|
||||||
|
if (-not (Test-FeatureApplied -FeatureId $fid)) {
|
||||||
|
$allApplied = $false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($allApplied) { return $i }
|
||||||
|
$i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -8,33 +8,33 @@ function ImportRegistryFile {
|
|||||||
Write-Host $message
|
Write-Host $message
|
||||||
|
|
||||||
$usesOfflineHive = $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User")
|
$usesOfflineHive = $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User")
|
||||||
$regFilePath = if ($usesOfflineHive) {
|
$regFilePath = Get-RegistryFilePathForFeature -RegistryKey $path
|
||||||
"$script:RegfilesPath\Sysprep\$path"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
"$script:RegfilesPath\$path"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $regFilePath)) {
|
if (-not (Test-Path $regFilePath)) {
|
||||||
$errorMessage = "Unable to find registry file: $path ($regFilePath)"
|
$errorMessage = "Unable to find registry file: $path ($regFilePath)"
|
||||||
|
$script:RegistryImportFailures++
|
||||||
Write-Host "Error: $errorMessage" -ForegroundColor Red
|
Write-Host "Error: $errorMessage" -ForegroundColor Red
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
throw $errorMessage
|
throw $errorMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
# Reset exit code before running reg.exe for reliable success detection
|
$importScript = {
|
||||||
$global:LASTEXITCODE = 0
|
param($targetRegFilePath, $hiveContext)
|
||||||
|
|
||||||
if ($usesOfflineHive) {
|
# When the target user's hive is already loaded under their SID, the .reg file's
|
||||||
# Sysprep targets Default user, User targets the specified user
|
# HKEY_USERS\Default paths won't match. Use the PowerShell registry writer instead,
|
||||||
$hiveDatPath = if ($script:Params.ContainsKey("Sysprep")) {
|
# which remaps Default → SID via Split-RegistryPath.
|
||||||
GetUserDirectory -userName "Default" -fileName "NTUSER.DAT"
|
$usePowerShellFallbackOnly = $hiveContext -and [bool]$hiveContext.WasAlreadyLoaded
|
||||||
} else {
|
|
||||||
GetUserDirectory -userName $script:Params.Item("User") -fileName "NTUSER.DAT"
|
if ($usePowerShellFallbackOnly) {
|
||||||
|
Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath
|
||||||
|
Write-Host "The operation completed successfully via PowerShell registry writer."
|
||||||
|
Write-Host ""
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
$regResult = Invoke-NonBlocking -ScriptBlock {
|
$regResult = Invoke-NonBlocking -ScriptBlock {
|
||||||
param($hivePath, $targetRegFilePath)
|
param($targetRegFilePath)
|
||||||
$result = @{
|
$result = @{
|
||||||
Output = @()
|
Output = @()
|
||||||
ExitCode = 0
|
ExitCode = 0
|
||||||
@@ -43,13 +43,6 @@ function ImportRegistryFile {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$global:LASTEXITCODE = 0
|
$global:LASTEXITCODE = 0
|
||||||
reg load "HKU\Default" $hivePath | Out-Null
|
|
||||||
$loadExitCode = $LASTEXITCODE
|
|
||||||
|
|
||||||
if ($loadExitCode -ne 0) {
|
|
||||||
throw "Failed to load user hive at '$hivePath' (exit code: $loadExitCode)"
|
|
||||||
}
|
|
||||||
|
|
||||||
$output = reg import $targetRegFilePath 2>&1
|
$output = reg import $targetRegFilePath 2>&1
|
||||||
$importExitCode = $LASTEXITCODE
|
$importExitCode = $LASTEXITCODE
|
||||||
|
|
||||||
@@ -66,27 +59,9 @@ function ImportRegistryFile {
|
|||||||
$result.Error = $_.Exception.Message
|
$result.Error = $_.Exception.Message
|
||||||
$result.ExitCode = if ($LASTEXITCODE -ne 0) { $LASTEXITCODE } else { 1 }
|
$result.ExitCode = if ($LASTEXITCODE -ne 0) { $LASTEXITCODE } else { 1 }
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
$global:LASTEXITCODE = 0
|
|
||||||
reg unload "HKU\Default" | Out-Null
|
|
||||||
$unloadExitCode = $LASTEXITCODE
|
|
||||||
if ($unloadExitCode -ne 0 -and -not $result.Error) {
|
|
||||||
$result.Error = "Failed to unload registry hive HKU\Default (exit code: $unloadExitCode)"
|
|
||||||
$result.ExitCode = $unloadExitCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result
|
return $result
|
||||||
} -ArgumentList @($hiveDatPath, $regFilePath)
|
} -ArgumentList $targetRegFilePath
|
||||||
}
|
|
||||||
else {
|
|
||||||
$regResult = Invoke-NonBlocking -ScriptBlock {
|
|
||||||
param($targetRegFilePath)
|
|
||||||
$global:LASTEXITCODE = 0
|
|
||||||
$output = reg import $targetRegFilePath 2>&1
|
|
||||||
return @{ Output = @($output); ExitCode = $LASTEXITCODE; Error = $null }
|
|
||||||
} -ArgumentList $regFilePath
|
|
||||||
}
|
|
||||||
|
|
||||||
$regOutput = @($regResult.Output)
|
$regOutput = @($regResult.Output)
|
||||||
$hasSuccess = ($regResult.ExitCode -eq 0) -and -not $regResult.Error
|
$hasSuccess = ($regResult.ExitCode -eq 0) -and -not $regResult.Error
|
||||||
@@ -107,11 +82,27 @@ function ImportRegistryFile {
|
|||||||
|
|
||||||
if (-not $hasSuccess) {
|
if (-not $hasSuccess) {
|
||||||
$details = if ($regResult.Error) { $regResult.Error } else { "Exit code: $($regResult.ExitCode)" }
|
$details = if ($regResult.Error) { $regResult.Error } else { "Exit code: $($regResult.ExitCode)" }
|
||||||
$errorMessage = "Failed importing registry file '$path'. $details"
|
Write-Warning "reg import failed for '$path'. Falling back to PowerShell registry writer. Details: $details"
|
||||||
Write-Host $errorMessage -ForegroundColor Red
|
Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath
|
||||||
Write-Host ""
|
Write-Host "The operation completed successfully via PowerShell registry writer."
|
||||||
throw $errorMessage
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($usesOfflineHive) {
|
||||||
|
# Sysprep targets Default user, User targets the specified user. Logged-in users already have their hive mounted under HKU\<SID>.
|
||||||
|
$targetUserName = if ($script:Params.ContainsKey("Sysprep")) { "Default" } else { $script:Params.Item("User") }
|
||||||
|
Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $importScript -ArgumentObject $regFilePath -PassHiveContext
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
& $importScript $regFilePath $null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$script:RegistryImportFailures++
|
||||||
|
Write-Host $_.Exception.Message -ForegroundColor Red
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -33,12 +33,49 @@ function Get-NormalizedSelectedFeatureIdsFromBackup {
|
|||||||
$errors.Add('SelectedFeatures must contain non-empty string feature IDs.')
|
$errors.Add('SelectedFeatures must contain non-empty string feature IDs.')
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($selectedFeatures.Count -eq 0) {
|
return [PSCustomObject]@{
|
||||||
$errors.Add('SelectedFeatures must contain at least one feature ID.')
|
SelectedFeatures = $selectedFeatures.ToArray()
|
||||||
|
Errors = $errors.ToArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-NormalizedSelectedUndoFeatureIdsFromBackup {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Backup
|
||||||
|
)
|
||||||
|
|
||||||
|
$selectedUndoFeatures = New-Object System.Collections.Generic.List[string]
|
||||||
|
$selectedUndoFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
$errors = New-Object System.Collections.Generic.List[string]
|
||||||
|
|
||||||
|
# SelectedUndoFeatures is optional - only process if present
|
||||||
|
if (-not $Backup.PSObject.Properties['SelectedUndoFeatures']) {
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
SelectedUndoFeatures = $selectedUndoFeatures.ToArray()
|
||||||
|
Errors = $errors.ToArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasInvalidSelectedUndoFeatureId = $false
|
||||||
|
foreach ($featureId in @($Backup.SelectedUndoFeatures)) {
|
||||||
|
if ($featureId -isnot [string] -or [string]::IsNullOrWhiteSpace([string]$featureId)) {
|
||||||
|
$hasInvalidSelectedUndoFeatureId = $true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedFeatureId = [string]$featureId
|
||||||
|
if ($selectedUndoFeatureIds.Add($normalizedFeatureId)) {
|
||||||
|
$selectedUndoFeatures.Add($normalizedFeatureId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasInvalidSelectedUndoFeatureId) {
|
||||||
|
$errors.Add('SelectedUndoFeatures must contain non-empty string feature IDs.')
|
||||||
}
|
}
|
||||||
|
|
||||||
return [PSCustomObject]@{
|
return [PSCustomObject]@{
|
||||||
SelectedFeatures = $selectedFeatures.ToArray()
|
SelectedUndoFeatures = $selectedUndoFeatures.ToArray()
|
||||||
Errors = $errors.ToArray()
|
Errors = $errors.ToArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,6 +133,9 @@ function Test-RegistryBackupMatchesSelectedFeatures {
|
|||||||
[AllowEmptyCollection()]
|
[AllowEmptyCollection()]
|
||||||
[string[]]$SelectedFeatureIds,
|
[string[]]$SelectedFeatureIds,
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
|
[AllowEmptyCollection()]
|
||||||
|
[string[]]$SelectedUndoFeatureIds,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
[string]$Target,
|
[string]$Target,
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
[AllowEmptyCollection()]
|
[AllowEmptyCollection()]
|
||||||
@@ -109,18 +149,19 @@ function Test-RegistryBackupMatchesSelectedFeatures {
|
|||||||
return $errors.ToArray()
|
return $errors.ToArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
$selectedRegistryFeatures = @(Get-SelectedRegistryFeaturesForBackupValidation -SelectedFeatureIds @($SelectedFeatureIds) -Errors $errors)
|
$selectedRegistryFeatures = @(Get-SelectedRegistryFeaturesForBackupValidation -SelectedFeatureIds @($SelectedFeatureIds) -IsUndoFeature:$false -Errors $errors)
|
||||||
|
$undoRegistryFeatures = @(Get-SelectedRegistryFeaturesForBackupValidation -SelectedFeatureIds @($SelectedUndoFeatureIds) -IsUndoFeature:$true -Errors $errors)
|
||||||
$useSysprepRegFiles = ($Target -eq 'DefaultUserProfile') -or ($Target -like 'User:*')
|
$useSysprepRegFiles = ($Target -eq 'DefaultUserProfile') -or ($Target -like 'User:*')
|
||||||
|
|
||||||
$capturePlans = @()
|
$capturePlans = @()
|
||||||
if ($errors.Count -eq 0 -and $selectedRegistryFeatures.Count -gt 0) {
|
if ($errors.Count -eq 0 -and ($selectedRegistryFeatures.Count -gt 0 -or $undoRegistryFeatures.Count -gt 0)) {
|
||||||
$capturePlans = @(Get-RegistryBackupCapturePlans -SelectedRegistryFeatures @($selectedRegistryFeatures) -UseSysprepRegFiles:$useSysprepRegFiles)
|
$capturePlans = @(Get-RegistryBackupCapturePlans -SelectedRegistryFeatures @($selectedRegistryFeatures) -UndoRegistryFeatures @($undoRegistryFeatures) -UseSysprepRegFiles:$useSysprepRegFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
$planMap = New-RegistryBackupAllowListPlanMap -CapturePlans @($capturePlans)
|
$planMap = New-RegistryBackupAllowListPlanMap -CapturePlans @($capturePlans)
|
||||||
|
|
||||||
if ($planMap.Count -eq 0 -and @($RegistryKeys).Count -gt 0) {
|
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.')
|
$errors.Add('Backup contains registry snapshots but no allowed registry paths were derived from the selected features.')
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($rootSnapshot in @($RegistryKeys)) {
|
foreach ($rootSnapshot in @($RegistryKeys)) {
|
||||||
@@ -136,6 +177,8 @@ function Get-SelectedRegistryFeaturesForBackupValidation {
|
|||||||
[AllowEmptyCollection()]
|
[AllowEmptyCollection()]
|
||||||
[string[]]$SelectedFeatureIds,
|
[string[]]$SelectedFeatureIds,
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
|
[bool]$IsUndoFeature,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
[AllowEmptyCollection()]
|
[AllowEmptyCollection()]
|
||||||
$Errors
|
$Errors
|
||||||
)
|
)
|
||||||
@@ -152,7 +195,26 @@ function Get-SelectedRegistryFeaturesForBackupValidation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$feature = $script:Features[$featureId]
|
$feature = $script:Features[$featureId]
|
||||||
if ($feature -and -not [string]::IsNullOrWhiteSpace([string]$feature.RegistryKey)) {
|
if (-not $feature) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# For undo features, check RegistryUndoKey if present (real features)
|
||||||
|
# Otherwise check RegistryKey (for synthetic features from backup capture)
|
||||||
|
$registryKeyToUse = if ($IsUndoFeature) {
|
||||||
|
$key = [string]$feature.RegistryUndoKey
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($key)) {
|
||||||
|
$key
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[string]$feature.RegistryKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[string]$feature.RegistryKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($registryKeyToUse)) {
|
||||||
$selectedRegistryFeatures.Add($feature)
|
$selectedRegistryFeatures.Add($feature)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function ReplaceStartMenuForAllUsers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Replace the startmenu for all users, when using the default startmenuTemplate this clears all pinned apps
|
# Replace the startmenu at the specified location, when using the default startmenuTemplate this clears all pinned apps
|
||||||
# Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/
|
# Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/
|
||||||
function ReplaceStartMenu {
|
function ReplaceStartMenu {
|
||||||
param (
|
param (
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
# Restart the Windows Explorer process
|
# Restart the Windows Explorer process
|
||||||
function RestartExplorer {
|
function RestartExplorer {
|
||||||
|
# Restarting Explorer while running in Sysprep or User context is not necessary
|
||||||
|
if ($script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host "> Attempting to restart the Windows Explorer process to apply all changes..."
|
Write-Host "> Attempting to restart the Windows Explorer process to apply all changes..."
|
||||||
|
|
||||||
if ($script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User") -or $script:Params.ContainsKey("NoRestartExplorer")) {
|
if ($script:Params.ContainsKey("NoRestartExplorer")) {
|
||||||
Write-Host "Explorer process restart was skipped, please manually reboot your PC to apply all changes" -ForegroundColor Yellow
|
Write-Host "Explorer process restart was skipped, please manually reboot your PC to apply all changes" -ForegroundColor Yellow
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,38 +7,21 @@ function Invoke-WithLoadedRestoreHive {
|
|||||||
$ArgumentObject = $null
|
$ArgumentObject = $null
|
||||||
)
|
)
|
||||||
|
|
||||||
$hiveDatPath = if ($Target -eq 'DefaultUserProfile') {
|
$targetUserName = if ($Target -eq 'DefaultUserProfile') {
|
||||||
GetUserDirectory -userName 'Default' -fileName 'NTUSER.DAT'
|
'Default'
|
||||||
}
|
}
|
||||||
elseif ($Target -like 'User:*') {
|
elseif ($Target -like 'User:*') {
|
||||||
$userName = $Target.Substring(5)
|
$userName = $Target.Substring(5)
|
||||||
if ([string]::IsNullOrWhiteSpace($userName)) {
|
if ([string]::IsNullOrWhiteSpace($userName)) {
|
||||||
throw 'Invalid backup target format for user restore.'
|
throw 'Invalid backup target format for user restore.'
|
||||||
}
|
}
|
||||||
GetUserDirectory -userName $userName -fileName 'NTUSER.DAT'
|
$userName
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw "Unsupported backup target '$Target'."
|
throw "Unsupported backup target '$Target'."
|
||||||
}
|
}
|
||||||
|
|
||||||
$global:LASTEXITCODE = 0
|
Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $ScriptBlock -ArgumentObject $ArgumentObject
|
||||||
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 {
|
function Restore-RegistryKeySnapshot {
|
||||||
|
|||||||
@@ -87,7 +87,17 @@ function Normalize-RegistryBackup {
|
|||||||
$errors.Add([string]$selectedFeatureParseError)
|
$errors.Add([string]$selectedFeatureParseError)
|
||||||
}
|
}
|
||||||
|
|
||||||
$allowListValidationErrors = @(Test-RegistryBackupMatchesSelectedFeatures -SelectedFeatureIds @($selectedFeatures) -Target $normalizedTarget -RegistryKeys @($normalizedKeys))
|
$selectedUndoFeatureParseResult = Get-NormalizedSelectedUndoFeatureIdsFromBackup -Backup $Backup
|
||||||
|
$selectedUndoFeatures = @($selectedUndoFeatureParseResult.SelectedUndoFeatures)
|
||||||
|
foreach ($selectedUndoFeatureParseError in @($selectedUndoFeatureParseResult.Errors)) {
|
||||||
|
$errors.Add([string]$selectedUndoFeatureParseError)
|
||||||
|
}
|
||||||
|
|
||||||
|
$allSelectedFeatures = @($selectedFeatures) + @($selectedUndoFeatures)
|
||||||
|
if ($allSelectedFeatures.Count -eq 0) {
|
||||||
|
$errors.Add('Backup must contain at least one feature ID in SelectedFeatures or SelectedUndoFeatures.')
|
||||||
|
}
|
||||||
|
$allowListValidationErrors = @(Test-RegistryBackupMatchesSelectedFeatures -SelectedFeatureIds @($selectedFeatures) -SelectedUndoFeatureIds @($selectedUndoFeatures) -Target $normalizedTarget -RegistryKeys @($normalizedKeys))
|
||||||
foreach ($allowListValidationError in $allowListValidationErrors) {
|
foreach ($allowListValidationError in $allowListValidationErrors) {
|
||||||
$errors.Add([string]$allowListValidationError)
|
$errors.Add([string]$allowListValidationError)
|
||||||
}
|
}
|
||||||
@@ -110,6 +120,7 @@ function Normalize-RegistryBackup {
|
|||||||
ComputerName = [string]$Backup.ComputerName
|
ComputerName = [string]$Backup.ComputerName
|
||||||
Target = $normalizedTarget
|
Target = $normalizedTarget
|
||||||
SelectedFeatures = @($selectedFeatures)
|
SelectedFeatures = @($selectedFeatures)
|
||||||
|
SelectedUndoFeatures = @($selectedUndoFeatures)
|
||||||
RegistryKeys = @($normalizedKeys)
|
RegistryKeys = @($normalizedKeys)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
49
Scripts/Features/WindowsOptionalFeatures.ps1
Normal file
49
Scripts/Features/WindowsOptionalFeatures.ps1
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Enables a Windows optional feature and pipes its output to the console
|
||||||
|
function EnableWindowsFeature {
|
||||||
|
param (
|
||||||
|
[string]$FeatureName
|
||||||
|
)
|
||||||
|
|
||||||
|
$result = Invoke-NonBlocking -ScriptBlock {
|
||||||
|
param($name)
|
||||||
|
Enable-WindowsOptionalFeature -Online -FeatureName $name -All -NoRestart
|
||||||
|
} -ArgumentList $FeatureName
|
||||||
|
|
||||||
|
$dismResult = @($result) | Where-Object { $_ -is [Microsoft.Dism.Commands.ImageObject] }
|
||||||
|
if ($dismResult) {
|
||||||
|
Write-Host ($dismResult | Out-String).Trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Disables a Windows optional feature and pipes its output to the console
|
||||||
|
function DisableWindowsFeature {
|
||||||
|
param (
|
||||||
|
[string]$FeatureName
|
||||||
|
)
|
||||||
|
|
||||||
|
$result = Invoke-NonBlocking -ScriptBlock {
|
||||||
|
param($name)
|
||||||
|
Disable-WindowsOptionalFeature -Online -FeatureName $name -NoRestart
|
||||||
|
} -ArgumentList $FeatureName
|
||||||
|
|
||||||
|
$dismResult = @($result) | Where-Object { $_ -is [Microsoft.Dism.Commands.ImageObject] }
|
||||||
|
if ($dismResult) {
|
||||||
|
Write-Host ($dismResult | Out-String).Trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-WindowsOptionalFeatureEnabled {
|
||||||
|
param (
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$FeatureName
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
$feature = Get-WindowsOptionalFeature -Online -FeatureName $FeatureName -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($feature.State -eq 'Enabled')
|
||||||
|
}
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
# Checks if the system is set to use dark mode for apps
|
# Checks if the system is set to use dark mode for apps
|
||||||
function GetSystemUsesDarkMode {
|
function GetSystemUsesDarkMode {
|
||||||
try {
|
try {
|
||||||
return (Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize' -Name 'AppsUseLightTheme').AppsUseLightTheme -eq 0
|
$personalizeKey = Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize'
|
||||||
|
|
||||||
|
if ($null -eq $personalizeKey) {
|
||||||
|
Write-Host "WARNING: Unable to retrieve personalization settings." -ForegroundColor Yellow
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
return $personalizeKey.AppsUseLightTheme -eq 0
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
return $false
|
return $false
|
||||||
|
|||||||
540
Scripts/GUI/MainWindow-AppSelection.ps1
Normal file
540
Scripts/GUI/MainWindow-AppSelection.ps1
Normal file
@@ -0,0 +1,540 @@
|
|||||||
|
# MainWindow-AppSelection.ps1
|
||||||
|
# App-selection panel functions: tri-state helpers, sorting, search/highlight, app loading, preset management, and removal scope.
|
||||||
|
|
||||||
|
function Add-TriStateClickBehavior {
|
||||||
|
param([System.Windows.Controls.CheckBox]$CheckBox)
|
||||||
|
|
||||||
|
if (-not $CheckBox -or -not $CheckBox.IsThreeState) { return }
|
||||||
|
|
||||||
|
if (-not $CheckBox.PSObject.Properties['WasIndeterminateBeforeClick']) {
|
||||||
|
Add-Member -InputObject $CheckBox -MemberType NoteProperty -Name 'WasIndeterminateBeforeClick' -Value $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$CheckBox.Add_PreviewMouseLeftButtonDown({
|
||||||
|
$this.WasIndeterminateBeforeClick = ($this.IsChecked -eq [System.Nullable[bool]]$null)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-NormalizedCheckboxState {
|
||||||
|
param([System.Windows.Controls.CheckBox]$CheckBox)
|
||||||
|
|
||||||
|
if ($CheckBox.PSObject.Properties['WasIndeterminateBeforeClick'] -and $CheckBox.WasIndeterminateBeforeClick) {
|
||||||
|
# WPF toggles null -> false before Click handlers fire; restore desired mixed -> checked behavior.
|
||||||
|
$CheckBox.WasIndeterminateBeforeClick = $false
|
||||||
|
$CheckBox.IsChecked = $true
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($CheckBox.IsChecked -eq $true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-TriStatePresetCheckBoxState {
|
||||||
|
param(
|
||||||
|
[System.Windows.Controls.CheckBox]$CheckBox,
|
||||||
|
[int]$Total,
|
||||||
|
[int]$Selected
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $CheckBox) { return }
|
||||||
|
|
||||||
|
if ($Total -eq 0) {
|
||||||
|
$CheckBox.IsEnabled = $false
|
||||||
|
$CheckBox.IsChecked = $false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$CheckBox.IsEnabled = $true
|
||||||
|
if ($Selected -eq 0) {
|
||||||
|
$CheckBox.IsChecked = $false
|
||||||
|
}
|
||||||
|
elseif ($Selected -eq $Total) {
|
||||||
|
$CheckBox.IsChecked = $true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$CheckBox.IsChecked = [System.Nullable[bool]]$null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-SortArrows {
|
||||||
|
param(
|
||||||
|
[System.Windows.Controls.TextBlock]$SortArrowName,
|
||||||
|
[System.Windows.Controls.TextBlock]$SortArrowDescription,
|
||||||
|
[System.Windows.Controls.TextBlock]$SortArrowAppId
|
||||||
|
)
|
||||||
|
|
||||||
|
$ease = New-Object System.Windows.Media.Animation.CubicEase
|
||||||
|
$ease.EasingMode = 'EaseOut'
|
||||||
|
$arrows = @{
|
||||||
|
'Name' = $SortArrowName
|
||||||
|
'Description' = $SortArrowDescription
|
||||||
|
'AppId' = $SortArrowAppId
|
||||||
|
}
|
||||||
|
foreach ($col in $arrows.Keys) {
|
||||||
|
$tb = $arrows[$col]
|
||||||
|
# Active column: full opacity, rotate to indicate direction (0 = up/asc, 180 = down/desc)
|
||||||
|
# Inactive columns: dim, reset to 0
|
||||||
|
if ($col -eq $script:SortColumn) {
|
||||||
|
$targetAngle = if ($script:SortAscending) { 0 } else { 180 }
|
||||||
|
$tb.Opacity = 1.0
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$targetAngle = 0
|
||||||
|
$tb.Opacity = 0.3
|
||||||
|
}
|
||||||
|
$anim = New-Object System.Windows.Media.Animation.DoubleAnimation
|
||||||
|
$anim.To = $targetAngle
|
||||||
|
$anim.Duration = [System.Windows.Duration]::new([System.TimeSpan]::FromMilliseconds(200))
|
||||||
|
$anim.EasingFunction = $ease
|
||||||
|
$tb.RenderTransform.BeginAnimation([System.Windows.Media.RotateTransform]::AngleProperty, $anim)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-AppsPanelRebuildSearchIndex {
|
||||||
|
param(
|
||||||
|
[System.Windows.Controls.Panel]$AppsPanel,
|
||||||
|
$ActiveMatch = $null
|
||||||
|
)
|
||||||
|
|
||||||
|
$newMatches = @()
|
||||||
|
$newActiveIndex = -1
|
||||||
|
$i = 0
|
||||||
|
foreach ($child in $AppsPanel.Children) {
|
||||||
|
if ($child -is [System.Windows.Controls.CheckBox] -and $child.Background -ne [System.Windows.Media.Brushes]::Transparent) {
|
||||||
|
$newMatches += $child
|
||||||
|
if ($null -ne $ActiveMatch -and [System.Object]::ReferenceEquals($child, $ActiveMatch)) {
|
||||||
|
$newActiveIndex = $i
|
||||||
|
}
|
||||||
|
$i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$script:AppSearchMatches = $newMatches
|
||||||
|
$script:AppSearchMatchIndex = if ($newActiveIndex -ge 0) { $newActiveIndex } elseif ($newMatches.Count -gt 0) { 0 } else { -1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-AppsPanelSort {
|
||||||
|
param(
|
||||||
|
[System.Windows.Controls.Panel]$AppsPanel,
|
||||||
|
[System.Windows.Controls.TextBlock]$SortArrowName,
|
||||||
|
[System.Windows.Controls.TextBlock]$SortArrowDescription,
|
||||||
|
[System.Windows.Controls.TextBlock]$SortArrowAppId
|
||||||
|
)
|
||||||
|
|
||||||
|
$children = @($AppsPanel.Children)
|
||||||
|
$key = switch ($script:SortColumn) {
|
||||||
|
'Name' { { $_.AppName } }
|
||||||
|
'Description' { { $_.AppDescription } }
|
||||||
|
'AppId' { { $_.AppIdDisplay } }
|
||||||
|
}
|
||||||
|
$sorted = $children | Sort-Object $key -Descending:(-not $script:SortAscending)
|
||||||
|
$AppsPanel.Children.Clear()
|
||||||
|
foreach ($checkbox in $sorted) {
|
||||||
|
$AppsPanel.Children.Add($checkbox) | Out-Null
|
||||||
|
}
|
||||||
|
Update-SortArrows -SortArrowName $SortArrowName -SortArrowDescription $SortArrowDescription -SortArrowAppId $SortArrowAppId
|
||||||
|
|
||||||
|
# Rebuild search match list in new sorted order so keyboard navigation stays correct
|
||||||
|
if ($script:AppSearchMatches.Count -gt 0) {
|
||||||
|
$activeMatch = if ($script:AppSearchMatchIndex -ge 0 -and $script:AppSearchMatchIndex -lt $script:AppSearchMatches.Count) {
|
||||||
|
$script:AppSearchMatches[$script:AppSearchMatchIndex]
|
||||||
|
}
|
||||||
|
else { $null }
|
||||||
|
Update-AppsPanelRebuildSearchIndex -AppsPanel $AppsPanel -ActiveMatch $activeMatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-AppSelectionStatus {
|
||||||
|
param(
|
||||||
|
[System.Windows.Controls.Panel]$AppsPanel,
|
||||||
|
[System.Windows.Controls.TextBlock]$AppSelectionStatus,
|
||||||
|
[System.Windows.Controls.ComboBox]$AppRemovalScopeCombo,
|
||||||
|
[System.Windows.Controls.Border]$AppRemovalScopeSection,
|
||||||
|
[System.Windows.Controls.TextBlock]$AppRemovalScopeDescription,
|
||||||
|
[System.Windows.Controls.ComboBox]$UserSelectionCombo
|
||||||
|
)
|
||||||
|
|
||||||
|
$selectedCount = 0
|
||||||
|
foreach ($child in $AppsPanel.Children) {
|
||||||
|
if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) {
|
||||||
|
$selectedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$AppSelectionStatus.Text = "$selectedCount app(s) selected for removal"
|
||||||
|
|
||||||
|
if ($AppRemovalScopeCombo -and $AppRemovalScopeSection -and $AppRemovalScopeDescription) {
|
||||||
|
if ($selectedCount -gt 0) {
|
||||||
|
$AppRemovalScopeSection.Visibility = 'Visible'
|
||||||
|
if ($UserSelectionCombo.SelectedIndex -ne 2) {
|
||||||
|
$AppRemovalScopeCombo.IsEnabled = $true
|
||||||
|
}
|
||||||
|
Update-AppRemovalScopeDescription -AppRemovalScopeCombo $AppRemovalScopeCombo -AppRemovalScopeDescription $AppRemovalScopeDescription
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$AppRemovalScopeSection.Visibility = 'Collapsed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-AppRemovalScopeDescription {
|
||||||
|
param(
|
||||||
|
[System.Windows.Controls.ComboBox]$AppRemovalScopeCombo,
|
||||||
|
[System.Windows.Controls.TextBlock]$AppRemovalScopeDescription
|
||||||
|
)
|
||||||
|
|
||||||
|
$selectedItem = $AppRemovalScopeCombo.SelectedItem
|
||||||
|
if ($selectedItem) {
|
||||||
|
switch ($selectedItem.Content) {
|
||||||
|
"All users" {
|
||||||
|
$AppRemovalScopeDescription.Text = "Apps will be removed for all users and from the Windows image to prevent reinstallation for new users."
|
||||||
|
}
|
||||||
|
"Current user only" {
|
||||||
|
$AppRemovalScopeDescription.Text = "Apps will only be removed for the current user. Existing and new users will not be affected."
|
||||||
|
}
|
||||||
|
"Target user only" {
|
||||||
|
$AppRemovalScopeDescription.Text = "Apps will only be removed for the specified target user. Existing and new users will not be affected."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-AppPreset {
|
||||||
|
param(
|
||||||
|
[System.Windows.Controls.Panel]$AppsPanel,
|
||||||
|
[scriptblock]$MatchFilter,
|
||||||
|
[bool]$Check,
|
||||||
|
[switch]$Exclusive
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($child in $AppsPanel.Children) {
|
||||||
|
if ($child -is [System.Windows.Controls.CheckBox]) {
|
||||||
|
if ($Exclusive) {
|
||||||
|
$child.IsChecked = (& $MatchFilter $child)
|
||||||
|
}
|
||||||
|
elseif (& $MatchFilter $child) {
|
||||||
|
$child.IsChecked = $Check
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Update-AppPresetStates -AppsPanel $AppsPanel
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-AppPresetStates {
|
||||||
|
param([System.Windows.Controls.Panel]$AppsPanel)
|
||||||
|
|
||||||
|
$script:UpdatingPresets = $true
|
||||||
|
try {
|
||||||
|
# Helper: count matching and checked apps, set checkbox state
|
||||||
|
function SetPresetState($CheckBox, [scriptblock]$MatchFilter) {
|
||||||
|
$total = 0; $checked = 0
|
||||||
|
foreach ($child in $AppsPanel.Children) {
|
||||||
|
if ($child -is [System.Windows.Controls.CheckBox]) {
|
||||||
|
if (& $MatchFilter $child) {
|
||||||
|
$total++
|
||||||
|
if ($child.IsChecked) { $checked++ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Set-TriStatePresetCheckBoxState -CheckBox $CheckBox -Total $total -Selected $checked
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find preset checkboxes via window
|
||||||
|
$window = $script:MainWindow
|
||||||
|
$presetDefaultApps = $window.FindName('PresetDefaultApps')
|
||||||
|
$presetLastUsed = $window.FindName('PresetLastUsed')
|
||||||
|
|
||||||
|
SetPresetState $presetDefaultApps { param($c) $c.SelectedByDefault -eq $true }
|
||||||
|
foreach ($jsonCb in $script:JsonPresetCheckboxes) {
|
||||||
|
$localIds = $jsonCb.PresetAppIds
|
||||||
|
SetPresetState $jsonCb { param($c) (@($c.AppIds) | Where-Object { $localIds -contains $_ }).Count -gt 0 }.GetNewClosure()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Last used preset: only update if it's visible (has saved apps)
|
||||||
|
if ($presetLastUsed.Visibility -ne 'Collapsed' -and $script:SavedAppIds) {
|
||||||
|
SetPresetState $presetLastUsed { param($c) (@($c.AppIds) | Where-Object { $script:SavedAppIds -contains $_ }).Count -gt 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$script:UpdatingPresets = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Scroll-ToItemIfNotVisible {
|
||||||
|
param (
|
||||||
|
[System.Windows.Controls.ScrollViewer]$ScrollViewer,
|
||||||
|
[System.Windows.UIElement]$Item,
|
||||||
|
[System.Windows.UIElement]$Container
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $ScrollViewer -or -not $Item -or -not $Container) { return }
|
||||||
|
|
||||||
|
try {
|
||||||
|
$itemPosition = $Item.TransformToAncestor($Container).Transform([System.Windows.Point]::new(0, 0)).Y
|
||||||
|
$viewportHeight = $ScrollViewer.ViewportHeight
|
||||||
|
$itemHeight = $Item.ActualHeight
|
||||||
|
$currentOffset = $ScrollViewer.VerticalOffset
|
||||||
|
|
||||||
|
# Check if the item is currently visible in the viewport
|
||||||
|
$itemTop = $itemPosition - $currentOffset
|
||||||
|
$itemBottom = $itemTop + $itemHeight
|
||||||
|
|
||||||
|
$isVisible = ($itemTop -ge 0) -and ($itemBottom -le $viewportHeight)
|
||||||
|
|
||||||
|
# Only scroll if the item is not visible
|
||||||
|
if (-not $isVisible) {
|
||||||
|
# Center the item in the viewport
|
||||||
|
$targetOffset = $itemPosition - ($viewportHeight / 2) + ($itemHeight / 2)
|
||||||
|
$ScrollViewer.ScrollToVerticalOffset([Math]::Max(0, $targetOffset))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# Fallback to simple bring into view
|
||||||
|
$Item.BringIntoView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Find-ParentScrollViewer {
|
||||||
|
param ([System.Windows.UIElement]$Element)
|
||||||
|
|
||||||
|
$parent = [System.Windows.Media.VisualTreeHelper]::GetParent($Element)
|
||||||
|
while ($null -ne $parent) {
|
||||||
|
if ($parent -is [System.Windows.Controls.ScrollViewer]) {
|
||||||
|
return $parent
|
||||||
|
}
|
||||||
|
$parent = [System.Windows.Media.VisualTreeHelper]::GetParent($parent)
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Load-AppsWithList {
|
||||||
|
param(
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
[System.Windows.Controls.Panel]$AppsPanel,
|
||||||
|
[System.Windows.Controls.CheckBox]$OnlyInstalledAppsBox,
|
||||||
|
[System.Windows.Controls.Border]$LoadingAppsIndicator,
|
||||||
|
[System.Windows.Controls.MenuItem]$ImportConfigBtn,
|
||||||
|
[string]$ListOfApps
|
||||||
|
)
|
||||||
|
|
||||||
|
$script:MainWindowLastSelectedCheckbox = $null
|
||||||
|
|
||||||
|
$loaderScriptPath = $script:LoadAppsDetailsScriptPath
|
||||||
|
$appsFilePath = $script:AppsListFilePath
|
||||||
|
$onlyInstalled = [bool]$OnlyInstalledAppsBox.IsChecked
|
||||||
|
|
||||||
|
# Use preloaded data if available; otherwise load in background job
|
||||||
|
if (-not $onlyInstalled -and $script:PreloadedAppData) {
|
||||||
|
$rawAppData = $script:PreloadedAppData
|
||||||
|
$script:PreloadedAppData = $null
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# Load apps details in a background job to keep the UI responsive
|
||||||
|
$rawAppData = Invoke-NonBlocking -ScriptBlock {
|
||||||
|
param($loaderScript, $appsListFilePath, $installedList, $onlyInstalled)
|
||||||
|
$script:AppsListFilePath = $appsListFilePath
|
||||||
|
. $loaderScript
|
||||||
|
LoadAppsDetailsFromJson -OnlyInstalled:$onlyInstalled -InstalledList $installedList -InitialCheckedFromJson:$false
|
||||||
|
} -ArgumentList $loaderScriptPath, $appsFilePath, $ListOfApps, $onlyInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
$appsToAdd = @($rawAppData | Where-Object { $_ -and ($_.AppId -or $_.FriendlyName) } | Sort-Object -Property FriendlyName)
|
||||||
|
|
||||||
|
$LoadingAppsIndicator.Visibility = 'Collapsed'
|
||||||
|
|
||||||
|
if ($appsToAdd.Count -eq 0) {
|
||||||
|
$OnlyInstalledAppsBox.IsHitTestVisible = $true
|
||||||
|
$Window.FindName('DeploymentApplyBtn').IsEnabled = $true
|
||||||
|
if ($ImportConfigBtn) {
|
||||||
|
$ImportConfigBtn.IsEnabled = $true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$brushSafe = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#4CAF50')
|
||||||
|
$brushUnsafe = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#F44336')
|
||||||
|
$brushDefault = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#FFC107')
|
||||||
|
$brushSafe.Freeze(); $brushUnsafe.Freeze(); $brushDefault.Freeze()
|
||||||
|
|
||||||
|
# Create WPF controls; pump the Dispatcher every batch so the spinner keeps animating.
|
||||||
|
$batchSize = 20
|
||||||
|
for ($i = 0; $i -lt $appsToAdd.Count; $i++) {
|
||||||
|
$app = $appsToAdd[$i]
|
||||||
|
|
||||||
|
$checkbox = New-Object System.Windows.Controls.CheckBox
|
||||||
|
$automationName = if ($app.FriendlyName) { $app.FriendlyName } elseif ($app.AppIdDisplay) { $app.AppIdDisplay } else { $null }
|
||||||
|
if ($automationName) { $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $automationName) }
|
||||||
|
$checkbox.Tag = $app.AppIdDisplay
|
||||||
|
$checkbox.IsChecked = $app.IsChecked
|
||||||
|
$checkbox.Style = $Window.Resources['AppsPanelCheckBoxStyle']
|
||||||
|
|
||||||
|
# Build table row: Recommendation dot | Name | Description | App ID
|
||||||
|
$row = New-Object System.Windows.Controls.Grid
|
||||||
|
$row.Style = $Window.Resources['AppTableRowStyle']
|
||||||
|
$c0 = New-Object System.Windows.Controls.ColumnDefinition; $c0.Width = $Window.Resources['AppTableDotColWidth']
|
||||||
|
$c1 = New-Object System.Windows.Controls.ColumnDefinition; $c1.Width = $Window.Resources['AppTableNameColWidth']
|
||||||
|
$c2 = New-Object System.Windows.Controls.ColumnDefinition; $c2.Width = $Window.Resources['AppTableDescColWidth']
|
||||||
|
$c3 = New-Object System.Windows.Controls.ColumnDefinition; $c3.Width = $Window.Resources['AppTableIdColWidth']
|
||||||
|
$row.ColumnDefinitions.Add($c0); $row.ColumnDefinitions.Add($c1)
|
||||||
|
$row.ColumnDefinitions.Add($c2); $row.ColumnDefinitions.Add($c3)
|
||||||
|
|
||||||
|
$dot = New-Object System.Windows.Shapes.Ellipse
|
||||||
|
$dot.Style = $Window.Resources['AppRecommendationDotStyle']
|
||||||
|
$dot.Fill = switch ($app.Recommendation) { 'safe' { $brushSafe } 'unsafe' { $brushUnsafe } default { $brushDefault } }
|
||||||
|
$dot.ToolTip = switch ($app.Recommendation) {
|
||||||
|
'safe' { '[Recommended] Safe to remove for most users' }
|
||||||
|
'unsafe' { '[Not Recommended] Only remove if you know what you are doing' }
|
||||||
|
default { "[Optional] Remove if you don't need this app" }
|
||||||
|
}
|
||||||
|
[System.Windows.Controls.Grid]::SetColumn($dot, 0)
|
||||||
|
|
||||||
|
$tbName = New-Object System.Windows.Controls.TextBlock
|
||||||
|
$tbName.Text = $app.FriendlyName
|
||||||
|
$tbName.Style = $Window.Resources['AppNameTextStyle']
|
||||||
|
[System.Windows.Controls.Grid]::SetColumn($tbName, 1)
|
||||||
|
|
||||||
|
$tbDesc = New-Object System.Windows.Controls.TextBlock
|
||||||
|
$tbDesc.Text = $app.Description
|
||||||
|
$tbDesc.Style = $Window.Resources['AppDescTextStyle']
|
||||||
|
$tbDesc.ToolTip = $app.Description
|
||||||
|
[System.Windows.Controls.Grid]::SetColumn($tbDesc, 2)
|
||||||
|
|
||||||
|
$tbId = New-Object System.Windows.Controls.TextBlock
|
||||||
|
$tbId.Text = $app.AppIdDisplay
|
||||||
|
$tbId.Style = $Window.Resources["AppIdTextStyle"]
|
||||||
|
$tbId.ToolTip = $app.AppIdDisplay
|
||||||
|
[System.Windows.Controls.Grid]::SetColumn($tbId, 3)
|
||||||
|
|
||||||
|
$row.Children.Add($dot) | Out-Null
|
||||||
|
$row.Children.Add($tbName) | Out-Null
|
||||||
|
$row.Children.Add($tbDesc) | Out-Null
|
||||||
|
$row.Children.Add($tbId) | Out-Null
|
||||||
|
$checkbox.Content = $row
|
||||||
|
|
||||||
|
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppName' -Value $app.FriendlyName
|
||||||
|
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppDescription' -Value $app.Description
|
||||||
|
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'SelectedByDefault' -Value $app.SelectedByDefault
|
||||||
|
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppIds' -Value @($app.AppId)
|
||||||
|
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppIdDisplay' -Value $app.AppIdDisplay
|
||||||
|
|
||||||
|
$checkbox.Add_Checked({
|
||||||
|
$w = $script:MainWindow
|
||||||
|
Update-AppSelectionStatus -AppsPanel $w.FindName('AppSelectionPanel') `
|
||||||
|
-AppSelectionStatus $w.FindName('AppSelectionStatus') `
|
||||||
|
-AppRemovalScopeCombo $w.FindName('AppRemovalScopeCombo') `
|
||||||
|
-AppRemovalScopeSection $w.FindName('AppRemovalScopeSection') `
|
||||||
|
-AppRemovalScopeDescription $w.FindName('AppRemovalScopeDescription') `
|
||||||
|
-UserSelectionCombo $w.FindName('UserSelectionCombo')
|
||||||
|
})
|
||||||
|
$checkbox.Add_Unchecked({
|
||||||
|
$w = $script:MainWindow
|
||||||
|
Update-AppSelectionStatus -AppsPanel $w.FindName('AppSelectionPanel') `
|
||||||
|
-AppSelectionStatus $w.FindName('AppSelectionStatus') `
|
||||||
|
-AppRemovalScopeCombo $w.FindName('AppRemovalScopeCombo') `
|
||||||
|
-AppRemovalScopeSection $w.FindName('AppRemovalScopeSection') `
|
||||||
|
-AppRemovalScopeDescription $w.FindName('AppRemovalScopeDescription') `
|
||||||
|
-UserSelectionCombo $w.FindName('UserSelectionCombo')
|
||||||
|
})
|
||||||
|
AttachShiftClickBehavior -checkbox $checkbox -appsPanel $AppsPanel `
|
||||||
|
-lastSelectedCheckboxRef ([ref]$script:MainWindowLastSelectedCheckbox) `
|
||||||
|
-updateStatusCallback {
|
||||||
|
$w = $script:MainWindow
|
||||||
|
Update-AppSelectionStatus -AppsPanel $w.FindName('AppSelectionPanel') `
|
||||||
|
-AppSelectionStatus $w.FindName('AppSelectionStatus') `
|
||||||
|
-AppRemovalScopeCombo $w.FindName('AppRemovalScopeCombo') `
|
||||||
|
-AppRemovalScopeSection $w.FindName('AppRemovalScopeSection') `
|
||||||
|
-AppRemovalScopeDescription $w.FindName('AppRemovalScopeDescription') `
|
||||||
|
-UserSelectionCombo $w.FindName('UserSelectionCombo')
|
||||||
|
}
|
||||||
|
|
||||||
|
$AppsPanel.Children.Add($checkbox) | Out-Null
|
||||||
|
|
||||||
|
if (($i + 1) % $batchSize -eq 0) { DoEvents }
|
||||||
|
}
|
||||||
|
|
||||||
|
$sortArrowName = $Window.FindName('SortArrowName')
|
||||||
|
$sortArrowDescription = $Window.FindName('SortArrowDescription')
|
||||||
|
$sortArrowAppId = $Window.FindName('SortArrowAppId')
|
||||||
|
Update-AppsPanelSort -AppsPanel $AppsPanel -SortArrowName $sortArrowName -SortArrowDescription $sortArrowDescription -SortArrowAppId $sortArrowAppId
|
||||||
|
|
||||||
|
# If Default Mode was clicked while apps were still loading, apply defaults now
|
||||||
|
if ($script:PendingDefaultMode) {
|
||||||
|
$script:PendingDefaultMode = $false
|
||||||
|
Invoke-AppPreset -AppsPanel $AppsPanel -MatchFilter { param($c) $c.SelectedByDefault -eq $true } -Exclusive
|
||||||
|
}
|
||||||
|
|
||||||
|
$appSelectionStatusText = $Window.FindName('AppSelectionStatus')
|
||||||
|
$appRemovalScopeCombo = $Window.FindName('AppRemovalScopeCombo')
|
||||||
|
$appRemovalScopeSection = $Window.FindName('AppRemovalScopeSection')
|
||||||
|
$appRemovalScopeDescription = $Window.FindName('AppRemovalScopeDescription')
|
||||||
|
$userSelectionCombo = $Window.FindName('UserSelectionCombo')
|
||||||
|
Update-AppSelectionStatus -AppsPanel $AppsPanel -AppSelectionStatus $appSelectionStatusText `
|
||||||
|
-AppRemovalScopeCombo $appRemovalScopeCombo -AppRemovalScopeSection $appRemovalScopeSection `
|
||||||
|
-AppRemovalScopeDescription $appRemovalScopeDescription -UserSelectionCombo $userSelectionCombo
|
||||||
|
|
||||||
|
# Re-enable controls now that the full, correctly-checked app list is ready
|
||||||
|
$OnlyInstalledAppsBox.IsHitTestVisible = $true
|
||||||
|
$Window.FindName('DeploymentApplyBtn').IsEnabled = $true
|
||||||
|
if ($ImportConfigBtn) {
|
||||||
|
$ImportConfigBtn.IsEnabled = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Load-AppsIntoMainUI {
|
||||||
|
param(
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
[System.Windows.Controls.Panel]$AppsPanel,
|
||||||
|
[System.Windows.Controls.CheckBox]$OnlyInstalledAppsBox,
|
||||||
|
[System.Windows.Controls.Border]$LoadingAppsIndicator,
|
||||||
|
[System.Windows.Controls.MenuItem]$ImportConfigBtn
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prevent concurrent loads
|
||||||
|
if ($script:IsLoadingApps) { return }
|
||||||
|
$script:IsLoadingApps = $true
|
||||||
|
|
||||||
|
if ($ImportConfigBtn) {
|
||||||
|
$ImportConfigBtn.IsEnabled = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show loading indicator and clear existing apps
|
||||||
|
$LoadingAppsIndicator.Visibility = 'Visible'
|
||||||
|
$AppsPanel.Children.Clear()
|
||||||
|
|
||||||
|
# Disable controls while apps are loading so they can't be interacted with mid-load
|
||||||
|
$Window.FindName('DeploymentApplyBtn').IsEnabled = $false
|
||||||
|
$OnlyInstalledAppsBox.IsHitTestVisible = $false
|
||||||
|
|
||||||
|
# Update navigation buttons to disable Next/Previous
|
||||||
|
Update-NavigationButtons -Window $Window -TabControl $Window.FindName('MainTabControl')
|
||||||
|
|
||||||
|
# Force a render so the loading indicator is visible, then schedule the
|
||||||
|
# actual loading at Background priority so this call returns immediately.
|
||||||
|
# This is critical when called from Add_Loaded: the window must finish
|
||||||
|
# its initialization before we start a nested message pump via DoEvents.
|
||||||
|
$Window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Render, [action] {})
|
||||||
|
$Window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action] {
|
||||||
|
try {
|
||||||
|
$listOfApps = ""
|
||||||
|
|
||||||
|
if ($OnlyInstalledAppsBox.IsChecked -and ($script:WingetInstalled -eq $true)) {
|
||||||
|
$listOfApps = GetInstalledAppsViaWinget -TimeOut 10 -NonBlocking
|
||||||
|
|
||||||
|
if ($null -eq $listOfApps) {
|
||||||
|
Show-MessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null
|
||||||
|
$OnlyInstalledAppsBox.IsChecked = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Load-AppsWithList -Window $Window -AppsPanel $AppsPanel -OnlyInstalledAppsBox $OnlyInstalledAppsBox `
|
||||||
|
-LoadingAppsIndicator $LoadingAppsIndicator -ImportConfigBtn $ImportConfigBtn -ListOfApps $listOfApps
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Failed to load apps list: $($_.Exception.Message)"
|
||||||
|
$LoadingAppsIndicator.Visibility = 'Collapsed'
|
||||||
|
$OnlyInstalledAppsBox.IsHitTestVisible = $true
|
||||||
|
$Window.FindName('DeploymentApplyBtn').IsEnabled = $true
|
||||||
|
if ($ImportConfigBtn) { $ImportConfigBtn.IsEnabled = $true }
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$script:IsLoadingApps = $false
|
||||||
|
}
|
||||||
|
}) | Out-Null
|
||||||
|
}
|
||||||
487
Scripts/GUI/MainWindow-Deployment.ps1
Normal file
487
Scripts/GUI/MainWindow-Deployment.ps1
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
# MainWindow-Deployment.ps1
|
||||||
|
# Overview generation, pending tweak actions, feature labels, tweak preset maps, apply logic, user mode state, user selection, and validation.
|
||||||
|
|
||||||
|
function Get-FeatureLabel {
|
||||||
|
param(
|
||||||
|
[string]$FeatureId,
|
||||||
|
$FallbackLabel = $null
|
||||||
|
)
|
||||||
|
|
||||||
|
$label = $script:FeatureLabelLookup[$FeatureId]
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace([string]$label)) {
|
||||||
|
return [string]$label
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace([string]$FallbackLabel)) {
|
||||||
|
return [string]$FallbackLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
return [string]$FeatureId
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-UndoFeatureLabel {
|
||||||
|
param(
|
||||||
|
[string]$FeatureId,
|
||||||
|
$FallbackLabel = $null
|
||||||
|
)
|
||||||
|
|
||||||
|
$undoLabel = $script:UndoFeatureLabelLookup[$FeatureId]
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace([string]$undoLabel)) {
|
||||||
|
return [string]$undoLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fall back to the regular label (prefixed for undo context)
|
||||||
|
$label = Get-FeatureLabel -FeatureId $FeatureId -FallbackLabel $FallbackLabel
|
||||||
|
return [string]$label
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PendingTweakActions {
|
||||||
|
param(
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
[bool]$ShowAppliedTweaksMode
|
||||||
|
)
|
||||||
|
|
||||||
|
$actions = New-Object System.Collections.Generic.List[object]
|
||||||
|
|
||||||
|
if (-not $script:UiControlMappings) {
|
||||||
|
return @($actions.ToArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($mappingKey in $script:UiControlMappings.Keys) {
|
||||||
|
$control = $Window.FindName($mappingKey)
|
||||||
|
if (-not $control) { continue }
|
||||||
|
$mapping = $script:UiControlMappings[$mappingKey]
|
||||||
|
|
||||||
|
if ($control -is [System.Windows.Controls.CheckBox] -and $mapping.Type -eq 'feature') {
|
||||||
|
$wasApplied = $false
|
||||||
|
if ($ShowAppliedTweaksMode -and $null -ne $control.PSObject.Properties['SystemState']) {
|
||||||
|
$wasApplied = [bool]$control.SystemState
|
||||||
|
}
|
||||||
|
elseif ($null -ne $control.PSObject.Properties['InitialState']) {
|
||||||
|
$wasApplied = [bool]$control.InitialState
|
||||||
|
}
|
||||||
|
elseif ($null -ne $control.PSObject.Properties['SystemState']) {
|
||||||
|
$wasApplied = [bool]$control.SystemState
|
||||||
|
}
|
||||||
|
$isNowChecked = $control.IsChecked -eq $true
|
||||||
|
|
||||||
|
if (-not $wasApplied -and $isNowChecked) {
|
||||||
|
$actions.Add([PSCustomObject]@{
|
||||||
|
Action = 'Apply'
|
||||||
|
FeatureId = [string]$mapping.FeatureId
|
||||||
|
Label = (Get-FeatureLabel -FeatureId $mapping.FeatureId -FallbackLabel $mapping.Label)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
elseif ($wasApplied -and -not $isNowChecked) {
|
||||||
|
$actions.Add([PSCustomObject]@{
|
||||||
|
Action = 'Undo'
|
||||||
|
FeatureId = [string]$mapping.FeatureId
|
||||||
|
Label = (Get-FeatureLabel -FeatureId $mapping.FeatureId -FallbackLabel $mapping.Label)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($control -is [System.Windows.Controls.ComboBox] -and $mapping.Type -eq 'group') {
|
||||||
|
$wasIndex = 0
|
||||||
|
if ($ShowAppliedTweaksMode -and $null -ne $control.PSObject.Properties['SystemIndex']) {
|
||||||
|
$wasIndex = [int]$control.SystemIndex
|
||||||
|
}
|
||||||
|
elseif ($null -ne $control.PSObject.Properties['InitialIndex']) {
|
||||||
|
$wasIndex = [int]$control.InitialIndex
|
||||||
|
}
|
||||||
|
elseif ($null -ne $control.PSObject.Properties['SystemIndex']) {
|
||||||
|
$wasIndex = [int]$control.SystemIndex
|
||||||
|
}
|
||||||
|
$isNowIndex = $control.SelectedIndex
|
||||||
|
|
||||||
|
if ($wasIndex -eq $isNowIndex) { continue }
|
||||||
|
|
||||||
|
if ($isNowIndex -gt 0 -and $isNowIndex -le $mapping.Values.Count) {
|
||||||
|
$selectedValue = $mapping.Values[$isNowIndex - 1]
|
||||||
|
foreach ($fid in $selectedValue.FeatureIds) {
|
||||||
|
$actions.Add([PSCustomObject]@{
|
||||||
|
Action = 'Apply'
|
||||||
|
FeatureId = [string]$fid
|
||||||
|
Label = (Get-FeatureLabel -FeatureId $fid)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return @($actions.ToArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-Overview {
|
||||||
|
param(
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
[System.Windows.Controls.StackPanel]$AppsPanel,
|
||||||
|
$ShowCurrentlyAppliedTweaksCheckBox
|
||||||
|
)
|
||||||
|
|
||||||
|
$changesList = @()
|
||||||
|
$showAppliedTweaksMode = ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -eq $true)
|
||||||
|
|
||||||
|
# Collect selected apps
|
||||||
|
$selectedAppsCount = 0
|
||||||
|
foreach ($child in $AppsPanel.Children) {
|
||||||
|
if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) {
|
||||||
|
$selectedAppsCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($selectedAppsCount -gt 0) {
|
||||||
|
$changesList += "Remove $selectedAppsCount application(s)"
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($tweakAction in @(Get-PendingTweakActions -Window $Window -ShowAppliedTweaksMode:$showAppliedTweaksMode)) {
|
||||||
|
if ($tweakAction.Action -eq 'Undo') {
|
||||||
|
$changesList += "Undo: $($tweakAction.Label)"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$changesList += $tweakAction.Label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $changesList
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-ShowChangesOverview {
|
||||||
|
param(
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
[System.Windows.Controls.StackPanel]$AppsPanel,
|
||||||
|
$ShowCurrentlyAppliedTweaksCheckBox
|
||||||
|
)
|
||||||
|
|
||||||
|
$changesList = New-Overview -Window $Window -AppsPanel $AppsPanel -ShowCurrentlyAppliedTweaksCheckBox $ShowCurrentlyAppliedTweaksCheckBox
|
||||||
|
|
||||||
|
if ($changesList.Count -eq 0) {
|
||||||
|
Show-MessageBox -Message 'No changes have been selected.' -Title 'Selected Changes' -Button 'OK' -Icon 'Information'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = ($changesList | ForEach-Object { "$([char]0x2022) $_" }) -join "`n"
|
||||||
|
Show-MessageBox -Message $message -Title 'Selected Changes' -Button 'OK' -Icon 'None' -Width 600
|
||||||
|
}
|
||||||
|
|
||||||
|
function Build-TweakPresetControlMap {
|
||||||
|
param(
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
$SettingsJson
|
||||||
|
)
|
||||||
|
|
||||||
|
$presetMap = @{}
|
||||||
|
if (-not $SettingsJson -or -not $SettingsJson.Settings -or -not $script:UiControlMappings) {
|
||||||
|
return $presetMap
|
||||||
|
}
|
||||||
|
|
||||||
|
# FeatureId -> control metadata, similar to ApplySettingsToUiControls lookup.
|
||||||
|
$featureIdIndex = @{}
|
||||||
|
foreach ($controlName in $script:UiControlMappings.Keys) {
|
||||||
|
$control = $Window.FindName($controlName)
|
||||||
|
if (-not $control -or $control.Visibility -ne 'Visible') { continue }
|
||||||
|
|
||||||
|
$mapping = $script:UiControlMappings[$controlName]
|
||||||
|
if ($mapping.Type -eq 'group') {
|
||||||
|
$i = 1
|
||||||
|
foreach ($val in $mapping.Values) {
|
||||||
|
foreach ($fid in $val.FeatureIds) {
|
||||||
|
$featureIdIndex[$fid] = @{ ControlName = $controlName; Control = $control; MappingType = 'group'; Index = $i }
|
||||||
|
}
|
||||||
|
$i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($mapping.Type -eq 'feature') {
|
||||||
|
$featureIdIndex[$mapping.FeatureId] = @{ ControlName = $controlName; Control = $control; MappingType = 'feature' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($setting in $SettingsJson.Settings) {
|
||||||
|
if ($setting.Value -ne $true) { continue }
|
||||||
|
if ($setting.Name -eq 'CreateRestorePoint') { continue }
|
||||||
|
|
||||||
|
$entry = $featureIdIndex[$setting.Name]
|
||||||
|
if (-not $entry) { continue }
|
||||||
|
if ($presetMap.ContainsKey($entry.ControlName)) { continue }
|
||||||
|
|
||||||
|
$controlType = if ($entry.Control -is [System.Windows.Controls.CheckBox]) { 'CheckBox' } else { 'ComboBox' }
|
||||||
|
$desiredValue = switch ($entry.MappingType) {
|
||||||
|
'group' { $entry.Index }
|
||||||
|
default { if ($controlType -eq 'CheckBox') { $true } else { 1 } }
|
||||||
|
}
|
||||||
|
|
||||||
|
$presetMap[$entry.ControlName] = @{ Control = $entry.Control; ControlType = $controlType; DesiredValue = $desiredValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
return $presetMap
|
||||||
|
}
|
||||||
|
|
||||||
|
function Build-CategoryTweakPresetMap {
|
||||||
|
param(
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
[string]$Category
|
||||||
|
)
|
||||||
|
|
||||||
|
$presetMap = @{}
|
||||||
|
if (-not $script:UiControlMappings) { return $presetMap }
|
||||||
|
|
||||||
|
foreach ($controlName in $script:UiControlMappings.Keys) {
|
||||||
|
$mapping = $script:UiControlMappings[$controlName]
|
||||||
|
if ($mapping.Category -ne $Category) { continue }
|
||||||
|
|
||||||
|
$control = $Window.FindName($controlName)
|
||||||
|
if (-not $control -or $control.Visibility -ne 'Visible') { continue }
|
||||||
|
|
||||||
|
$controlType = if ($control -is [System.Windows.Controls.CheckBox]) { 'CheckBox' } else { 'ComboBox' }
|
||||||
|
$desiredValue = if ($controlType -eq 'CheckBox') { $true } else { 1 }
|
||||||
|
$presetMap[$controlName] = @{ Control = $control; ControlType = $controlType; DesiredValue = $desiredValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
return $presetMap
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SavedAppIdsFromSettingsJson {
|
||||||
|
param($SettingsJson)
|
||||||
|
|
||||||
|
if (-not $SettingsJson -or -not $SettingsJson.Settings) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$appsValue = $null
|
||||||
|
foreach ($setting in $SettingsJson.Settings) {
|
||||||
|
if ($setting.Name -eq 'Apps' -and $setting.Value) {
|
||||||
|
$appsValue = $setting.Value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $appsValue) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$savedAppIds = @()
|
||||||
|
if ($appsValue -is [string]) {
|
||||||
|
$savedAppIds = $appsValue.Split(',')
|
||||||
|
}
|
||||||
|
elseif ($appsValue -is [array]) {
|
||||||
|
$savedAppIds = $appsValue
|
||||||
|
}
|
||||||
|
|
||||||
|
$savedAppIds = $savedAppIds | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }
|
||||||
|
if ($savedAppIds.Count -eq 0) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
return $savedAppIds
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-ApplyTweakPresetMap {
|
||||||
|
param(
|
||||||
|
[hashtable]$PresetMap,
|
||||||
|
[bool]$Check
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $PresetMap) {
|
||||||
|
$PresetMap = @{}
|
||||||
|
}
|
||||||
|
|
||||||
|
$wasUpdatingTweakPresets = [bool]$script:UpdatingTweakPresets
|
||||||
|
$script:UpdatingTweakPresets = $true
|
||||||
|
try {
|
||||||
|
foreach ($target in $PresetMap.Values) {
|
||||||
|
$control = $target.Control
|
||||||
|
if (-not $control) { continue }
|
||||||
|
|
||||||
|
if ($target.ControlType -eq 'CheckBox') {
|
||||||
|
$control.IsChecked = $Check
|
||||||
|
}
|
||||||
|
elseif ($target.ControlType -eq 'ComboBox') {
|
||||||
|
$desiredIndex = [int]$target.DesiredValue
|
||||||
|
if ($Check) {
|
||||||
|
$control.SelectedIndex = $desiredIndex
|
||||||
|
}
|
||||||
|
elseif ($control.SelectedIndex -eq $desiredIndex) {
|
||||||
|
$control.SelectedIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$script:UpdatingTweakPresets = $wasUpdatingTweakPresets
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $wasUpdatingTweakPresets) {
|
||||||
|
Update-TweakPresetStates -Window $script:MainWindow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-TweakPresetCheckBoxState {
|
||||||
|
param(
|
||||||
|
[System.Windows.Controls.CheckBox]$PresetCheckBox,
|
||||||
|
[hashtable]$PresetMap
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $PresetCheckBox) { return }
|
||||||
|
if (-not $PresetMap) {
|
||||||
|
$PresetMap = @{}
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = $PresetMap.Count
|
||||||
|
$selected = 0
|
||||||
|
|
||||||
|
foreach ($target in $PresetMap.Values) {
|
||||||
|
$control = $target.Control
|
||||||
|
if (-not $control) { continue }
|
||||||
|
|
||||||
|
if ($target.ControlType -eq 'CheckBox' -and $control.IsChecked -eq $true) {
|
||||||
|
$selected++
|
||||||
|
}
|
||||||
|
elseif ($target.ControlType -eq 'ComboBox' -and $control.SelectedIndex -eq [int]$target.DesiredValue) {
|
||||||
|
$selected++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Set-TriStatePresetCheckBoxState -CheckBox $PresetCheckBox -Total $total -Selected $selected
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-TweakPresetStates {
|
||||||
|
param([System.Windows.Window]$Window)
|
||||||
|
|
||||||
|
$script:UpdatingTweakPresets = $true
|
||||||
|
try {
|
||||||
|
$presetDefaultTweaksBtn = $Window.FindName('PresetDefaultTweaksBtn')
|
||||||
|
$presetLastUsedTweaksBtn = $Window.FindName('PresetLastUsedTweaksBtn')
|
||||||
|
$presetPrivacyTweaksBtn = $Window.FindName('PresetPrivacyTweaksBtn')
|
||||||
|
$presetAITweaksBtn = $Window.FindName('PresetAITweaksBtn')
|
||||||
|
|
||||||
|
Set-TweakPresetCheckBoxState -PresetCheckBox $presetDefaultTweaksBtn -PresetMap $script:DefaultTweakPresetMap
|
||||||
|
if ($presetLastUsedTweaksBtn -and $presetLastUsedTweaksBtn.Visibility -ne 'Collapsed') {
|
||||||
|
Set-TweakPresetCheckBoxState -PresetCheckBox $presetLastUsedTweaksBtn -PresetMap $script:LastUsedTweakPresetMap
|
||||||
|
}
|
||||||
|
Set-TweakPresetCheckBoxState -PresetCheckBox $presetPrivacyTweaksBtn -PresetMap $script:PrivacyTweakPresetMap
|
||||||
|
Set-TweakPresetCheckBoxState -PresetCheckBox $presetAITweaksBtn -PresetMap $script:AITweakPresetMap
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$script:UpdatingTweakPresets = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Register-TweakPresetControlStateHandlers {
|
||||||
|
param([System.Windows.Window]$Window)
|
||||||
|
|
||||||
|
if (-not $script:UiControlMappings) { return }
|
||||||
|
|
||||||
|
foreach ($controlName in $script:UiControlMappings.Keys) {
|
||||||
|
$control = $Window.FindName($controlName)
|
||||||
|
if (-not $control) { continue }
|
||||||
|
|
||||||
|
if ($control -is [System.Windows.Controls.CheckBox]) {
|
||||||
|
$control.Add_Checked({ if (-not $script:UpdatingTweakPresets) { Update-TweakPresetStates -Window $script:MainWindow } })
|
||||||
|
$control.Add_Unchecked({ if (-not $script:UpdatingTweakPresets) { Update-TweakPresetStates -Window $script:MainWindow } })
|
||||||
|
}
|
||||||
|
elseif ($control -is [System.Windows.Controls.ComboBox]) {
|
||||||
|
$control.Add_SelectionChanged({ if (-not $script:UpdatingTweakPresets) { Update-TweakPresetStates -Window $script:MainWindow } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Initialize-TweakPresetSources {
|
||||||
|
param(
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
$DefaultSettingsJson,
|
||||||
|
$LastUsedSettingsJson
|
||||||
|
)
|
||||||
|
|
||||||
|
$script:DefaultTweakPresetMap = Build-TweakPresetControlMap -Window $Window -SettingsJson $DefaultSettingsJson
|
||||||
|
$script:LastUsedTweakPresetMap = Build-TweakPresetControlMap -Window $Window -SettingsJson $LastUsedSettingsJson
|
||||||
|
$script:PrivacyTweakPresetMap = Build-CategoryTweakPresetMap -Window $Window -Category 'Privacy & Suggested Content'
|
||||||
|
$script:AITweakPresetMap = Build-CategoryTweakPresetMap -Window $Window -Category 'AI'
|
||||||
|
|
||||||
|
$presetLastUsedTweaksBtn = $Window.FindName('PresetLastUsedTweaksBtn')
|
||||||
|
if ($presetLastUsedTweaksBtn) {
|
||||||
|
$presetLastUsedTweaksBtn.Visibility = if ($script:LastUsedTweakPresetMap.Count -gt 0) { 'Visible' } else { 'Collapsed' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-AppliedTweaksUserModeState {
|
||||||
|
param(
|
||||||
|
[System.Windows.Controls.CheckBox]$ShowCurrentlyAppliedTweaksCheckBox,
|
||||||
|
[System.Windows.Controls.ComboBox]$UserSelectionCombo
|
||||||
|
)
|
||||||
|
|
||||||
|
# Show/hide detect applied tweaks checkbox based on user mode
|
||||||
|
if ($ShowCurrentlyAppliedTweaksCheckBox) {
|
||||||
|
if ($UserSelectionCombo.SelectedIndex -eq 0) {
|
||||||
|
$ShowCurrentlyAppliedTweaksCheckBox.Visibility = 'Visible'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$ShowCurrentlyAppliedTweaksCheckBox.Visibility = 'Collapsed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Enable/disable user mode combo based on params only (not checkbox)
|
||||||
|
if ($script:Params.ContainsKey('Sysprep') -or $script:Params.ContainsKey('User')) {
|
||||||
|
$UserSelectionCombo.IsEnabled = $false
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$UserSelectionCombo.IsEnabled = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-UserSelectionDescription {
|
||||||
|
param(
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
[System.Windows.Controls.ComboBox]$UserSelectionCombo,
|
||||||
|
[System.Windows.Controls.TextBox]$OtherUsernameTextBox,
|
||||||
|
[System.Windows.Controls.TextBlock]$UserSelectionDescription
|
||||||
|
)
|
||||||
|
|
||||||
|
switch ($UserSelectionCombo.SelectedIndex) {
|
||||||
|
0 {
|
||||||
|
$currentUserName = GetUserName
|
||||||
|
if ([string]::IsNullOrWhiteSpace($currentUserName)) {
|
||||||
|
$UserSelectionDescription.Text = "The currently logged-in user profile"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$UserSelectionDescription.Text = "The currently logged-in user profile: $currentUserName"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1 {
|
||||||
|
$targetUserName = $OtherUsernameTextBox.Text.Trim()
|
||||||
|
if ([string]::IsNullOrWhiteSpace($targetUserName)) {
|
||||||
|
$UserSelectionDescription.Text = "A different user profile on this system"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$UserSelectionDescription.Text = "A different user profile on this system: $targetUserName"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
$UserSelectionDescription.Text = "The default user template, affecting all new users created after this point. Useful for Sysprep deployment."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-OtherUsername {
|
||||||
|
param(
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
[System.Windows.Controls.ComboBox]$UserSelectionCombo,
|
||||||
|
[System.Windows.Controls.TextBox]$OtherUsernameTextBox,
|
||||||
|
[System.Windows.Controls.TextBlock]$UsernameValidationMessage
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only validate if "Other User" is selected
|
||||||
|
if ($UserSelectionCombo.SelectedIndex -ne 1) {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
$errorBrush = $Window.Resources['ValidationErrorColor']
|
||||||
|
$successBrush = $Window.Resources['ValidationSuccessColor']
|
||||||
|
$validationResult = Test-TargetUserName -UserName $OtherUsernameTextBox.Text
|
||||||
|
|
||||||
|
$UsernameValidationMessage.Text = $validationResult.Message
|
||||||
|
if ($validationResult.IsValid) {
|
||||||
|
$UsernameValidationMessage.Foreground = $successBrush
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
$UsernameValidationMessage.Foreground = $errorBrush
|
||||||
|
return $false
|
||||||
|
}
|
||||||
72
Scripts/GUI/MainWindow-Navigation.ps1
Normal file
72
Scripts/GUI/MainWindow-Navigation.ps1
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# MainWindow-Navigation.ps1
|
||||||
|
# Wizard navigation helpers: tab navigation buttons and progress indicators.
|
||||||
|
|
||||||
|
function Update-NavigationButtons {
|
||||||
|
param(
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
[System.Windows.Controls.TabControl]$TabControl
|
||||||
|
)
|
||||||
|
|
||||||
|
$currentIndex = $TabControl.SelectedIndex
|
||||||
|
$totalTabs = $TabControl.Items.Count
|
||||||
|
|
||||||
|
$previousBtn = $Window.FindName('PreviousBtn')
|
||||||
|
$nextBtn = $Window.FindName('NextBtn')
|
||||||
|
|
||||||
|
$homeIndex = 0
|
||||||
|
$overviewIndex = $totalTabs - 1
|
||||||
|
|
||||||
|
# Navigation button visibility
|
||||||
|
if ($currentIndex -eq $homeIndex) {
|
||||||
|
$nextBtn.Visibility = 'Collapsed'
|
||||||
|
$previousBtn.Visibility = 'Collapsed'
|
||||||
|
}
|
||||||
|
elseif ($currentIndex -eq $overviewIndex) {
|
||||||
|
$nextBtn.Visibility = 'Collapsed'
|
||||||
|
$previousBtn.Visibility = 'Visible'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$nextBtn.Visibility = 'Visible'
|
||||||
|
$previousBtn.Visibility = 'Visible'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update progress indicators
|
||||||
|
# Tab indices: 0=Home, 1=App Removal, 2=Tweaks, 3=Deployment Settings
|
||||||
|
$progressIndicator1 = $Window.FindName('ProgressIndicator1') # App Removal
|
||||||
|
$progressIndicator2 = $Window.FindName('ProgressIndicator2') # Tweaks
|
||||||
|
$progressIndicator3 = $Window.FindName('ProgressIndicator3') # Deployment Settings
|
||||||
|
$bottomNavGrid = $Window.FindName('BottomNavGrid')
|
||||||
|
|
||||||
|
# Hide bottom navigation on home page
|
||||||
|
if ($currentIndex -eq 0) {
|
||||||
|
$bottomNavGrid.Visibility = 'Collapsed'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$bottomNavGrid.Visibility = 'Visible'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update indicator colors based on current tab
|
||||||
|
# Indicator 1 (App Removal) - tab index 1
|
||||||
|
if ($currentIndex -ge 1) {
|
||||||
|
$progressIndicator1.Fill = $Window.Resources['ProgressActiveColor']
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$progressIndicator1.Fill = $Window.Resources['ProgressInactiveColor']
|
||||||
|
}
|
||||||
|
|
||||||
|
# Indicator 2 (Tweaks) - tab index 2
|
||||||
|
if ($currentIndex -ge 2) {
|
||||||
|
$progressIndicator2.Fill = $Window.Resources['ProgressActiveColor']
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$progressIndicator2.Fill = $Window.Resources['ProgressInactiveColor']
|
||||||
|
}
|
||||||
|
|
||||||
|
# Indicator 3 (Deployment Settings) - tab index 3
|
||||||
|
if ($currentIndex -ge 3) {
|
||||||
|
$progressIndicator3.Fill = $Window.Resources['ProgressActiveColor']
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$progressIndicator3.Fill = $Window.Resources['ProgressInactiveColor']
|
||||||
|
}
|
||||||
|
}
|
||||||
511
Scripts/GUI/MainWindow-TweaksBuilder.ps1
Normal file
511
Scripts/GUI/MainWindow-TweaksBuilder.ps1
Normal file
@@ -0,0 +1,511 @@
|
|||||||
|
# MainWindow-TweaksBuilder.ps1
|
||||||
|
# Dynamic tweaks UI construction from Features.json, tweak state management, selection clear, and search/highlight.
|
||||||
|
|
||||||
|
function Build-DynamicTweaks {
|
||||||
|
param(
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
[int]$WinVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0"
|
||||||
|
|
||||||
|
if (-not $featuresJson) {
|
||||||
|
throw "Unable to load Features.json file. The GUI cannot continue without feature definitions."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Column containers
|
||||||
|
$col0 = $Window.FindName('Column0Panel')
|
||||||
|
$col1 = $Window.FindName('Column1Panel')
|
||||||
|
$col2 = $Window.FindName('Column2Panel')
|
||||||
|
$columns = @($col0, $col1, $col2) | Where-Object { $_ -ne $null }
|
||||||
|
|
||||||
|
# Clear all columns for fully dynamic panel creation
|
||||||
|
foreach ($col in $columns) {
|
||||||
|
if ($col) { $col.Children.Clear() }
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:UiControlMappings = @{}
|
||||||
|
$script:CategoryCardMap = @{}
|
||||||
|
$script:TweaksCompactMode = $null
|
||||||
|
$script:TweaksCardsMovedFromCol2 = @()
|
||||||
|
|
||||||
|
function CreateLabeledCombo($parent, $labelText, $comboName, $items) {
|
||||||
|
# If only 2 items (No Change + one option), use a checkbox instead
|
||||||
|
if ($items.Count -eq 2) {
|
||||||
|
$checkbox = New-Object System.Windows.Controls.CheckBox
|
||||||
|
$checkbox.Content = $labelText
|
||||||
|
$checkbox.Name = $comboName
|
||||||
|
$checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $labelText)
|
||||||
|
$checkbox.IsChecked = $false
|
||||||
|
$checkbox.Style = $Window.Resources["FeatureCheckboxStyle"]
|
||||||
|
$parent.Children.Add($checkbox) | Out-Null
|
||||||
|
|
||||||
|
# Register the checkbox with the window's name scope
|
||||||
|
try {
|
||||||
|
[System.Windows.NameScope]::SetNameScope($checkbox, [System.Windows.NameScope]::GetNameScope($Window))
|
||||||
|
$Window.RegisterName($comboName, $checkbox)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# Name might already be registered, ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return $checkbox
|
||||||
|
}
|
||||||
|
|
||||||
|
# Otherwise use a combobox for multiple options
|
||||||
|
# Wrap label in a Border for search highlighting
|
||||||
|
$lblBorder = New-Object System.Windows.Controls.Border
|
||||||
|
$lblBorder.Style = $Window.Resources['LabelBorderStyle']
|
||||||
|
$lblBorderName = "$comboName`_LabelBorder"
|
||||||
|
$lblBorder.Name = $lblBorderName
|
||||||
|
|
||||||
|
$lbl = New-Object System.Windows.Controls.TextBlock
|
||||||
|
$lbl.Text = $labelText
|
||||||
|
$lbl.Style = $Window.Resources['LabelStyle']
|
||||||
|
$labelName = "$comboName`_Label"
|
||||||
|
$lbl.Name = $labelName
|
||||||
|
|
||||||
|
$lblBorder.Child = $lbl
|
||||||
|
$parent.Children.Add($lblBorder) | Out-Null
|
||||||
|
|
||||||
|
# Register the label border with the window's name scope
|
||||||
|
try {
|
||||||
|
[System.Windows.NameScope]::SetNameScope($lblBorder, [System.Windows.NameScope]::GetNameScope($Window))
|
||||||
|
$Window.RegisterName($lblBorderName, $lblBorder)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# Name might already be registered, ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
$combo = New-Object System.Windows.Controls.ComboBox
|
||||||
|
$combo.Name = $comboName
|
||||||
|
$combo.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $labelText)
|
||||||
|
foreach ($item in $items) { $comboItem = New-Object System.Windows.Controls.ComboBoxItem; $comboItem.Content = $item; $combo.Items.Add($comboItem) | Out-Null }
|
||||||
|
$combo.SelectedIndex = 0
|
||||||
|
$parent.Children.Add($combo) | Out-Null
|
||||||
|
|
||||||
|
# Register the combo box with the window's name scope
|
||||||
|
try {
|
||||||
|
[System.Windows.NameScope]::SetNameScope($combo, [System.Windows.NameScope]::GetNameScope($Window))
|
||||||
|
$Window.RegisterName($comboName, $combo)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# Name might already be registered, ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return $combo
|
||||||
|
}
|
||||||
|
|
||||||
|
function GetWikiUrlForCategory($category) {
|
||||||
|
if (-not $category) { return 'https://github.com/Raphire/Win11Debloat/wiki/Features' }
|
||||||
|
|
||||||
|
$slug = $category.ToLowerInvariant()
|
||||||
|
$slug = $slug -replace '&', ''
|
||||||
|
$slug = $slug -replace '[^a-z0-9\s-]', ''
|
||||||
|
$slug = $slug -replace '\s', '-'
|
||||||
|
|
||||||
|
return "https://github.com/Raphire/Win11Debloat/wiki/Features#$slug"
|
||||||
|
}
|
||||||
|
|
||||||
|
function GetOrCreateCategoryCard($categoryObj) {
|
||||||
|
$categoryName = $categoryObj.Name
|
||||||
|
$categoryIcon = $categoryObj.Icon
|
||||||
|
|
||||||
|
if ($script:CategoryCardMap.ContainsKey($categoryName)) { return $script:CategoryCardMap[$categoryName] }
|
||||||
|
|
||||||
|
# Create a new card Border + StackPanel and add to shortest column
|
||||||
|
$target = $columns | Sort-Object @{Expression = { $_.Children.Count }; Ascending = $true }, @{Expression = { $columns.IndexOf($_) }; Ascending = $true } | Select-Object -First 1
|
||||||
|
|
||||||
|
$border = New-Object System.Windows.Controls.Border
|
||||||
|
$border.Style = $Window.Resources['CategoryCardBorderStyle']
|
||||||
|
$border.Tag = 'DynamicCategory'
|
||||||
|
|
||||||
|
$panel = New-Object System.Windows.Controls.StackPanel
|
||||||
|
$safe = ($categoryName -replace '[^a-zA-Z0-9_]', '_')
|
||||||
|
$panel.Name = "Category_{0}_Panel" -f $safe
|
||||||
|
|
||||||
|
$headerRow = New-Object System.Windows.Controls.StackPanel
|
||||||
|
$headerRow.Orientation = 'Horizontal'
|
||||||
|
|
||||||
|
# Add category icon
|
||||||
|
$icon = New-Object System.Windows.Controls.TextBlock
|
||||||
|
# Convert HTML entity to character (e.g.,  -> actual character)
|
||||||
|
if ($categoryIcon -match '&#x([0-9A-Fa-f]+);') {
|
||||||
|
$hexValue = [Convert]::ToInt32($matches[1], 16)
|
||||||
|
$icon.Text = [char]$hexValue
|
||||||
|
}
|
||||||
|
$icon.Style = $Window.Resources['CategoryHeaderIcon']
|
||||||
|
$headerRow.Children.Add($icon) | Out-Null
|
||||||
|
|
||||||
|
$header = New-Object System.Windows.Controls.TextBlock
|
||||||
|
$header.Text = $categoryName
|
||||||
|
$header.Style = $Window.Resources['CategoryHeaderTextBlock']
|
||||||
|
$headerRow.Children.Add($header) | Out-Null
|
||||||
|
|
||||||
|
$helpIcon = New-Object System.Windows.Controls.TextBlock
|
||||||
|
$helpIcon.Text = '(?)'
|
||||||
|
$helpIcon.Style = $Window.Resources['CategoryHelpLinkTextStyle']
|
||||||
|
|
||||||
|
$helpBtn = New-Object System.Windows.Controls.Button
|
||||||
|
$helpBtn.Content = $helpIcon
|
||||||
|
$helpBtn.ToolTip = "Open wiki for more info on '$categoryName' tweaks"
|
||||||
|
$helpBtn.Tag = (GetWikiUrlForCategory -category $categoryName)
|
||||||
|
$helpBtn.Style = $Window.Resources['CategoryHelpLinkButtonStyle']
|
||||||
|
$helpBtn.Add_Click({
|
||||||
|
param($button, $e)
|
||||||
|
if ($button.Tag) { Start-Process $button.Tag }
|
||||||
|
})
|
||||||
|
$headerRow.Children.Add($helpBtn) | Out-Null
|
||||||
|
|
||||||
|
$panel.Children.Add($headerRow) | Out-Null
|
||||||
|
|
||||||
|
$border.Child = $panel
|
||||||
|
$target.Children.Add($border) | Out-Null
|
||||||
|
|
||||||
|
$script:CategoryCardMap[$categoryName] = $panel
|
||||||
|
return $panel
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine categories present (from lists and features)
|
||||||
|
$categoriesPresent = @{}
|
||||||
|
if ($featuresJson.UiGroups) {
|
||||||
|
foreach ($g in $featuresJson.UiGroups) { if ($g.Category) { $categoriesPresent[$g.Category] = $true } }
|
||||||
|
}
|
||||||
|
foreach ($f in $featuresJson.Features) { if ($f.Category) { $categoriesPresent[$f.Category] = $true } }
|
||||||
|
|
||||||
|
# Create cards in the order defined in Features.json Categories (if present)
|
||||||
|
$orderedCategories = @()
|
||||||
|
if ($featuresJson.Categories) {
|
||||||
|
foreach ($c in $featuresJson.Categories) {
|
||||||
|
$categoryName = if ($c -is [string]) { $c } else { $c.Name }
|
||||||
|
if ($categoriesPresent.ContainsKey($categoryName)) {
|
||||||
|
# Store the full category object (or create one with default icon for string categories)
|
||||||
|
$categoryObj = if ($c -is [string]) { @{Name = $c; Icon = '' } } else { $c }
|
||||||
|
$orderedCategories += $categoryObj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# For backward compatibility, create category objects from keys
|
||||||
|
foreach ($catName in $categoriesPresent.Keys) {
|
||||||
|
$orderedCategories += @{Name = $catName; Icon = '' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($categoryObj in $orderedCategories) {
|
||||||
|
$categoryName = $categoryObj.Name
|
||||||
|
|
||||||
|
# Create/get card for this category
|
||||||
|
$panel = GetOrCreateCategoryCard -categoryObj $categoryObj
|
||||||
|
if (-not $panel) { continue }
|
||||||
|
|
||||||
|
# Collect groups and features for this category, then sort by priority
|
||||||
|
$categoryItems = @()
|
||||||
|
|
||||||
|
# Add any groups for this category
|
||||||
|
if ($featuresJson.UiGroups) {
|
||||||
|
$groupIndex = 0
|
||||||
|
foreach ($group in $featuresJson.UiGroups) {
|
||||||
|
if ($group.Category -ne $categoryName) { $groupIndex++; continue }
|
||||||
|
$categoryItems += [PSCustomObject]@{
|
||||||
|
Type = 'group'
|
||||||
|
Data = $group
|
||||||
|
Priority = if ($null -ne $group.Priority) { $group.Priority } else { [int]::MaxValue }
|
||||||
|
OriginalIndex = $groupIndex
|
||||||
|
}
|
||||||
|
$groupIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add individual features for this category
|
||||||
|
$featureIndex = 0
|
||||||
|
foreach ($feature in $featuresJson.Features) {
|
||||||
|
if ($feature.Category -ne $categoryName) { $featureIndex++; continue }
|
||||||
|
|
||||||
|
# Check version and feature compatibility using Features.json
|
||||||
|
if (($feature.MinVersion -and $WinVersion -lt $feature.MinVersion) -or ($feature.MaxVersion -and $WinVersion -gt $feature.MaxVersion) -or ($feature.FeatureId -eq 'DisableModernStandbyNetworking' -and (-not $script:ModernStandbySupported))) {
|
||||||
|
$featureIndex++; continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Skip if feature part of a group
|
||||||
|
$inGroup = $false
|
||||||
|
if ($featuresJson.UiGroups) {
|
||||||
|
foreach ($g in $featuresJson.UiGroups) { foreach ($val in $g.Values) { if ($val.FeatureIds -contains $feature.FeatureId) { $inGroup = $true; break } }; if ($inGroup) { break } }
|
||||||
|
}
|
||||||
|
if ($inGroup) { $featureIndex++; continue }
|
||||||
|
|
||||||
|
$categoryItems += [PSCustomObject]@{
|
||||||
|
Type = 'feature'
|
||||||
|
Data = $feature
|
||||||
|
Priority = if ($null -ne $feature.Priority) { $feature.Priority } else { [int]::MaxValue }
|
||||||
|
OriginalIndex = $featureIndex
|
||||||
|
}
|
||||||
|
$featureIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sort by priority first, then by original index for items with same/no priority
|
||||||
|
$sortedItems = $categoryItems | Sort-Object -Property Priority, OriginalIndex
|
||||||
|
|
||||||
|
# Render sorted items
|
||||||
|
foreach ($item in $sortedItems) {
|
||||||
|
if ($item.Type -eq 'group') {
|
||||||
|
$group = $item.Data
|
||||||
|
$items = @('No Change') + ($group.Values | ForEach-Object { $_.Label })
|
||||||
|
$comboName = 'Group_{0}Combo' -f $group.GroupId
|
||||||
|
$combo = CreateLabeledCombo -parent $panel -labelText $group.Label -comboName $comboName -items $items
|
||||||
|
# attach tooltip from UiGroups if present
|
||||||
|
if ($group.ToolTip) {
|
||||||
|
$tipBlock = New-Object System.Windows.Controls.TextBlock
|
||||||
|
$tipBlock.Text = $group.ToolTip
|
||||||
|
$tipBlock.TextWrapping = 'Wrap'
|
||||||
|
$tipBlock.MaxWidth = 420
|
||||||
|
$combo.ToolTip = $tipBlock
|
||||||
|
$lblBorderObj = $null
|
||||||
|
try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {}
|
||||||
|
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
|
||||||
|
}
|
||||||
|
$script:UiControlMappings[$comboName] = @{ Type = 'group'; Values = $group.Values; Label = $group.Label; Category = $categoryName }
|
||||||
|
}
|
||||||
|
elseif ($item.Type -eq 'feature') {
|
||||||
|
$feature = $item.Data
|
||||||
|
$opt = 'Apply'
|
||||||
|
if ($feature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($feature.FeatureId -match '^Enable') { $opt = 'Enable' }
|
||||||
|
$items = @('No Change', $opt)
|
||||||
|
$comboName = ("Feature_{0}_Combo" -f $feature.FeatureId) -replace '[^a-zA-Z0-9_]', ''
|
||||||
|
$combo = CreateLabeledCombo -parent $panel -labelText $feature.Label -comboName $comboName -items $items
|
||||||
|
# attach tooltip from Features.json if present, and include the disabled-state reason
|
||||||
|
if ($feature.ToolTip -or $feature.DisableWhenApplied -eq $true) {
|
||||||
|
$tooltipText = $feature.ToolTip
|
||||||
|
if ($feature.DisableWhenApplied -eq $true) {
|
||||||
|
$tooltipText = "This tweak is already applied and cannot be undone automatically. Visit the Win11Debloat wiki for instructions on how to manually revert this change."
|
||||||
|
}
|
||||||
|
|
||||||
|
$tipBlock = New-Object System.Windows.Controls.TextBlock
|
||||||
|
$tipBlock.Text = $tooltipText
|
||||||
|
$tipBlock.TextWrapping = 'Wrap'
|
||||||
|
$tipBlock.MaxWidth = 420
|
||||||
|
$combo.ToolTip = $tipBlock
|
||||||
|
[System.Windows.Controls.ToolTipService]::SetShowOnDisabled($combo, $true)
|
||||||
|
$lblBorderObj = $null
|
||||||
|
try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {}
|
||||||
|
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
|
||||||
|
}
|
||||||
|
$script:UiControlMappings[$comboName] = @{ Type = 'feature'; FeatureId = $feature.FeatureId; Label = $feature.Label; Category = $categoryName }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build a feature-label lookup so GenerateOverview can resolve feature IDs without reloading JSON
|
||||||
|
$script:FeatureLabelLookup = @{}
|
||||||
|
$script:UndoFeatureLabelLookup = @{}
|
||||||
|
foreach ($f in $featuresJson.Features) {
|
||||||
|
$script:FeatureLabelLookup[$f.FeatureId] = $f.Label
|
||||||
|
$script:UndoFeatureLabelLookup[$f.FeatureId] = $f.UndoLabel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-CurrentTweakSystemState {
|
||||||
|
param(
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
[bool]$ApplyToUi
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $script:UiControlMappings) { return }
|
||||||
|
if (-not $script:Features) { return }
|
||||||
|
|
||||||
|
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0"
|
||||||
|
if (-not $featuresJson) { return }
|
||||||
|
|
||||||
|
$groupMap = @{}
|
||||||
|
if ($featuresJson.UiGroups) {
|
||||||
|
foreach ($g in $featuresJson.UiGroups) {
|
||||||
|
$groupMap[$g.GroupId] = $g
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($controlName in $script:UiControlMappings.Keys) {
|
||||||
|
$control = $Window.FindName($controlName)
|
||||||
|
if (-not $control) { continue }
|
||||||
|
$mapping = $script:UiControlMappings[$controlName]
|
||||||
|
|
||||||
|
if ($control -is [System.Windows.Controls.CheckBox] -and $mapping.Type -eq 'feature') {
|
||||||
|
$applied = $false
|
||||||
|
try { $applied = [bool](Test-FeatureApplied -FeatureId $mapping.FeatureId) } catch {}
|
||||||
|
$featureObj = $script:Features[$mapping.FeatureId]
|
||||||
|
$disableWhenApplied = $featureObj -and $featureObj.DisableWhenApplied -eq $true
|
||||||
|
Add-Member -InputObject $control -MemberType NoteProperty -Name 'SystemState' -Value $applied -Force
|
||||||
|
Add-Member -InputObject $control -MemberType NoteProperty -Name 'DisableWhenApplied' -Value $disableWhenApplied -Force
|
||||||
|
|
||||||
|
if ($ApplyToUi) {
|
||||||
|
$control.IsChecked = $applied
|
||||||
|
$control.IsEnabled = -not ($applied -and $disableWhenApplied)
|
||||||
|
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialState' -Value $applied -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($control -is [System.Windows.Controls.ComboBox] -and $mapping.Type -eq 'group') {
|
||||||
|
$groupId = $null
|
||||||
|
if ($controlName -match '^Group_(.+)Combo$') { $groupId = $matches[1] }
|
||||||
|
$activeIndex = 0
|
||||||
|
if ($groupId -and $groupMap.ContainsKey($groupId)) {
|
||||||
|
try { $activeIndex = Get-CurrentGroupActiveIndex -Group $groupMap[$groupId] } catch {}
|
||||||
|
}
|
||||||
|
Add-Member -InputObject $control -MemberType NoteProperty -Name 'SystemIndex' -Value $activeIndex -Force
|
||||||
|
|
||||||
|
if ($ApplyToUi) {
|
||||||
|
$control.SelectedIndex = $activeIndex
|
||||||
|
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialIndex' -Value $activeIndex -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Load-CurrentTweakStateIntoUI {
|
||||||
|
param([System.Windows.Window]$Window)
|
||||||
|
|
||||||
|
Update-CurrentTweakSystemState -Window $Window -ApplyToUi:$true
|
||||||
|
}
|
||||||
|
|
||||||
|
function Reset-TweaksToSystemState {
|
||||||
|
param(
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
[bool]$LoadSystemState
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $script:UiControlMappings) { return }
|
||||||
|
|
||||||
|
foreach ($controlName in $script:UiControlMappings.Keys) {
|
||||||
|
$control = $Window.FindName($controlName)
|
||||||
|
if (-not $control) { continue }
|
||||||
|
|
||||||
|
if ($control -is [System.Windows.Controls.CheckBox]) {
|
||||||
|
if ($LoadSystemState) {
|
||||||
|
# Set checkbox to the currently applied state from registry
|
||||||
|
$applied = if ($null -ne $control.PSObject.Properties['SystemState']) { [bool]$control.SystemState } else { $false }
|
||||||
|
$disableWhenApplied = $null -ne $control.PSObject.Properties['DisableWhenApplied'] -and [bool]$control.DisableWhenApplied
|
||||||
|
$control.IsChecked = $applied
|
||||||
|
$control.IsEnabled = -not ($applied -and $disableWhenApplied)
|
||||||
|
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialState' -Value $applied -Force
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# Clear the checkbox
|
||||||
|
$control.IsChecked = $false
|
||||||
|
$control.IsEnabled = $true
|
||||||
|
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialState' -Value $false -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($control -is [System.Windows.Controls.ComboBox]) {
|
||||||
|
if ($LoadSystemState) {
|
||||||
|
# Set combobox to the currently applied state from registry
|
||||||
|
$idx = if ($null -ne $control.PSObject.Properties['SystemIndex']) { [int]$control.SystemIndex } else { 0 }
|
||||||
|
$control.SelectedIndex = $idx
|
||||||
|
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialIndex' -Value $idx -Force
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# Reset to first item (No Change)
|
||||||
|
$control.SelectedIndex = 0
|
||||||
|
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialIndex' -Value 0 -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-TweaksResponsiveColumns {
|
||||||
|
param([System.Windows.Window]$Window)
|
||||||
|
|
||||||
|
$tweaksGrid = $Window.FindName('TweaksGrid')
|
||||||
|
$col0 = $Window.FindName('Column0Panel')
|
||||||
|
$col1 = $Window.FindName('Column1Panel')
|
||||||
|
$col2 = $Window.FindName('Column2Panel')
|
||||||
|
|
||||||
|
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 = @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Clear-TweakSelections {
|
||||||
|
param([System.Windows.Window]$Window)
|
||||||
|
|
||||||
|
if (-not $script:UiControlMappings) { return }
|
||||||
|
|
||||||
|
foreach ($controlName in $script:UiControlMappings.Keys) {
|
||||||
|
$control = $Window.FindName($controlName)
|
||||||
|
if ($control -is [System.Windows.Controls.CheckBox]) {
|
||||||
|
$control.IsChecked = $false
|
||||||
|
$control.IsEnabled = $true
|
||||||
|
}
|
||||||
|
elseif ($control -is [System.Windows.Controls.ComboBox]) {
|
||||||
|
$control.SelectedIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Clear-TweakHighlights {
|
||||||
|
param([System.Windows.Window]$Window)
|
||||||
|
|
||||||
|
$col0 = $Window.FindName('Column0Panel')
|
||||||
|
$col1 = $Window.FindName('Column1Panel')
|
||||||
|
$col2 = $Window.FindName('Column2Panel')
|
||||||
|
$columns = @($col0, $col1, $col2) | Where-Object { $_ -ne $null }
|
||||||
|
foreach ($column in $columns) {
|
||||||
|
foreach ($card in $column.Children) {
|
||||||
|
if ($card -is [System.Windows.Controls.Border] -and $card.Child -is [System.Windows.Controls.StackPanel]) {
|
||||||
|
foreach ($control in $card.Child.Children) {
|
||||||
|
if ($control -is [System.Windows.Controls.CheckBox] -or
|
||||||
|
($control -is [System.Windows.Controls.Border] -and $control.Name -like '*_LabelBorder')) {
|
||||||
|
$control.Background = [System.Windows.Media.Brushes]::Transparent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-ComboBoxContainsMatch {
|
||||||
|
param ([System.Windows.Controls.ComboBox]$ComboBox, [string]$SearchText)
|
||||||
|
|
||||||
|
foreach ($item in $ComboBox.Items) {
|
||||||
|
$itemText = if ($item -is [System.Windows.Controls.ComboBoxItem]) { $item.Content.ToString().ToLower() } else { $item.ToString().ToLower() }
|
||||||
|
if ($itemText.Contains($SearchText)) { return $true }
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
215
Scripts/GUI/MainWindow-WindowChrome.ps1
Normal file
215
Scripts/GUI/MainWindow-WindowChrome.ps1
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
# MainWindow-WindowChrome.ps1
|
||||||
|
# Window sizing, DPI-aware coordinate conversion, maximized-window taskbar-constraint helpers, and UI animations.
|
||||||
|
|
||||||
|
function Register-MaximizedWindowHelper {
|
||||||
|
if (-not ([System.Management.Automation.PSTypeName]'Win11Debloat.MaximizedWindowHelper').Type) {
|
||||||
|
Add-Type -Namespace Win11Debloat -Name MaximizedWindowHelper `
|
||||||
|
-ReferencedAssemblies 'PresentationFramework','System.Windows.Forms','System.Drawing' `
|
||||||
|
-MemberDefinition @'
|
||||||
|
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
|
||||||
|
private struct MINMAXINFO {
|
||||||
|
public POINT ptReserved, ptMaxSize, ptMaxPosition, ptMinTrackSize, ptMaxTrackSize;
|
||||||
|
}
|
||||||
|
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
|
||||||
|
private struct POINT { public int x, y; }
|
||||||
|
|
||||||
|
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
||||||
|
private static extern System.IntPtr MonitorFromWindow(System.IntPtr hwnd, uint dwFlags);
|
||||||
|
|
||||||
|
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)]
|
||||||
|
private static extern bool GetMonitorInfo(System.IntPtr hMonitor, ref MONITORINFO lpmi);
|
||||||
|
|
||||||
|
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
|
||||||
|
private struct RECT {
|
||||||
|
public int Left, Top, Right, Bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
|
||||||
|
private struct MONITORINFO {
|
||||||
|
public int cbSize;
|
||||||
|
public RECT rcMonitor;
|
||||||
|
public RECT rcWork;
|
||||||
|
public uint dwFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static System.IntPtr WmGetMinMaxInfoHook(
|
||||||
|
System.IntPtr hwnd, int msg, System.IntPtr wParam, System.IntPtr lParam, ref bool handled) {
|
||||||
|
if (msg == 0x0024) { // WM_GETMINMAXINFO
|
||||||
|
var mmi = (MINMAXINFO)System.Runtime.InteropServices.Marshal.PtrToStructure(
|
||||||
|
lParam, typeof(MINMAXINFO));
|
||||||
|
|
||||||
|
const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
|
||||||
|
var monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
|
||||||
|
var monitorInfo = new MONITORINFO();
|
||||||
|
monitorInfo.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(MONITORINFO));
|
||||||
|
|
||||||
|
if (monitor != System.IntPtr.Zero && GetMonitorInfo(monitor, ref monitorInfo)) {
|
||||||
|
mmi.ptMaxPosition.x = monitorInfo.rcWork.Left - monitorInfo.rcMonitor.Left;
|
||||||
|
mmi.ptMaxPosition.y = monitorInfo.rcWork.Top - monitorInfo.rcMonitor.Top;
|
||||||
|
mmi.ptMaxSize.x = monitorInfo.rcWork.Right - monitorInfo.rcWork.Left;
|
||||||
|
mmi.ptMaxSize.y = monitorInfo.rcWork.Bottom - monitorInfo.rcWork.Top;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var screen = System.Windows.Forms.Screen.FromHandle(hwnd);
|
||||||
|
var wa = screen.WorkingArea;
|
||||||
|
var bounds = screen.Bounds;
|
||||||
|
mmi.ptMaxPosition.x = wa.Left - bounds.Left;
|
||||||
|
mmi.ptMaxPosition.y = wa.Top - bounds.Top;
|
||||||
|
mmi.ptMaxSize.x = wa.Width;
|
||||||
|
mmi.ptMaxSize.y = wa.Height;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Runtime.InteropServices.Marshal.StructureToPtr(mmi, lParam, true);
|
||||||
|
}
|
||||||
|
return System.IntPtr.Zero;
|
||||||
|
}
|
||||||
|
'@
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert screen-pixel coordinates to WPF device-independent pixels (DIP)
|
||||||
|
function ConvertTo-ScreenPointToDip {
|
||||||
|
param(
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
[double]$X,
|
||||||
|
[double]$Y
|
||||||
|
)
|
||||||
|
|
||||||
|
$source = [System.Windows.PresentationSource]::FromVisual($Window)
|
||||||
|
if ($null -eq $source -or $null -eq $source.CompositionTarget) {
|
||||||
|
return [System.Windows.Point]::new($X, $Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $source.CompositionTarget.TransformFromDevice.Transform([System.Windows.Point]::new($X, $Y))
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert screen-pixel size to WPF device-independent size
|
||||||
|
function ConvertTo-ScreenPixelsToDip {
|
||||||
|
param(
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
[double]$Width,
|
||||||
|
[double]$Height
|
||||||
|
)
|
||||||
|
|
||||||
|
$topLeft = ConvertTo-ScreenPointToDip -Window $Window -X 0 -Y 0
|
||||||
|
$bottomRight = ConvertTo-ScreenPointToDip -Window $Window -X $Width -Y $Height
|
||||||
|
return [System.Windows.Size]::new($bottomRight.X - $topLeft.X, $bottomRight.Y - $topLeft.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the screen that currently contains the window
|
||||||
|
function Get-WindowScreen {
|
||||||
|
param([System.Windows.Window]$Window)
|
||||||
|
|
||||||
|
$hwnd = (New-Object System.Windows.Interop.WindowInteropHelper($Window)).Handle
|
||||||
|
if ($hwnd -eq [IntPtr]::Zero) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
return [System.Windows.Forms.Screen]::FromHandle($hwnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update window border/corner chrome when transitioning between Normal and Maximized
|
||||||
|
function Update-MainWindowChrome {
|
||||||
|
param(
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
[System.Windows.Controls.Border]$MainBorder,
|
||||||
|
[System.Windows.Controls.Border]$TitleBarBackground,
|
||||||
|
[object]$NormalWindowShadow
|
||||||
|
)
|
||||||
|
|
||||||
|
$windowStateMaximized = [System.Windows.WindowState]::Maximized
|
||||||
|
$chrome = [System.Windows.Shell.WindowChrome]::GetWindowChrome($Window)
|
||||||
|
|
||||||
|
if ($Window.WindowState -eq $windowStateMaximized) {
|
||||||
|
$MainBorder.Margin = [System.Windows.Thickness]::new(0)
|
||||||
|
$MainBorder.BorderThickness = [System.Windows.Thickness]::new(0)
|
||||||
|
$MainBorder.CornerRadius = [System.Windows.CornerRadius]::new(0)
|
||||||
|
$MainBorder.Effect = $null
|
||||||
|
$TitleBarBackground.CornerRadius = [System.Windows.CornerRadius]::new(0)
|
||||||
|
# Zero out resize borders when maximized so the entire title bar row is draggable
|
||||||
|
if ($chrome) { $chrome.ResizeBorderThickness = [System.Windows.Thickness]::new(0) }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$MainBorder.Margin = [System.Windows.Thickness]::new(0)
|
||||||
|
$MainBorder.BorderThickness = [System.Windows.Thickness]::new(1)
|
||||||
|
$MainBorder.CornerRadius = [System.Windows.CornerRadius]::new(8)
|
||||||
|
$MainBorder.Effect = $NormalWindowShadow
|
||||||
|
$TitleBarBackground.CornerRadius = [System.Windows.CornerRadius]::new(8, 8, 0, 0)
|
||||||
|
if ($chrome) { $chrome.ResizeBorderThickness = [System.Windows.Thickness]::new(5) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set the initial window size and center on screen (normal state only)
|
||||||
|
function Set-MainWindowInitialSize {
|
||||||
|
param(
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
[double]$InitialNormalMaxWidth = 1400.0
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($Window.WindowState -ne [System.Windows.WindowState]::Normal) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$screen = Get-WindowScreen -Window $Window
|
||||||
|
if ($null -eq $screen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$workingAreaTopLeftDip = ConvertTo-ScreenPointToDip -Window $Window -X $screen.WorkingArea.Left -Y $screen.WorkingArea.Top
|
||||||
|
$workingAreaDip = ConvertTo-ScreenPixelsToDip -Window $Window -Width $screen.WorkingArea.Width -Height $screen.WorkingArea.Height
|
||||||
|
$Window.Width = [Math]::Min($InitialNormalMaxWidth, $workingAreaDip.Width)
|
||||||
|
$Window.Left = $workingAreaTopLeftDip.X + (($workingAreaDip.Width - $Window.Width) / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update the content grid margin to constrain max content width
|
||||||
|
function Update-MainWindowContentMargin {
|
||||||
|
param(
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
[System.Windows.Controls.Grid]$ContentGrid,
|
||||||
|
[double]$MaxContentWidth = 1600.0
|
||||||
|
)
|
||||||
|
|
||||||
|
$w = $Window.ActualWidth
|
||||||
|
if ($w -gt $MaxContentWidth) {
|
||||||
|
$gutter = [Math]::Floor(($w - $MaxContentWidth) / 2)
|
||||||
|
$ContentGrid.Margin = [System.Windows.Thickness]::new($gutter, 0, $gutter, 0)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$ContentGrid.Margin = [System.Windows.Thickness]::new(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Vertically center the home content panel
|
||||||
|
function Update-MainWindowHomeContentPosition {
|
||||||
|
param(
|
||||||
|
[System.Windows.Window]$Window,
|
||||||
|
[System.Windows.Controls.Panel]$HomeContentPanel
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($HomeContentPanel) {
|
||||||
|
$availableHeight = $Window.ActualHeight - 32 # subtract title bar height
|
||||||
|
if ($availableHeight -gt 0) {
|
||||||
|
$topMargin = ($availableHeight - 584) * 0.5
|
||||||
|
$HomeContentPanel.Margin = [System.Windows.Thickness]::new(0, $topMargin, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Start-DropdownArrowAnimation {
|
||||||
|
param(
|
||||||
|
[System.Windows.Controls.TextBlock]$Arrow,
|
||||||
|
[double]$Angle
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $Arrow) { return }
|
||||||
|
|
||||||
|
$animation = New-Object System.Windows.Media.Animation.DoubleAnimation
|
||||||
|
$animation.To = $Angle
|
||||||
|
$animation.Duration = [System.Windows.Duration]::new([System.TimeSpan]::FromMilliseconds(200))
|
||||||
|
|
||||||
|
$ease = New-Object System.Windows.Media.Animation.CubicEase
|
||||||
|
$ease.EasingMode = 'EaseOut'
|
||||||
|
$animation.EasingFunction = $ease
|
||||||
|
|
||||||
|
$Arrow.RenderTransform.BeginAnimation([System.Windows.Media.RotateTransform]::AngleProperty, $animation)
|
||||||
|
}
|
||||||
@@ -79,16 +79,70 @@ function Test-RestoreDialogFeatureVisibleInOverview {
|
|||||||
return -not [string]::IsNullOrWhiteSpace([string]$featureDefinition.Category)
|
return -not [string]::IsNullOrWhiteSpace([string]$featureDefinition.Category)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-SelectedForwardFeatureIdsFromBackup {
|
||||||
|
param($SelectedBackup)
|
||||||
|
|
||||||
|
$selectedFeatureIds = New-Object System.Collections.Generic.List[string]
|
||||||
|
$seenSelectedFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
|
||||||
|
foreach ($featureId in @($SelectedBackup.SelectedFeatures)) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$featureId)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedId = [string]$featureId
|
||||||
|
if ($seenSelectedFeatureIds.Add($normalizedId)) {
|
||||||
|
$selectedFeatureIds.Add($normalizedId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return @($selectedFeatureIds.ToArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SelectedUndoFeatureIdsFromBackup {
|
||||||
|
param($SelectedBackup)
|
||||||
|
|
||||||
|
$selectedUndoFeatureIds = New-Object System.Collections.Generic.List[string]
|
||||||
|
$seenUndoFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
|
||||||
|
foreach ($featureId in @($SelectedBackup.SelectedUndoFeatures)) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$featureId)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedId = [string]$featureId
|
||||||
|
if ($seenUndoFeatureIds.Add($normalizedId)) {
|
||||||
|
$selectedUndoFeatureIds.Add($normalizedId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return @($selectedUndoFeatureIds.ToArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-CombinedSelectedFeatureIdsFromBackup {
|
||||||
|
param($SelectedBackup)
|
||||||
|
|
||||||
|
$featureIds = New-Object System.Collections.Generic.List[string]
|
||||||
|
$seenIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
|
||||||
|
foreach ($featureId in @(Get-SelectedForwardFeatureIdsFromBackup -SelectedBackup $SelectedBackup) + @(Get-SelectedUndoFeatureIdsFromBackup -SelectedBackup $SelectedBackup)) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$featureId)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedId = [string]$featureId
|
||||||
|
if ($seenIds.Add($normalizedId)) {
|
||||||
|
$featureIds.Add($normalizedId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return @($featureIds.ToArray())
|
||||||
|
}
|
||||||
|
|
||||||
function Get-SelectedFeatureIdsFromBackup {
|
function Get-SelectedFeatureIdsFromBackup {
|
||||||
param($SelectedBackup)
|
param($SelectedBackup)
|
||||||
|
|
||||||
return @(
|
return @(Get-CombinedSelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
|
||||||
foreach ($featureId in @($SelectedBackup.SelectedFeatures)) {
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace([string]$featureId)) {
|
|
||||||
[string]$featureId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-RestoreBackupFeatureLists {
|
function Get-RestoreBackupFeatureLists {
|
||||||
|
|||||||
@@ -130,13 +130,9 @@ function Show-AppSelectionWindow {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($selectedApps -contains "Microsoft.WindowsStore" -and -not $Silent) {
|
if (-not (ConfirmUnsafeAppRemoval -SelectedApps $selectedApps -Owner $window)) {
|
||||||
$result = Show-MessageBox -Message 'Are you sure you wish to uninstall the Microsoft Store? This app cannot easily be reinstalled.' -Title 'Are you sure?' -Button 'YesNo' -Icon 'Warning' -Owner $window
|
|
||||||
|
|
||||||
if ($result -eq 'No') {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
SaveCustomAppsListToFile -appsList $selectedApps
|
SaveCustomAppsListToFile -appsList $selectedApps
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ function Show-ApplyModal {
|
|||||||
try {
|
try {
|
||||||
ExecuteAllChanges
|
ExecuteAllChanges
|
||||||
|
|
||||||
|
$registryImportFailureCount = [int]$script:RegistryImportFailures
|
||||||
|
|
||||||
# Restart explorer if requested
|
# Restart explorer if requested
|
||||||
if ($RestartExplorer -and -not $script:CancelRequested) {
|
if ($RestartExplorer -and -not $script:CancelRequested) {
|
||||||
RestartExplorer
|
RestartExplorer
|
||||||
@@ -139,7 +141,7 @@ function Show-ApplyModal {
|
|||||||
Write-Host ""
|
Write-Host ""
|
||||||
if ($script:CancelRequested) {
|
if ($script:CancelRequested) {
|
||||||
Write-Host "Script execution was cancelled by the user. Some changes may not have been applied."
|
Write-Host "Script execution was cancelled by the user. Some changes may not have been applied."
|
||||||
} else {
|
} elseif ($registryImportFailureCount -eq 0) {
|
||||||
Write-Host "All changes have been applied successfully!"
|
Write-Host "All changes have been applied successfully!"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +155,11 @@ function Show-ApplyModal {
|
|||||||
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d"))
|
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d"))
|
||||||
$script:ApplyCompletionTitleEl.Text = "Cancelled"
|
$script:ApplyCompletionTitleEl.Text = "Cancelled"
|
||||||
$script:ApplyCompletionMessageEl.Text = "Script execution was cancelled by the user."
|
$script:ApplyCompletionMessageEl.Text = "Script execution was cancelled by the user."
|
||||||
|
} elseif ($registryImportFailureCount -gt 0) {
|
||||||
|
$script:ApplyCompletionIconEl.Text = [char]0xE7BA
|
||||||
|
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d"))
|
||||||
|
$script:ApplyCompletionTitleEl.Text = "Changes Applied with Errors"
|
||||||
|
$script:ApplyCompletionMessageEl.Text = "$registryImportFailureCount registry change(s) failed. See console for details."
|
||||||
} else {
|
} else {
|
||||||
$script:ApplyCompletionTitleEl.Text = "Changes Applied"
|
$script:ApplyCompletionTitleEl.Text = "Changes Applied"
|
||||||
|
|
||||||
@@ -179,7 +186,7 @@ function Show-ApplyModal {
|
|||||||
$applyRebootPanel.Visibility = 'Visible'
|
$applyRebootPanel.Visibility = 'Visible'
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$script:ApplyCompletionMessageEl.Text = "Your clean system is ready. Thanks for using Win11Debloat!"
|
$script:ApplyCompletionMessageEl.Text = "Your system is ready. Thanks for using Win11Debloat!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -70,6 +70,9 @@ function Show-RestoreBackupDialog {
|
|||||||
$backupCreatedText = $window.FindName('BackupCreatedText')
|
$backupCreatedText = $window.FindName('BackupCreatedText')
|
||||||
$backupTargetText = $window.FindName('BackupTargetText')
|
$backupTargetText = $window.FindName('BackupTargetText')
|
||||||
$featuresItemsControl = $window.FindName('FeaturesItemsControl')
|
$featuresItemsControl = $window.FindName('FeaturesItemsControl')
|
||||||
|
$reappliedSeparator = $window.FindName('ReappliedSeparator')
|
||||||
|
$reappliedPanel = $window.FindName('ReappliedPanel')
|
||||||
|
$reappliedFeaturesItemsControl = $window.FindName('ReappliedFeaturesItemsControl')
|
||||||
$nonRevertibleSeparator = $window.FindName('NonRevertibleSeparator')
|
$nonRevertibleSeparator = $window.FindName('NonRevertibleSeparator')
|
||||||
$nonRevertiblePanel = $window.FindName('NonRevertiblePanel')
|
$nonRevertiblePanel = $window.FindName('NonRevertiblePanel')
|
||||||
$nonRevertibleFeaturesItemsControl = $window.FindName('NonRevertibleFeaturesItemsControl')
|
$nonRevertibleFeaturesItemsControl = $window.FindName('NonRevertibleFeaturesItemsControl')
|
||||||
@@ -119,6 +122,8 @@ function Show-RestoreBackupDialog {
|
|||||||
|
|
||||||
$overviewFeaturesSection.Visibility = 'Collapsed'
|
$overviewFeaturesSection.Visibility = 'Collapsed'
|
||||||
$overviewSummaryText.Visibility = 'Visible'
|
$overviewSummaryText.Visibility = 'Visible'
|
||||||
|
$reappliedSeparator.Visibility = 'Collapsed'
|
||||||
|
$reappliedPanel.Visibility = 'Collapsed'
|
||||||
$nonRevertibleSeparator.Visibility = 'Collapsed'
|
$nonRevertibleSeparator.Visibility = 'Collapsed'
|
||||||
$nonRevertiblePanel.Visibility = 'Collapsed'
|
$nonRevertiblePanel.Visibility = 'Collapsed'
|
||||||
$introInfoPanel.Visibility = 'Collapsed'
|
$introInfoPanel.Visibility = 'Collapsed'
|
||||||
@@ -215,13 +220,33 @@ function Show-RestoreBackupDialog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$selectedFeatureIds = Get-SelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup
|
$selectedForwardFeatureIds = @(Get-SelectedForwardFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
|
||||||
$featureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds $selectedFeatureIds -Features $script:Features
|
$selectedUndoFeatureIds = @(Get-SelectedUndoFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
|
||||||
$revertibleFeaturesList = @($featureLists.Revertible)
|
|
||||||
$nonRevertibleFeaturesList = @($featureLists.NonRevertible)
|
|
||||||
Write-Host "Backup overview prepared. Revertible=$($revertibleFeaturesList.Count), NonRevertible=$($nonRevertibleFeaturesList.Count)"
|
|
||||||
|
|
||||||
if ($revertibleFeaturesList.Count -eq 0) {
|
$seenForwardFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
foreach ($featureId in $selectedForwardFeatureIds) {
|
||||||
|
[void]$seenForwardFeatureIds.Add([string]$featureId)
|
||||||
|
}
|
||||||
|
|
||||||
|
$filteredUndoFeatureIds = New-Object System.Collections.Generic.List[string]
|
||||||
|
foreach ($featureId in $selectedUndoFeatureIds) {
|
||||||
|
if ($seenForwardFeatureIds.Contains([string]$featureId)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$filteredUndoFeatureIds.Add([string]$featureId)
|
||||||
|
}
|
||||||
|
|
||||||
|
$forwardFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds $selectedForwardFeatureIds -Features $script:Features
|
||||||
|
$undoFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds @($filteredUndoFeatureIds.ToArray()) -Features $script:Features
|
||||||
|
$combinedFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds (Get-SelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup) -Features $script:Features
|
||||||
|
|
||||||
|
$revertibleFeaturesList = @($forwardFeatureLists.Revertible)
|
||||||
|
$reappliedFeaturesList = @($undoFeatureLists.Revertible)
|
||||||
|
$nonRevertibleFeaturesList = @($combinedFeatureLists.NonRevertible)
|
||||||
|
Write-Host "Backup overview prepared. Reverted=$($revertibleFeaturesList.Count), ReApplied=$($reappliedFeaturesList.Count), NonRevertible=$($nonRevertibleFeaturesList.Count)"
|
||||||
|
|
||||||
|
if ($revertibleFeaturesList.Count -eq 0 -and $reappliedFeaturesList.Count -eq 0) {
|
||||||
throw 'The selected backup does not contain any changes that can be restored.'
|
throw 'The selected backup does not contain any changes that can be restored.'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,13 +254,16 @@ function Show-RestoreBackupDialog {
|
|||||||
$backupCreatedText.Text = $createdText
|
$backupCreatedText.Text = $createdText
|
||||||
$backupTargetText.Text = GetFriendlyRegistryBackupTarget -Target ([string]$SelectedBackup.Target)
|
$backupTargetText.Text = GetFriendlyRegistryBackupTarget -Target ([string]$SelectedBackup.Target)
|
||||||
$featuresItemsControl.ItemsSource = $revertibleFeaturesList
|
$featuresItemsControl.ItemsSource = $revertibleFeaturesList
|
||||||
$overviewFeaturesSection.Visibility = 'Visible'
|
$overviewFeaturesSection.Visibility = if ($revertibleFeaturesList.Count -gt 0) { 'Visible' } else { 'Collapsed' }
|
||||||
|
$reappliedFeaturesItemsControl.ItemsSource = $reappliedFeaturesList
|
||||||
|
if ($reappliedFeaturesList.Count -gt 0) { $reappliedPanel.Visibility = 'Visible' } else { $reappliedPanel.Visibility = 'Collapsed' }
|
||||||
|
if ($revertibleFeaturesList.Count -gt 0 -and $reappliedFeaturesList.Count -gt 0) { $reappliedSeparator.Visibility = 'Visible' } else { $reappliedSeparator.Visibility = 'Collapsed' }
|
||||||
$overviewSummaryText.Visibility = 'Collapsed'
|
$overviewSummaryText.Visibility = 'Collapsed'
|
||||||
$nonRevertibleFeaturesItemsControl.ItemsSource = $nonRevertibleFeaturesList
|
$nonRevertibleFeaturesItemsControl.ItemsSource = $nonRevertibleFeaturesList
|
||||||
|
|
||||||
$hasNonRevertibleItems = ($nonRevertibleFeaturesList.Count -gt 0)
|
$hasNonRevertibleItems = ($nonRevertibleFeaturesList.Count -gt 0)
|
||||||
if ($hasNonRevertibleItems) { $nonRevertiblePanel.Visibility = 'Visible' } else { $nonRevertiblePanel.Visibility = 'Collapsed' }
|
if ($hasNonRevertibleItems) { $nonRevertiblePanel.Visibility = 'Visible' } else { $nonRevertiblePanel.Visibility = 'Collapsed' }
|
||||||
if ($hasNonRevertibleItems) { $nonRevertibleSeparator.Visibility = 'Visible' } else { $nonRevertibleSeparator.Visibility = 'Collapsed' }
|
if ($hasNonRevertibleItems -and ($revertibleFeaturesList.Count -gt 0 -or $reappliedFeaturesList.Count -gt 0)) { $nonRevertibleSeparator.Visibility = 'Visible' } else { $nonRevertibleSeparator.Visibility = 'Collapsed' }
|
||||||
$introInfoPanel.Visibility = 'Collapsed'
|
$introInfoPanel.Visibility = 'Collapsed'
|
||||||
$overviewPanel.Visibility = 'Visible'
|
$overviewPanel.Visibility = 'Visible'
|
||||||
|
|
||||||
@@ -255,7 +283,7 @@ function Show-RestoreBackupDialog {
|
|||||||
|
|
||||||
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
|
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
|
||||||
$openDialog.Title = 'Select Registry Backup File'
|
$openDialog.Title = 'Select Registry Backup File'
|
||||||
$openDialog.Filter = 'Registry backup (*.json)|*.json|All files (*.*)|*.*'
|
$openDialog.Filter = 'Registry backup (*.json)|*.json'
|
||||||
$openDialog.DefaultExt = '.json'
|
$openDialog.DefaultExt = '.json'
|
||||||
$openDialog.InitialDirectory = $script:RegistryBackupsPath
|
$openDialog.InitialDirectory = $script:RegistryBackupsPath
|
||||||
|
|
||||||
@@ -295,6 +323,30 @@ function Show-RestoreBackupDialog {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (-not $useManualBackupFile) {
|
||||||
|
$autoBackupExists = $false
|
||||||
|
if ($scope -eq 'AllUsers') {
|
||||||
|
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
|
||||||
|
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
|
||||||
|
foreach ($startMenuPath in $usersStartMenuPaths) {
|
||||||
|
if (Test-Path -LiteralPath (Join-Path $startMenuPath.FullName 'start2.bin.bak')) {
|
||||||
|
$autoBackupExists = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$autoBackupPath = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin.bak"
|
||||||
|
$autoBackupExists = Test-Path -LiteralPath $autoBackupPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $autoBackupExists) {
|
||||||
|
$scopeText = (& $getStartMenuScopeInfo).SummaryText
|
||||||
|
Show-MessageBox -Owner $window -Title 'No Backup Found' -Message "No Start Menu backup file was found. You can uncheck the 'Automatically find Start Menu backup' option to select a backup file manually." -Button 'OK' -Icon 'Warning' | Out-Null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$window.Tag = @{
|
$window.Tag = @{
|
||||||
Result = 'RestoreStartMenu'
|
Result = 'RestoreStartMenu'
|
||||||
StartMenuScope = $scope
|
StartMenuScope = $scope
|
||||||
|
|||||||
@@ -7,10 +7,15 @@ function Show-RestoreBackupWindow {
|
|||||||
try {
|
try {
|
||||||
Write-Host 'Opening restore backup dialog.'
|
Write-Host 'Opening restore backup dialog.'
|
||||||
|
|
||||||
|
$restoreResult = [PSCustomObject]@{
|
||||||
|
RestoredRegistry = $false
|
||||||
|
RestoredStartMenu = $false
|
||||||
|
}
|
||||||
|
|
||||||
$dialogResult = Show-RestoreBackupDialog -Owner $Owner
|
$dialogResult = Show-RestoreBackupDialog -Owner $Owner
|
||||||
if (-not $dialogResult -or $dialogResult.Result -eq 'Cancel') {
|
if (-not $dialogResult -or $dialogResult.Result -eq 'Cancel') {
|
||||||
Write-Host 'Restore canceled by user.'
|
Write-Host 'Restore canceled by user.'
|
||||||
return
|
return $restoreResult
|
||||||
}
|
}
|
||||||
|
|
||||||
$successMessage = $null
|
$successMessage = $null
|
||||||
@@ -24,7 +29,8 @@ function Show-RestoreBackupWindow {
|
|||||||
|
|
||||||
Write-Host "User confirmed registry restore for $($backup.Target)."
|
Write-Host "User confirmed registry restore for $($backup.Target)."
|
||||||
Restore-RegistryBackupState -Backup $backup
|
Restore-RegistryBackupState -Backup $backup
|
||||||
$successMessage = 'Registry backup restored successfully. Please restart your computer for all changes to take effect.'
|
$restoreResult.RestoredRegistry = $true
|
||||||
|
$successMessage = 'Registry backup restored successfully. Some changes may require a restart to take effect.'
|
||||||
}
|
}
|
||||||
elseif ($dialogResult.Result -eq 'RestoreStartMenu') {
|
elseif ($dialogResult.Result -eq 'RestoreStartMenu') {
|
||||||
$scope = $dialogResult.StartMenuScope
|
$scope = $dialogResult.StartMenuScope
|
||||||
@@ -69,6 +75,8 @@ function Show-RestoreBackupWindow {
|
|||||||
$successMessage = "The Start Menu backup was successfully restored for the current user. The changes will apply the next time you sign in."
|
$successMessage = "The Start Menu backup was successfully restored for the current user. The changes will apply the next time you sign in."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$restoreResult.RestoredStartMenu = $true
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($warningMessage) {
|
if ($warningMessage) {
|
||||||
@@ -79,10 +87,16 @@ function Show-RestoreBackupWindow {
|
|||||||
Write-Host "$successMessage"
|
Write-Host "$successMessage"
|
||||||
Show-MessageBox -Title 'Backup Restored' -Message $successMessage -Icon Success
|
Show-MessageBox -Title 'Backup Restored' -Message $successMessage -Icon Success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $restoreResult
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
$errorMessage = if ($_.Exception.Message) { $_.Exception.Message } else { 'An unexpected error occurred.' }
|
$errorMessage = if ($_.Exception.Message) { $_.Exception.Message } else { 'An unexpected error occurred.' }
|
||||||
Write-Error "Restore operation failed: $errorMessage"
|
Write-Error "Restore operation failed: $errorMessage"
|
||||||
Show-MessageBox -Title 'Error' -Message "Restore failed: $errorMessage" -Icon Error
|
Show-MessageBox -Title 'Error' -Message "Restore failed: $errorMessage" -Icon Error
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
RestoredRegistry = $false
|
||||||
|
RestoredStartMenu = $false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ param (
|
|||||||
[switch]$RemoveApps,
|
[switch]$RemoveApps,
|
||||||
[switch]$RemoveAppsCustom,
|
[switch]$RemoveAppsCustom,
|
||||||
[switch]$RemoveGamingApps,
|
[switch]$RemoveGamingApps,
|
||||||
[switch]$RemoveCommApps,
|
|
||||||
[switch]$RemoveHPApps,
|
[switch]$RemoveHPApps,
|
||||||
[switch]$RemoveW11Outlook,
|
|
||||||
[switch]$ForceRemoveEdge,
|
[switch]$ForceRemoveEdge,
|
||||||
[switch]$DisableDVR,
|
[switch]$DisableDVR,
|
||||||
[switch]$DisableGameBarIntegration,
|
[switch]$DisableGameBarIntegration,
|
||||||
@@ -58,7 +56,7 @@ param (
|
|||||||
[switch]$HideSearchTb, [switch]$ShowSearchIconTb, [switch]$ShowSearchLabelTb, [switch]$ShowSearchBoxTb,
|
[switch]$HideSearchTb, [switch]$ShowSearchIconTb, [switch]$ShowSearchLabelTb, [switch]$ShowSearchBoxTb,
|
||||||
[switch]$HideTaskview,
|
[switch]$HideTaskview,
|
||||||
[switch]$DisableStartRecommended,
|
[switch]$DisableStartRecommended,
|
||||||
[switch]$DisableStartAllApps,
|
[switch]$DisableStartAllApps, [switch]$StartAllAppsCategory, [switch]$StartAllAppsGrid, [switch]$StartAllAppsList,
|
||||||
[switch]$DisableStartPhoneLink,
|
[switch]$DisableStartPhoneLink,
|
||||||
[switch]$DisableCopilot,
|
[switch]$DisableCopilot,
|
||||||
[switch]$DisableRecall,
|
[switch]$DisableRecall,
|
||||||
@@ -135,12 +133,12 @@ catch {
|
|||||||
Exit
|
Exit
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Output ""
|
# Remove old script folder if it exists, but keep configs, logs and backups
|
||||||
Write-Output "> Cleaning up old Win11Debloat folder..."
|
|
||||||
|
|
||||||
# Remove old script folder if it exists, but keep config and log files
|
|
||||||
if (Test-Path $tempWorkPath) {
|
if (Test-Path $tempWorkPath) {
|
||||||
Get-ChildItem -Path $tempWorkPath -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Config,Logs,Backups | Remove-Item -Recurse -Force
|
Write-Output ""
|
||||||
|
Write-Output "> Cleaning up old Win11Debloat folder..."
|
||||||
|
|
||||||
|
Get-ChildItem -Path $tempWorkPath -Exclude Config,Logs,Backups | Remove-Item -Recurse -Force
|
||||||
}
|
}
|
||||||
|
|
||||||
$configDir = Join-Path $tempWorkPath 'Config'
|
$configDir = Join-Path $tempWorkPath 'Config'
|
||||||
@@ -148,6 +146,9 @@ $backupDir = Join-Path $tempWorkPath 'ConfigOld'
|
|||||||
|
|
||||||
# Temporarily move existing config files if they exist to prevent them from being overwritten by the new script files, will be moved back after the new script is unpacked
|
# Temporarily move existing config files if they exist to prevent them from being overwritten by the new script files, will be moved back after the new script is unpacked
|
||||||
if (Test-Path "$configDir") {
|
if (Test-Path "$configDir") {
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output "> Backing up existing config files..."
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null
|
New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null
|
||||||
|
|
||||||
$filesToKeep = @(
|
$filesToKeep = @(
|
||||||
@@ -178,6 +179,9 @@ if (Test-Path "$backupDir") {
|
|||||||
New-Item -ItemType Directory -Path "$configDir" -Force | Out-Null
|
New-Item -ItemType Directory -Path "$configDir" -Force | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output "> Restoring existing config files..."
|
||||||
|
|
||||||
Get-ChildItem -Path "$backupDir" -Recurse | Move-Item -Destination "$configDir"
|
Get-ChildItem -Path "$backupDir" -Recurse | Move-Item -Destination "$configDir"
|
||||||
Remove-Item "$backupDir" -Recurse -Force
|
Remove-Item "$backupDir" -Recurse -Force
|
||||||
}
|
}
|
||||||
@@ -218,13 +222,13 @@ if ($null -ne $debloatProcess) {
|
|||||||
$debloatProcess.WaitForExit()
|
$debloatProcess.WaitForExit()
|
||||||
}
|
}
|
||||||
|
|
||||||
# Remove all remaining script files, except for CustomAppsList and LastUsedSettings.json files
|
# Remove all remaining script files, except for configs, logs and backups
|
||||||
if (Test-Path $tempWorkPath) {
|
if (Test-Path $tempWorkPath) {
|
||||||
Write-Output ""
|
Write-Output ""
|
||||||
Write-Output "> Cleaning up..."
|
Write-Output "> Cleaning up..."
|
||||||
|
|
||||||
# Cleanup, remove Win11Debloat directory
|
# Cleanup, remove Win11Debloat directory
|
||||||
Get-ChildItem -Path $tempWorkPath -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Win11Debloat-Run.log,Config,Logs,Backups | Remove-Item -Recurse -Force
|
Get-ChildItem -Path $tempWorkPath -Exclude Config,Logs,Backups | Remove-Item -Recurse -Force
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Output ""
|
Write-Output ""
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ param (
|
|||||||
[switch]$RemoveApps,
|
[switch]$RemoveApps,
|
||||||
[switch]$RemoveAppsCustom,
|
[switch]$RemoveAppsCustom,
|
||||||
[switch]$RemoveGamingApps,
|
[switch]$RemoveGamingApps,
|
||||||
[switch]$RemoveCommApps,
|
|
||||||
[switch]$RemoveHPApps,
|
[switch]$RemoveHPApps,
|
||||||
[switch]$RemoveW11Outlook,
|
|
||||||
[switch]$ForceRemoveEdge,
|
[switch]$ForceRemoveEdge,
|
||||||
[switch]$DisableDVR,
|
[switch]$DisableDVR,
|
||||||
[switch]$DisableGameBarIntegration,
|
[switch]$DisableGameBarIntegration,
|
||||||
@@ -58,7 +56,7 @@ param (
|
|||||||
[switch]$HideSearchTb, [switch]$ShowSearchIconTb, [switch]$ShowSearchLabelTb, [switch]$ShowSearchBoxTb,
|
[switch]$HideSearchTb, [switch]$ShowSearchIconTb, [switch]$ShowSearchLabelTb, [switch]$ShowSearchBoxTb,
|
||||||
[switch]$HideTaskview,
|
[switch]$HideTaskview,
|
||||||
[switch]$DisableStartRecommended,
|
[switch]$DisableStartRecommended,
|
||||||
[switch]$DisableStartAllApps,
|
[switch]$DisableStartAllApps, [switch]$StartAllAppsCategory, [switch]$StartAllAppsGrid, [switch]$StartAllAppsList,
|
||||||
[switch]$DisableStartPhoneLink,
|
[switch]$DisableStartPhoneLink,
|
||||||
[switch]$DisableCopilot,
|
[switch]$DisableCopilot,
|
||||||
[switch]$DisableRecall,
|
[switch]$DisableRecall,
|
||||||
@@ -136,12 +134,12 @@ catch {
|
|||||||
Exit
|
Exit
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Output ""
|
# Remove old script folder if it exists, but keep configs, logs and backups
|
||||||
Write-Output "> Cleaning up old Win11Debloat folder..."
|
|
||||||
|
|
||||||
# Remove old script folder if it exists, but keep config and log files
|
|
||||||
if (Test-Path $tempWorkPath) {
|
if (Test-Path $tempWorkPath) {
|
||||||
Get-ChildItem -Path $tempWorkPath -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Config,Logs,Backups | Remove-Item -Recurse -Force
|
Write-Output ""
|
||||||
|
Write-Output "> Cleaning up old Win11Debloat folder..."
|
||||||
|
|
||||||
|
Get-ChildItem -Path $tempWorkPath -Exclude Config,Logs,Backups | Remove-Item -Recurse -Force
|
||||||
}
|
}
|
||||||
|
|
||||||
$configDir = Join-Path $tempWorkPath 'Config'
|
$configDir = Join-Path $tempWorkPath 'Config'
|
||||||
@@ -149,6 +147,9 @@ $backupDir = Join-Path $tempWorkPath 'ConfigOld'
|
|||||||
|
|
||||||
# Temporarily move existing config files if they exist to prevent them from being overwritten by the new script files, will be moved back after the new script is unpacked
|
# Temporarily move existing config files if they exist to prevent them from being overwritten by the new script files, will be moved back after the new script is unpacked
|
||||||
if (Test-Path "$configDir") {
|
if (Test-Path "$configDir") {
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output "> Backing up existing config files..."
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null
|
New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null
|
||||||
|
|
||||||
$filesToKeep = @(
|
$filesToKeep = @(
|
||||||
@@ -171,7 +172,7 @@ Expand-Archive $tempArchivePath $tempWorkPath
|
|||||||
Remove-Item $tempArchivePath
|
Remove-Item $tempArchivePath
|
||||||
|
|
||||||
# Move files
|
# Move files
|
||||||
Get-ChildItem -Path (Join-Path $tempWorkPath 'Raphire-Win11Debloat-*') -Recurse | Move-Item -Destination $tempWorkPath
|
Get-ChildItem -Path (Join-Path $tempWorkPath '*Win11Debloat-*') -Recurse | Move-Item -Destination $tempWorkPath
|
||||||
|
|
||||||
# Add existing config files back to Config folder
|
# Add existing config files back to Config folder
|
||||||
if (Test-Path "$backupDir") {
|
if (Test-Path "$backupDir") {
|
||||||
@@ -179,6 +180,9 @@ if (Test-Path "$backupDir") {
|
|||||||
New-Item -ItemType Directory -Path "$configDir" -Force | Out-Null
|
New-Item -ItemType Directory -Path "$configDir" -Force | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output "> Restoring existing config files..."
|
||||||
|
|
||||||
Get-ChildItem -Path "$backupDir" -Recurse | Move-Item -Destination "$configDir"
|
Get-ChildItem -Path "$backupDir" -Recurse | Move-Item -Destination "$configDir"
|
||||||
Remove-Item "$backupDir" -Recurse -Force
|
Remove-Item "$backupDir" -Recurse -Force
|
||||||
}
|
}
|
||||||
@@ -219,13 +223,13 @@ if ($null -ne $debloatProcess) {
|
|||||||
$debloatProcess.WaitForExit()
|
$debloatProcess.WaitForExit()
|
||||||
}
|
}
|
||||||
|
|
||||||
# Remove all remaining script files, except for CustomAppsList and LastUsedSettings.json files
|
# Remove all remaining script files, except for configs, logs and backups
|
||||||
if (Test-Path $tempWorkPath) {
|
if (Test-Path $tempWorkPath) {
|
||||||
Write-Output ""
|
Write-Output ""
|
||||||
Write-Output "> Cleaning up..."
|
Write-Output "> Cleaning up..."
|
||||||
|
|
||||||
# Cleanup, remove Win11Debloat directory
|
# Cleanup, remove Win11Debloat directory
|
||||||
Get-ChildItem -Path $tempWorkPath -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Win11Debloat-Run.log,Config,Logs,Backups | Remove-Item -Recurse -Force
|
Get-ChildItem -Path $tempWorkPath -Exclude Config,Logs,Backups | Remove-Item -Recurse -Force
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Output ""
|
Write-Output ""
|
||||||
|
|||||||
223
Scripts/Helpers/ApplyRegistryRegFile.ps1
Normal file
223
Scripts/Helpers/ApplyRegistryRegFile.ps1
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
function Get-NormalizedRegistryValueName {
|
||||||
|
param(
|
||||||
|
[AllowNull()]
|
||||||
|
$ValueName
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrEmpty([string]$ValueName)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return [string]$ValueName
|
||||||
|
}
|
||||||
|
|
||||||
|
function Convert-RegOperationToValueKind {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Operation
|
||||||
|
)
|
||||||
|
|
||||||
|
$valueName = if ([string]::IsNullOrEmpty([string]$Operation.ValueName)) { '' } else { [string]$Operation.ValueName }
|
||||||
|
$valueType = [string]$Operation.ValueType
|
||||||
|
$operationKeyPath = [string]$Operation.KeyPath
|
||||||
|
|
||||||
|
switch ($valueType) {
|
||||||
|
'DWord' {
|
||||||
|
$unsigned = [uint32]$Operation.ValueData
|
||||||
|
$value = [BitConverter]::ToInt32([BitConverter]::GetBytes($unsigned), 0)
|
||||||
|
return @{ Name = $valueName; Kind = [Microsoft.Win32.RegistryValueKind]::DWord; Value = $value }
|
||||||
|
}
|
||||||
|
'String' {
|
||||||
|
return @{ Name = $valueName; Kind = [Microsoft.Win32.RegistryValueKind]::String; Value = [string]$Operation.ValueData }
|
||||||
|
}
|
||||||
|
'Binary' {
|
||||||
|
return @{ Name = $valueName; Kind = [Microsoft.Win32.RegistryValueKind]::Binary; Value = [byte[]]$Operation.ValueData }
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
throw "Unsupported value type '$valueType' while applying reg operation for '$operationKeyPath'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Remove-RegistrySubKeyTreeIfExists {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[Microsoft.Win32.RegistryKey]$RootKey,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$SubKeyPath
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
$RootKey.DeleteSubKeyTree($SubKeyPath, $false)
|
||||||
|
}
|
||||||
|
catch [System.UnauthorizedAccessException], [System.Security.SecurityException] {
|
||||||
|
throw
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# Best-effort cleanup only; missing keys are fine.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RegistryKeyForOperation {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$RegistryPath,
|
||||||
|
[switch]$CreateIfMissing,
|
||||||
|
[bool]$OpenKey = $true
|
||||||
|
)
|
||||||
|
|
||||||
|
$parts = Split-RegistryPath -path $RegistryPath
|
||||||
|
if (-not $parts) {
|
||||||
|
throw "Unsupported registry path: $RegistryPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$rootKey = Get-RegistryRootKey -hiveName $parts.Hive
|
||||||
|
if (-not $rootKey) {
|
||||||
|
throw "Unsupported registry hive '$($parts.Hive)' in path '$RegistryPath'"
|
||||||
|
}
|
||||||
|
|
||||||
|
$subKeyPath = $parts.SubKey
|
||||||
|
if ([string]::IsNullOrWhiteSpace($subKeyPath)) {
|
||||||
|
return [PSCustomObject]@{ RootKey = $rootKey; SubKeyPath = $null; Key = $rootKey }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $OpenKey) {
|
||||||
|
return [PSCustomObject]@{ RootKey = $rootKey; SubKeyPath = $subKeyPath; Key = $null }
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = if ($CreateIfMissing) {
|
||||||
|
$rootKey.CreateSubKey($subKeyPath)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$rootKey.OpenSubKey($subKeyPath, $true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [PSCustomObject]@{ RootKey = $rootKey; SubKeyPath = $subKeyPath; Key = $key }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-RegistryDeleteValueOperation {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Operation,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$KeyInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $KeyInfo.Key) {
|
||||||
|
$valueName = Get-NormalizedRegistryValueName -ValueName $Operation.ValueName
|
||||||
|
$displayValueName = if ([string]::IsNullOrEmpty($valueName)) { '(Default)' } else { $valueName }
|
||||||
|
Write-Verbose "Unable to find or open key '$($Operation.KeyPath)' and value '$displayValueName'"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$valueName = Get-NormalizedRegistryValueName -ValueName $Operation.ValueName
|
||||||
|
$KeyInfo.Key.DeleteValue($valueName, $false)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$KeyInfo.Key.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-RegistrySetValueOperation {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Operation,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$KeyInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $KeyInfo.Key) {
|
||||||
|
throw [System.UnauthorizedAccessException]::new("Unable to open or create registry key '$($Operation.KeyPath)'")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$setArgs = Convert-RegOperationToValueKind -Operation $Operation
|
||||||
|
$KeyInfo.Key.SetValue($setArgs.Name, $setArgs.Value, $setArgs.Kind)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$KeyInfo.Key.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-RegistryOperationAccessDeniedWarning {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Operation,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$ExceptionMessage
|
||||||
|
)
|
||||||
|
|
||||||
|
$keyPath = [string]$Operation.KeyPath
|
||||||
|
$operationType = [string]$Operation.OperationType
|
||||||
|
|
||||||
|
if ($operationType -eq 'SetValue' -or $operationType -eq 'DeleteValue') {
|
||||||
|
$valueName = Get-NormalizedRegistryValueName -ValueName $Operation.ValueName
|
||||||
|
$displayValueName = if ([string]::IsNullOrEmpty($valueName)) { '(Default)' } else { $valueName }
|
||||||
|
Write-Warning "Skipping operation '$operationType' on key '$keyPath' value '$displayValueName' due to access restrictions: $ExceptionMessage"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Warning "Skipping operation '$operationType' on key '$keyPath' due to access restrictions: $ExceptionMessage"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-RegistryOperation {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Operation,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$RegFilePath
|
||||||
|
)
|
||||||
|
|
||||||
|
$operationType = [string]$Operation.OperationType
|
||||||
|
$isSetValueOperation = $operationType -eq 'SetValue'
|
||||||
|
$isDeleteKeyOperation = $operationType -eq 'DeleteKey'
|
||||||
|
|
||||||
|
$keyInfo = Get-RegistryKeyForOperation -RegistryPath $Operation.KeyPath -CreateIfMissing:$isSetValueOperation -OpenKey:(-not $isDeleteKeyOperation)
|
||||||
|
|
||||||
|
switch ($operationType) {
|
||||||
|
'DeleteKey' {
|
||||||
|
if ($null -ne $keyInfo.SubKeyPath) {
|
||||||
|
Remove-RegistrySubKeyTreeIfExists -RootKey $keyInfo.RootKey -SubKeyPath $keyInfo.SubKeyPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'DeleteValue' {
|
||||||
|
Invoke-RegistryDeleteValueOperation -Operation $Operation -KeyInfo $keyInfo
|
||||||
|
}
|
||||||
|
'SetValue' {
|
||||||
|
Invoke-RegistrySetValueOperation -Operation $Operation -KeyInfo $keyInfo
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
throw "Unsupported reg operation type '$($Operation.OperationType)' in '$RegFilePath'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-RegistryOperationsFromRegFile {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$RegFilePath
|
||||||
|
)
|
||||||
|
|
||||||
|
$accessDeniedCount = 0
|
||||||
|
$operations = @(Get-RegFileOperations -regFilePath $RegFilePath)
|
||||||
|
$totalOperations = $operations.Count
|
||||||
|
|
||||||
|
foreach ($operation in $operations) {
|
||||||
|
try {
|
||||||
|
Invoke-RegistryOperation -Operation $operation -RegFilePath $RegFilePath
|
||||||
|
}
|
||||||
|
catch [System.UnauthorizedAccessException], [System.Security.SecurityException] {
|
||||||
|
$accessDeniedCount++
|
||||||
|
Write-RegistryOperationAccessDeniedWarning -Operation $operation -ExceptionMessage $_.Exception.Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($totalOperations -gt 0 -and $accessDeniedCount -eq $totalOperations) {
|
||||||
|
throw "Registry fallback import could not apply any operations in '$RegFilePath' because all $accessDeniedCount operation(s) were blocked by access restrictions."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($accessDeniedCount -gt 0) {
|
||||||
|
Write-Warning "Registry fallback import completed with $accessDeniedCount access-restricted operation(s) skipped in '$RegFilePath'."
|
||||||
|
}
|
||||||
|
}
|
||||||
34
Scripts/Helpers/ConfirmUnsafeAppRemoval.ps1
Normal file
34
Scripts/Helpers/ConfirmUnsafeAppRemoval.ps1
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Shows confirmation dialogs for apps that require extra caution before removal.
|
||||||
|
# Returns $true if the user confirmed all warnings (or if no warnings were triggered),
|
||||||
|
# $false if the user declined any warning.
|
||||||
|
function ConfirmUnsafeAppRemoval {
|
||||||
|
param (
|
||||||
|
[string[]]$SelectedApps,
|
||||||
|
$Owner = $null
|
||||||
|
)
|
||||||
|
|
||||||
|
# Skip all warnings in Silent mode
|
||||||
|
if ($Silent) {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Microsoft Store warning
|
||||||
|
if ($SelectedApps -contains "Microsoft.WindowsStore") {
|
||||||
|
$result = Show-MessageBox -Message 'Are you sure that you wish to uninstall the Microsoft Store? This app cannot easily be reinstalled.' -Title 'Are you sure?' -Button 'YesNo' -Icon 'Warning' -Owner $Owner
|
||||||
|
|
||||||
|
if ($result -eq 'No') {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Windows Terminal warning
|
||||||
|
if ($SelectedApps -contains "Microsoft.WindowsTerminal") {
|
||||||
|
$result = Show-MessageBox -Message 'Are you sure that you wish to remove Windows Terminal? Windows Terminal is the default command-line app for Windows. Ensure you are not running Win11Debloat via Windows Terminal before proceeding to avoid a mid-process failure.' -Title 'Are you sure?' -Button 'YesNo' -Icon 'Warning' -Owner $Owner
|
||||||
|
|
||||||
|
if ($result -eq 'No') {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $true
|
||||||
|
}
|
||||||
@@ -129,10 +129,13 @@ function Convert-RegValueData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($valueData -match '^"(?<value>.*)"$') {
|
if ($valueData -match '^"(?<value>.*)"$') {
|
||||||
|
$stringValue = $matches.value
|
||||||
|
# Unescape registry string escape sequences
|
||||||
|
$stringValue = $stringValue -replace '\\"', '"' -replace '\\\\', '\'
|
||||||
return [PSCustomObject]@{
|
return [PSCustomObject]@{
|
||||||
OperationType = 'SetValue'
|
OperationType = 'SetValue'
|
||||||
ValueType = 'String'
|
ValueType = 'String'
|
||||||
ValueData = $matches.value
|
ValueData = $stringValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,16 +27,19 @@ function Split-RegistryPath {
|
|||||||
$null
|
$null
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($hiveName.Equals('HKEY_USERS', [System.StringComparison]::OrdinalIgnoreCase) -and -not [string]::IsNullOrWhiteSpace($normalizedSubKey)) {
|
if ($hiveName.Equals('HKEY_USERS', [System.StringComparison]::OrdinalIgnoreCase) -and
|
||||||
|
-not [string]::IsNullOrWhiteSpace($normalizedSubKey) -and
|
||||||
|
-not [string]::IsNullOrWhiteSpace([string]$script:RegistryTargetHiveMountName)) {
|
||||||
if ($normalizedSubKey -match '^(?<mount>[^\\]+)(?:\\(?<rest>.*))?$') {
|
if ($normalizedSubKey -match '^(?<mount>[^\\]+)(?:\\(?<rest>.*))?$') {
|
||||||
$mountName = [string]$matches.mount
|
$mountName = [string]$matches.mount
|
||||||
if ($mountName.Equals('.DEFAULT', [System.StringComparison]::OrdinalIgnoreCase)) {
|
if ($mountName.Equals('Default', [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
$remainingSubKey = if ($matches.rest) { [string]$matches.rest } else { '' }
|
$remainingSubKey = if ($matches.rest) { [string]$matches.rest } else { '' }
|
||||||
|
$targetMountName = [string]$script:RegistryTargetHiveMountName
|
||||||
if ([string]::IsNullOrWhiteSpace($remainingSubKey)) {
|
if ([string]::IsNullOrWhiteSpace($remainingSubKey)) {
|
||||||
$normalizedSubKey = 'Default'
|
$normalizedSubKey = $targetMountName
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$normalizedSubKey = "Default\$remainingSubKey"
|
$normalizedSubKey = "$targetMountName\$remainingSubKey"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,14 +70,14 @@ function Get-RegistryRootKey {
|
|||||||
function Get-RegistryFilePathForFeature {
|
function Get-RegistryFilePathForFeature {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
$Feature,
|
[string]$RegistryKey,
|
||||||
[switch]$UseSysprepRegFiles
|
[switch]$UseSysprepRegFiles
|
||||||
)
|
)
|
||||||
|
|
||||||
$useSysprepLayout = $UseSysprepRegFiles -or $script:Params.ContainsKey('Sysprep') -or $script:Params.ContainsKey('User')
|
$useSysprepLayout = $UseSysprepRegFiles -or $script:Params.ContainsKey('Sysprep') -or $script:Params.ContainsKey('User')
|
||||||
if ($useSysprepLayout) {
|
if ($useSysprepLayout) {
|
||||||
return Join-Path (Join-Path $script:RegfilesPath 'Sysprep') $Feature.RegistryKey
|
return Join-Path (Join-Path $script:RegfilesPath 'Sysprep') $RegistryKey
|
||||||
}
|
}
|
||||||
|
|
||||||
return Join-Path $script:RegfilesPath $Feature.RegistryKey
|
return Join-Path $script:RegfilesPath $RegistryKey
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,14 +31,6 @@ function Test-TargetUserName {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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]@{
|
return [PSCustomObject]@{
|
||||||
IsValid = $true
|
IsValid = $true
|
||||||
UserName = $normalizedUserName
|
UserName = $normalizedUserName
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
function TestIfUserIsLoggedIn {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$Username
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
|
||||||
$quserOutput = @(& quser 2>$null)
|
|
||||||
if ($LASTEXITCODE -ne 0 -or -not $quserOutput) {
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($line in ($quserOutput | Select-Object -Skip 1)) {
|
|
||||||
if ([string]::IsNullOrWhiteSpace($line)) { continue }
|
|
||||||
|
|
||||||
# Remove current-session marker and split columns.
|
|
||||||
$normalizedLine = $line.TrimStart('>', ' ')
|
|
||||||
$parts = $normalizedLine -split '\s+'
|
|
||||||
if ($parts.Count -eq 0) { continue }
|
|
||||||
|
|
||||||
$sessionUser = $parts[0]
|
|
||||||
if ([string]::IsNullOrWhiteSpace($sessionUser)) { continue }
|
|
||||||
|
|
||||||
# Normalize possible DOMAIN\user or user@domain formats.
|
|
||||||
if ($sessionUser.Contains('\')) {
|
|
||||||
$sessionUser = ($sessionUser -split '\\')[-1]
|
|
||||||
}
|
|
||||||
if ($sessionUser.Contains('@')) {
|
|
||||||
$sessionUser = ($sessionUser -split '@')[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($sessionUser.Equals($Username, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
152
Scripts/Helpers/UserHiveHelpers.ps1
Normal file
152
Scripts/Helpers/UserHiveHelpers.ps1
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
function New-TargetUserHiveContext {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$TargetUserName,
|
||||||
|
[AllowNull()]
|
||||||
|
[object]$UserContext,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$HiveDatPath,
|
||||||
|
[AllowNull()]
|
||||||
|
[string]$MountName,
|
||||||
|
[bool]$WasAlreadyLoaded = $false,
|
||||||
|
[bool]$WasLoadedByScript = $false
|
||||||
|
)
|
||||||
|
|
||||||
|
$effectiveMountName = if ([string]::IsNullOrWhiteSpace($MountName)) { 'Default' } else { $MountName }
|
||||||
|
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
TargetUserName = $TargetUserName
|
||||||
|
UserSid = if ($UserContext) { $UserContext.UserSid } else { $null }
|
||||||
|
ProfilePath = if ($UserContext) { $UserContext.ProfilePath } else { $null }
|
||||||
|
HiveDatPath = $HiveDatPath
|
||||||
|
MountName = $effectiveMountName
|
||||||
|
WasAlreadyLoaded = $WasAlreadyLoaded
|
||||||
|
WasLoadedByScript = $WasLoadedByScript
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-TargetUserHiveContext {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$TargetUserName
|
||||||
|
)
|
||||||
|
|
||||||
|
$normalizedTargetUserName = NormalizeUserLookupValue -Value $TargetUserName
|
||||||
|
if ([string]::IsNullOrWhiteSpace($normalizedTargetUserName)) {
|
||||||
|
throw 'Target user name for registry hive resolution is empty.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$userContext = ResolveUserProfileContext -UserName $normalizedTargetUserName
|
||||||
|
if (-not $userContext -or [string]::IsNullOrWhiteSpace([string]$userContext.ProfilePath)) {
|
||||||
|
throw "Unable to resolve profile path for target user '$normalizedTargetUserName'."
|
||||||
|
}
|
||||||
|
|
||||||
|
$hiveDatPath = Join-Path $userContext.ProfilePath 'NTUSER.DAT'
|
||||||
|
if (-not (Test-Path -LiteralPath $hiveDatPath)) {
|
||||||
|
throw "Unable to find target user hive at '$hiveDatPath'."
|
||||||
|
}
|
||||||
|
|
||||||
|
$isDefaultProfile = $normalizedTargetUserName.Equals('Default', [System.StringComparison]::OrdinalIgnoreCase)
|
||||||
|
$userSid = if ($userContext) { [string]$userContext.UserSid } else { '' }
|
||||||
|
|
||||||
|
if ((-not $isDefaultProfile) -and (-not [string]::IsNullOrWhiteSpace($userSid))) {
|
||||||
|
$loadedHivePath = "Registry::HKEY_USERS\$userSid"
|
||||||
|
if (Test-Path -LiteralPath $loadedHivePath) {
|
||||||
|
return (New-TargetUserHiveContext `
|
||||||
|
-TargetUserName $normalizedTargetUserName `
|
||||||
|
-UserContext $userContext `
|
||||||
|
-HiveDatPath $hiveDatPath `
|
||||||
|
-MountName $userSid `
|
||||||
|
-WasAlreadyLoaded $true `
|
||||||
|
-WasLoadedByScript $false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (New-TargetUserHiveContext `
|
||||||
|
-TargetUserName $normalizedTargetUserName `
|
||||||
|
-UserContext $userContext `
|
||||||
|
-HiveDatPath $hiveDatPath `
|
||||||
|
-MountName 'Default' `
|
||||||
|
-WasAlreadyLoaded $false `
|
||||||
|
-WasLoadedByScript $false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-LoadedTargetUserHiveContext {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$HiveContext
|
||||||
|
)
|
||||||
|
|
||||||
|
$userSid = [string]$HiveContext.UserSid
|
||||||
|
if ([string]::IsNullOrWhiteSpace($userSid)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$loadedHivePath = "Registry::HKEY_USERS\$userSid"
|
||||||
|
if (-not (Test-Path -LiteralPath $loadedHivePath)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (New-TargetUserHiveContext `
|
||||||
|
-TargetUserName $HiveContext.TargetUserName `
|
||||||
|
-UserContext ([PSCustomObject]@{ UserSid = $HiveContext.UserSid; ProfilePath = $HiveContext.ProfilePath }) `
|
||||||
|
-HiveDatPath $HiveContext.HiveDatPath `
|
||||||
|
-MountName $userSid `
|
||||||
|
-WasAlreadyLoaded $true `
|
||||||
|
-WasLoadedByScript $false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-WithTargetUserHive {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$TargetUserName,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[scriptblock]$ScriptBlock,
|
||||||
|
$ArgumentObject = $null,
|
||||||
|
[switch]$PassHiveContext
|
||||||
|
)
|
||||||
|
|
||||||
|
$hiveContext = Resolve-TargetUserHiveContext -TargetUserName $TargetUserName
|
||||||
|
$previousHiveMountName = $script:RegistryTargetHiveMountName
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (-not $hiveContext.WasAlreadyLoaded) {
|
||||||
|
$global:LASTEXITCODE = 0
|
||||||
|
reg load "HKU\$($hiveContext.MountName)" "$($hiveContext.HiveDatPath)" | Out-Null
|
||||||
|
$loadExitCode = $LASTEXITCODE
|
||||||
|
|
||||||
|
if ($loadExitCode -ne 0) {
|
||||||
|
$loadedSidContext = Resolve-LoadedTargetUserHiveContext -HiveContext $hiveContext
|
||||||
|
if ($loadedSidContext) {
|
||||||
|
$hiveContext = $loadedSidContext
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw "Failed to load target user hive '$($hiveContext.HiveDatPath)' (exit code: $loadExitCode)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$hiveContext.WasLoadedByScript = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:RegistryTargetHiveMountName = [string]$hiveContext.MountName
|
||||||
|
|
||||||
|
if ($PassHiveContext) {
|
||||||
|
return & $ScriptBlock $ArgumentObject $hiveContext
|
||||||
|
}
|
||||||
|
|
||||||
|
return & $ScriptBlock $ArgumentObject
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$script:RegistryTargetHiveMountName = $previousHiveMountName
|
||||||
|
|
||||||
|
if ($hiveContext -and $hiveContext.WasLoadedByScript) {
|
||||||
|
$global:LASTEXITCODE = 0
|
||||||
|
reg unload "HKU\$($hiveContext.MountName)" | Out-Null
|
||||||
|
$unloadExitCode = $LASTEXITCODE
|
||||||
|
if ($unloadExitCode -ne 0) {
|
||||||
|
Write-Warning "Failed to unload registry hive 'HKU\$($hiveContext.MountName)' (exit code: $unloadExitCode)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,9 +17,7 @@ param (
|
|||||||
[switch]$RemoveApps,
|
[switch]$RemoveApps,
|
||||||
[switch]$RemoveAppsCustom,
|
[switch]$RemoveAppsCustom,
|
||||||
[switch]$RemoveGamingApps,
|
[switch]$RemoveGamingApps,
|
||||||
[switch]$RemoveCommApps,
|
|
||||||
[switch]$RemoveHPApps,
|
[switch]$RemoveHPApps,
|
||||||
[switch]$RemoveW11Outlook,
|
|
||||||
[switch]$ForceRemoveEdge,
|
[switch]$ForceRemoveEdge,
|
||||||
[switch]$DisableDVR,
|
[switch]$DisableDVR,
|
||||||
[switch]$DisableGameBarIntegration,
|
[switch]$DisableGameBarIntegration,
|
||||||
@@ -59,7 +57,7 @@ param (
|
|||||||
[switch]$HideSearchTb, [switch]$ShowSearchIconTb, [switch]$ShowSearchLabelTb, [switch]$ShowSearchBoxTb,
|
[switch]$HideSearchTb, [switch]$ShowSearchIconTb, [switch]$ShowSearchLabelTb, [switch]$ShowSearchBoxTb,
|
||||||
[switch]$HideTaskview,
|
[switch]$HideTaskview,
|
||||||
[switch]$DisableStartRecommended,
|
[switch]$DisableStartRecommended,
|
||||||
[switch]$DisableStartAllApps,
|
[switch]$DisableStartAllApps, [switch]$StartAllAppsCategory, [switch]$StartAllAppsGrid, [switch]$StartAllAppsList,
|
||||||
[switch]$DisableStartPhoneLink,
|
[switch]$DisableStartPhoneLink,
|
||||||
[switch]$DisableCopilot,
|
[switch]$DisableCopilot,
|
||||||
[switch]$DisableRecall,
|
[switch]$DisableRecall,
|
||||||
@@ -141,7 +139,7 @@ if (-not $isAdmin) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Define script-level variables & paths
|
# Define script-level variables & paths
|
||||||
$script:Version = "2026.05.11"
|
$script:Version = "2026.06.11"
|
||||||
$configPath = Join-Path $PSScriptRoot 'Config'
|
$configPath = Join-Path $PSScriptRoot 'Config'
|
||||||
$logsPath = Join-Path $PSScriptRoot 'Logs'
|
$logsPath = Join-Path $PSScriptRoot 'Logs'
|
||||||
$schemasPath = Join-Path $PSScriptRoot 'Schemas'
|
$schemasPath = Join-Path $PSScriptRoot 'Schemas'
|
||||||
@@ -293,6 +291,7 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
|||||||
. "$PSScriptRoot/Scripts/CLI/PrintHeader.ps1"
|
. "$PSScriptRoot/Scripts/CLI/PrintHeader.ps1"
|
||||||
|
|
||||||
# Features functions
|
# Features functions
|
||||||
|
. "$PSScriptRoot/Scripts/Features/GetCurrentTweakState.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Features/ExecuteChanges.ps1"
|
. "$PSScriptRoot/Scripts/Features/ExecuteChanges.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Features/CreateSystemRestorePoint.ps1"
|
. "$PSScriptRoot/Scripts/Features/CreateSystemRestorePoint.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Features/BackupRegistryFeatureSelection.ps1"
|
. "$PSScriptRoot/Scripts/Features/BackupRegistryFeatureSelection.ps1"
|
||||||
@@ -302,7 +301,7 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
|||||||
. "$PSScriptRoot/Scripts/Features/RestoreRegistryApplyState.ps1"
|
. "$PSScriptRoot/Scripts/Features/RestoreRegistryApplyState.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Features/RestoreRegistryBackup.ps1"
|
. "$PSScriptRoot/Scripts/Features/RestoreRegistryBackup.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Features/DisableStoreSearchSuggestions.ps1"
|
. "$PSScriptRoot/Scripts/Features/DisableStoreSearchSuggestions.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Features/EnableWindowsFeature.ps1"
|
. "$PSScriptRoot/Scripts/Features/WindowsOptionalFeatures.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Features/ImportRegistryFile.ps1"
|
. "$PSScriptRoot/Scripts/Features/ImportRegistryFile.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Features/ReplaceStartMenu.ps1"
|
. "$PSScriptRoot/Scripts/Features/ReplaceStartMenu.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Features/RestartExplorer.ps1"
|
. "$PSScriptRoot/Scripts/Features/RestartExplorer.ps1"
|
||||||
@@ -330,6 +329,11 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
|||||||
. "$PSScriptRoot/Scripts/GUI/Show-RestoreBackupWindow.ps1"
|
. "$PSScriptRoot/Scripts/GUI/Show-RestoreBackupWindow.ps1"
|
||||||
. "$PSScriptRoot/Scripts/GUI/RestoreBackupDialogFeatureLists.ps1"
|
. "$PSScriptRoot/Scripts/GUI/RestoreBackupDialogFeatureLists.ps1"
|
||||||
. "$PSScriptRoot/Scripts/GUI/Show-RestoreBackupDialog.ps1"
|
. "$PSScriptRoot/Scripts/GUI/Show-RestoreBackupDialog.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/GUI/MainWindow-WindowChrome.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/GUI/MainWindow-AppSelection.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/GUI/MainWindow-TweaksBuilder.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/GUI/MainWindow-Navigation.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/GUI/MainWindow-Deployment.ps1"
|
||||||
. "$PSScriptRoot/Scripts/GUI/Show-MainWindow.ps1"
|
. "$PSScriptRoot/Scripts/GUI/Show-MainWindow.ps1"
|
||||||
. "$PSScriptRoot/Scripts/GUI/Show-AboutDialog.ps1"
|
. "$PSScriptRoot/Scripts/GUI/Show-AboutDialog.ps1"
|
||||||
. "$PSScriptRoot/Scripts/GUI/Show-Bubble.ps1"
|
. "$PSScriptRoot/Scripts/GUI/Show-Bubble.ps1"
|
||||||
@@ -337,6 +341,7 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
|||||||
# Helper functions
|
# Helper functions
|
||||||
. "$PSScriptRoot/Scripts/Helpers/AddParameter.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/AddParameter.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Helpers/ResolveUserProfilePath.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/ResolveUserProfilePath.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/Helpers/UserHiveHelpers.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Helpers/CheckIfUserExists.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/CheckIfUserExists.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Helpers/CheckModernStandbySupport.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/CheckModernStandbySupport.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Helpers/GenerateAppsList.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/GenerateAppsList.ps1"
|
||||||
@@ -349,7 +354,8 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
|||||||
. "$PSScriptRoot/Scripts/Helpers/GetUserDirectory.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/GetUserDirectory.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Helpers/GetUserName.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/GetUserName.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Helpers/RegistryPathHelpers.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/RegistryPathHelpers.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Helpers/TestIfUserIsLoggedIn.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/ApplyRegistryRegFile.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/Helpers/ConfirmUnsafeAppRemoval.ps1"
|
||||||
|
|
||||||
# Threading functions
|
# Threading functions
|
||||||
. "$PSScriptRoot/Scripts/Threading/DoEvents.ps1"
|
. "$PSScriptRoot/Scripts/Threading/DoEvents.ps1"
|
||||||
@@ -372,6 +378,7 @@ $WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\Current
|
|||||||
$script:ModernStandbySupported = CheckModernStandbySupport
|
$script:ModernStandbySupported = CheckModernStandbySupport
|
||||||
|
|
||||||
$script:Params = $PSBoundParameters
|
$script:Params = $PSBoundParameters
|
||||||
|
$script:UndoParams = @{}
|
||||||
|
|
||||||
# Add default Apps parameter when RemoveApps is requested and Apps was not explicitly provided
|
# Add default Apps parameter when RemoveApps is requested and Apps was not explicitly provided
|
||||||
if ((-not $script:Params.ContainsKey("Apps")) -and $script:Params.ContainsKey("RemoveApps")) {
|
if ((-not $script:Params.ContainsKey("Apps")) -and $script:Params.ContainsKey("RemoveApps")) {
|
||||||
@@ -401,7 +408,7 @@ else {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($script:Params.ContainsKey("Sysprep")) {
|
if ($script:Params.ContainsKey("Sysprep")) {
|
||||||
$defaultUserPath = GetUserDirectory -userName "Default"
|
GetUserDirectory -userName "Default" | Out-Null
|
||||||
|
|
||||||
# Exit script if run in Sysprep mode on Windows 10
|
# Exit script if run in Sysprep mode on Windows 10
|
||||||
if ($WinVersion -lt 22000) {
|
if ($WinVersion -lt 22000) {
|
||||||
@@ -412,10 +419,10 @@ if ($script:Params.ContainsKey("Sysprep")) {
|
|||||||
|
|
||||||
# Ensure that target user exists, if User or AppRemovalTarget parameter was provided
|
# Ensure that target user exists, if User or AppRemovalTarget parameter was provided
|
||||||
if ($script:Params.ContainsKey("User")) {
|
if ($script:Params.ContainsKey("User")) {
|
||||||
$userPath = GetUserDirectory -userName $script:Params.Item("User")
|
GetUserDirectory -userName $script:Params.Item("User") | Out-Null
|
||||||
}
|
}
|
||||||
if ($script:Params.ContainsKey("AppRemovalTarget")) {
|
if ($script:Params.ContainsKey("AppRemovalTarget")) {
|
||||||
$userPath = GetUserDirectory -userName $script:Params.Item("AppRemovalTarget")
|
GetUserDirectory -userName $script:Params.Item("AppRemovalTarget") | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
# Remove LastUsedSettings.json file if it exists and is empty
|
# Remove LastUsedSettings.json file if it exists and is empty
|
||||||
|
|||||||
Reference in New Issue
Block a user