mirror of
https://github.com/Raphire/Win11Debloat.git
synced 2026-07-03 07:08:27 +00:00
Compare commits
41 Commits
2026.05.20
...
2026.06.24
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ee0126259 | ||
|
|
e23ecf36d6 | ||
|
|
32dc3d6bdf | ||
|
|
f76adc5054 | ||
|
|
95b583606d | ||
|
|
693b805114 | ||
|
|
5ebc50d36a | ||
|
|
d1fe541b62 | ||
|
|
71e3f2e44d | ||
|
|
4891aa401a | ||
|
|
a6d59c0dc1 | ||
|
|
ac54bde383 | ||
|
|
dfe7810346 | ||
|
|
82894176d9 | ||
|
|
87b3035eda | ||
|
|
a89b53504c | ||
|
|
91a6266d50 | ||
|
|
469751f8e8 | ||
|
|
908274a500 | ||
|
|
6e4a616f1c | ||
|
|
535b62db40 | ||
|
|
c039b04717 | ||
|
|
a7a46bb5bf | ||
|
|
a95b5adee8 | ||
|
|
2b97021341 | ||
|
|
1235306f80 | ||
|
|
1a69d19f30 | ||
|
|
5628f6e0b7 | ||
|
|
6f349b4992 | ||
|
|
2193591448 | ||
|
|
e9269c5501 | ||
|
|
fdac0a6d14 | ||
|
|
2aa9afaa2c | ||
|
|
67c9cc6ba3 | ||
|
|
157d26bb22 | ||
|
|
53ca51dffd | ||
|
|
db24865051 | ||
|
|
33b77f19a0 | ||
|
|
37872b2030 | ||
|
|
abfc5db2c3 | ||
|
|
1d828d6a78 |
106
.github/CONTRIBUTING.md
vendored
106
.github/CONTRIBUTING.md
vendored
@@ -1,6 +1,7 @@
|
||||
# How to Contribute?
|
||||
|
||||
We welcome contributions from the community. You can contribute to Win11Debloat by:
|
||||
|
||||
- Reporting issues and bugs [here](https://github.com/Raphire/Win11Debloat/issues/new?template=bug_report.yml)
|
||||
- Submitting feature requests [here](https://github.com/Raphire/Win11Debloat/issues/new?template=feature_request.yml)
|
||||
- Testing Win11Debloat
|
||||
@@ -15,6 +16,7 @@ You can help us test the latest changes and additions to the script. If you enco
|
||||
> The prerelease version of Win11Debloat is meant for developers to test the script. Don't use this in production environments!
|
||||
|
||||
You can launch the prerelease version of Win11Debloat by running this command:
|
||||
|
||||
```ps1
|
||||
& ([scriptblock]::Create((irm "https://debloat.raphi.re/dev")))
|
||||
```
|
||||
@@ -28,12 +30,14 @@ You can launch the prerelease version of Win11Debloat by running this command:
|
||||
1. **Fork the project** on GitHub by clicking the "Fork" button at the top right of the repository page.
|
||||
|
||||
2. **Clone the repository** to your local machine:
|
||||
|
||||
```powershell
|
||||
git clone https://github.com/YOUR-USERNAME/Win11Debloat.git
|
||||
cd Win11Debloat
|
||||
```
|
||||
|
||||
3. **Create a new branch** for your contribution:
|
||||
|
||||
```powershell
|
||||
git checkout -b feature/your-feature-name
|
||||
```
|
||||
@@ -42,11 +46,14 @@ You can launch the prerelease version of Win11Debloat by running this command:
|
||||
|
||||
1. Open PowerShell as an administrator
|
||||
2. Enable script execution if necessary:
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy Unrestricted -Scope Process -Force
|
||||
```
|
||||
|
||||
3. Navigate to your Win11Debloat directory
|
||||
4. Run the script:
|
||||
|
||||
```powershell
|
||||
.\Win11Debloat.ps1
|
||||
```
|
||||
@@ -57,18 +64,31 @@ You can launch the prerelease version of Win11Debloat by running this command:
|
||||
|
||||
Understanding the project structure is essential for contributing effectively:
|
||||
|
||||
```
|
||||
```text
|
||||
Win11Debloat/
|
||||
├── Win11Debloat.ps1 # Main PowerShell script
|
||||
├── Run.bat # Batch launcher for the quick launch method
|
||||
├── Scripts/ # Additional PowerShell scripts and functions
|
||||
│ └── Get.ps1 # Script used for the quick launch method to automatically download and run Win11debloat
|
||||
│ ├── Get.ps1 # Script used for the quick launch method to automatically download and run Win11debloat
|
||||
│ ├── AppRemoval/ # App package removal logic
|
||||
│ ├── CLI/ # Command-line interface helpers
|
||||
│ ├── Features/ # Feature apply/undo logic (e.g. InvokeChanges.ps1, ReplaceStartMenu.ps1)
|
||||
│ ├── FileIO/ # File input/output helpers
|
||||
│ ├── GUI/ # GUI window definitions and logic
|
||||
│ ├── Helpers/ # Shared helper functions
|
||||
│ └── Threading/ # Threading utilities
|
||||
├── Config/
|
||||
│ ├── Apps.json # List of supported apps for removal
|
||||
│ ├── DefaultSettings.json # Default configuration preset
|
||||
│ ├── Features.json # All features with metadata
|
||||
│ └── LastUsedSettings.json # Last used configuration (generated during use)
|
||||
├── Regfiles/ # Registry files for each feature
|
||||
└── Schemas/ # XAML Schemas for GUI elements
|
||||
├── Regfiles/ # Registry files for all features
|
||||
│ ├── Undo/ # Registry files for reverting features
|
||||
│ └── Sysprep/ # Registry files for Sysprep mode
|
||||
├── Schemas/ # XAML Schemas for GUI elements
|
||||
├── Assets/ # Static assets (icons, start menu templates)
|
||||
├── Backups/ # Registry backups (generated during use)
|
||||
└── Logs/ # Script logs (generated during use)
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
@@ -98,20 +118,20 @@ 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.
|
||||
|
||||
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**:
|
||||
- Main action files go in `Regfiles/`
|
||||
- Undo files go in `Regfiles/Undo/`
|
||||
- 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.
|
||||
5. **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.
|
||||
6. **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. **Missing Category**: Features without a `Category` field (set to `null`) won't appear in the GUI. This is intentional for command-line-only features, make sure this is what you want before submitting.
|
||||
|
||||
@@ -127,11 +147,13 @@ Avoid these common mistakes when contributing:
|
||||
To add a new app that can be removed via Win11Debloat:
|
||||
|
||||
1. **Find the AppId**: To find the correct AppId for an app:
|
||||
|
||||
```powershell
|
||||
Get-AppxPackage | Select-Object Name, PackageFullName
|
||||
```
|
||||
|
||||
2. **Edit `Config/Apps.json`**: Add a new entry to the `"Apps"` array:
|
||||
|
||||
```json
|
||||
{
|
||||
"FriendlyName": "Display Name",
|
||||
@@ -142,9 +164,10 @@ To add a new app that can be removed via Win11Debloat:
|
||||
```
|
||||
|
||||
3. **Follow the Guidelines**:
|
||||
- Use clear, user-friendly names for `FriendlyName`
|
||||
- Set `SelectedByDefault` to `true` only for apps that are largely considered bloatware, otherwise set to `false`
|
||||
- Provide a concise description explaining what the app does
|
||||
|
||||
- Use clear, user-friendly names for `FriendlyName`
|
||||
- Set `SelectedByDefault` to `true` only for apps that are largely considered bloatware, otherwise set to `false`
|
||||
- Provide a concise description explaining what the app does
|
||||
|
||||
### Adding a New Feature
|
||||
|
||||
@@ -162,6 +185,7 @@ Create new registry files in the `Regfiles/` directory:
|
||||
- **Sysprep file**: `Sysprep/Disable_YourFeature.reg` (for Sysprep mode)
|
||||
|
||||
Example registry file structure:
|
||||
|
||||
```reg
|
||||
Windows Registry Editor Version 5.00
|
||||
|
||||
@@ -170,6 +194,7 @@ Windows Registry Editor Version 5.00
|
||||
```
|
||||
|
||||
A Sysprep registry file should apply the same changes as the normal action. Replace the hive of registry keys that start with `HKEY_CURRENT_USER` with `hkey_users\default`. For example:
|
||||
|
||||
```reg
|
||||
Windows Registry Editor Version 5.00
|
||||
|
||||
@@ -179,7 +204,7 @@ Windows Registry Editor Version 5.00
|
||||
|
||||
#### 1b. Implement the Feature Logic
|
||||
|
||||
If your feature requires more than just applying a registry file, add custom logic to the main script in the appropriate section. In most cases this will involve creating a new entry in the `ExecuteParameter` function for your new feature.
|
||||
If your feature requires more than just applying a registry file, add custom logic to the main script in the appropriate section. In most cases this will involve creating a new entry in the `Invoke-FeatureApply` function (in `Scripts/Features/InvokeChanges.ps1`) for your new feature. If your feature also requires custom undo logic (beyond a simple registry file import), add a corresponding entry to the `Invoke-FeatureUndo` function in the same file.
|
||||
|
||||
#### 2. Add Feature to Features.json
|
||||
|
||||
@@ -192,35 +217,39 @@ Add your feature to the `"Features"` array in `Config/Features.json`:
|
||||
"ToolTip": "Detailed explanation of what this feature does and its impact.",
|
||||
"Category": "Privacy & Suggested Content",
|
||||
"Priority": 1,
|
||||
"Action": "Disable",
|
||||
"RegistryKey": "Disable_YourFeature.reg",
|
||||
"ApplyText": "Disabling your feature...",
|
||||
"UndoAction": "Enable",
|
||||
"ApplyText": "Disabling your feature",
|
||||
"UndoLabel": "Short description for the undo",
|
||||
"ApplyUndoText": "Enabling your feature",
|
||||
"RegistryUndoKey": "Enable_YourFeature.reg",
|
||||
"RequiresReboot": false,
|
||||
"DisableWhenApplied": false,
|
||||
"MinVersion": null,
|
||||
"MaxVersion": null
|
||||
}
|
||||
```
|
||||
|
||||
**Field Descriptions**:
|
||||
- `FeatureId`: Unique identifier (must match parameter name in Win11Debloat.ps1 and Get.ps1)
|
||||
- `Label`: Short description shown in the UI, written in a way to fit with the Action or UndoAction prefixed
|
||||
- `ToolTip`: Detailed explanation of what the feature does, used for tooltips in the GUI
|
||||
|
||||
- `FeatureId`: Unique identifier, this must match parameter name in the Win11Debloat.ps1 and Get.ps1 files.
|
||||
- `Label`: Short description shown in the UI and wiki documentation.
|
||||
- `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.
|
||||
- `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
|
||||
- `ApplyText`: Message shown when applying the feature
|
||||
- `UndoAction`: Action word for reverting (e.g., "Enable", "Show")
|
||||
- `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
|
||||
- `MinVersion`: Minimum Windows build version (e.g., "22000") or null
|
||||
- `MaxVersion`: Maximum Windows version or null
|
||||
- `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.
|
||||
- `UndoLabel`: Short description for the undo shown in the UI.
|
||||
- `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.
|
||||
- `RequiresReboot`: Optional boolean. Set to `true` if the feature requires a system reboot to take effect.
|
||||
- `DisableWhenApplied`: Optional boolean. Set to `true` if the feature has no supported undo method.
|
||||
- `MinVersion`: Minimum Windows build version (e.g., "22000") or null.
|
||||
- `MaxVersion`: Maximum Windows version or null.
|
||||
|
||||
#### 3. Add Command-Line Parameter
|
||||
|
||||
Add a corresponding parameter to both `Win11Debloat.ps1` AND `Scripts/Get.ps1`, the parameter name should match the FeatureId you have defined in `Features.json`. In most cases this will be a switch parameter, example:
|
||||
|
||||
```powershell
|
||||
[switch]$YourFeatureId,
|
||||
```
|
||||
@@ -233,12 +262,14 @@ Add a corresponding parameter to both `Win11Debloat.ps1` AND `Scripts/Get.ps1`,
|
||||
The default preset (`Config/DefaultSettings.json`) defines which features are automatically applied when users run Win11Debloat in "Default Mode" or with the `-RunDefaults` parameter. This preset should include features that are widely considered to improve the Windows experience without breaking functionality.
|
||||
|
||||
**When to add a feature to the default preset:**
|
||||
|
||||
- The feature removes obvious bloatware or distractions
|
||||
- The feature enhances privacy without breaking core functionality
|
||||
- The feature is generally non-controversial and beneficial to most users
|
||||
- The change can be easily reverted if needed
|
||||
|
||||
**When NOT to add a feature to the default preset:**
|
||||
|
||||
- The feature significantly changes core Windows behavior
|
||||
- The feature might break applications or workflows for some users
|
||||
- The feature is highly opinionated or preference-based
|
||||
@@ -254,10 +285,12 @@ To add your feature to the default preset, edit `Config/DefaultSettings.json` an
|
||||
```
|
||||
|
||||
**Field Descriptions**:
|
||||
|
||||
- `Name`: Must exactly match the `FeatureId` from Features.json
|
||||
- `Value`: Set to `true` to enable the feature in default mode
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "1.0",
|
||||
@@ -283,12 +316,13 @@ To add your feature to the default preset, edit `Config/DefaultSettings.json` an
|
||||
To add a new category for organizing features:
|
||||
|
||||
- Add a new category entry to the `"Categories"` array in `Config/Features.json`:
|
||||
```json
|
||||
{
|
||||
"Name": "Your Category Name",
|
||||
"Icon": "#### ;"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"Name": "Your Category Name",
|
||||
"Icon": "#### ;"
|
||||
}
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> Use [Segoe Fluent Icon Assets](https://learn.microsoft.com/en-us/windows/apps/design/iconography/segoe-fluent-icons-font) for icon codes.
|
||||
@@ -320,17 +354,20 @@ UI Groups allow features to be grouped together in the GUI with a combobox (drop
|
||||
## Submitting a Pull Request
|
||||
|
||||
1. **Commit your changes** with clear, descriptive commit messages:
|
||||
|
||||
```powershell
|
||||
git add .
|
||||
git commit -m "Add feature: Description of your changes"
|
||||
```
|
||||
|
||||
2. **Push to your fork**:
|
||||
|
||||
```powershell
|
||||
git push origin feature/your-feature-name
|
||||
```
|
||||
|
||||
3. **Create a Pull Request** on GitHub:
|
||||
|
||||
- Go to the original Win11Debloat repository
|
||||
- Click "New Pull Request"
|
||||
- Select your fork and branch
|
||||
@@ -342,6 +379,7 @@ UI Groups allow features to be grouped together in the GUI with a combobox (drop
|
||||
# Questions?
|
||||
|
||||
If you have questions about contributing, feel free to:
|
||||
|
||||
- Open a [discussion](https://github.com/Raphire/Win11Debloat/discussions)
|
||||
- Comment on an existing issue
|
||||
- Ask in your pull request
|
||||
- Ask in your pull request
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: "🐞 Bug report"
|
||||
description: "Report an issue you encountered"
|
||||
labels: ["bug"]
|
||||
labels: ["bug", "unconfirmed"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,7 +1,3 @@
|
||||
LastSettings
|
||||
SavedSettings
|
||||
LastUsedSettings.json
|
||||
CustomAppsList
|
||||
Logs/*
|
||||
Win11Debloat.log
|
||||
Backups/*
|
||||
497
Config/Apps.json
497
Config/Apps.json
File diff suppressed because it is too large
Load Diff
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/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.
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.
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.
Binary file not shown.
6
Run.bat
6
Run.bat
@@ -19,15 +19,17 @@ if exist "%wtDefaultPath%" (
|
||||
set "wtPath="
|
||||
)
|
||||
|
||||
set "SCRIPT_PATH=\"%~dp0Win11Debloat.ps1\""
|
||||
|
||||
:: Launch script
|
||||
if defined wtPath (
|
||||
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
|
||||
) else (
|
||||
echo Windows Terminal not found. Using default PowerShell instead...
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Message Text -->
|
||||
<Grid Grid.Row="0" Margin="20,4,20,20">
|
||||
<Grid Grid.Row="0" Margin="20,4,20,16">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
@@ -79,15 +79,15 @@
|
||||
<!-- Separator -->
|
||||
<Border Grid.Row="1"
|
||||
Height="1"
|
||||
Background="{DynamicResource BorderColor}"
|
||||
Background="{DynamicResource AppBorderColor}"
|
||||
Margin="20,0"/>
|
||||
|
||||
<!-- Content -->
|
||||
<StackPanel Grid.Row="2" Margin="20,18,20,20">
|
||||
<StackPanel Grid.Row="2" Margin="20,16,20,16">
|
||||
<!-- Donation Message -->
|
||||
<TextBlock Text="Win11Debloat is a passion project that I maintain in my free time. If you've found this tool useful, please consider making a small donation to support its development. I really appreciate it!"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
Foreground="{DynamicResource AppFgColor}"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,0,0,15"/>
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
FontWeight="SemiBold"
|
||||
FontFamily="Segoe Fluent Icons"
|
||||
Text=""
|
||||
Foreground="{DynamicResource CloseHover}"
|
||||
Foreground="{DynamicResource TitleBarCloseHoverColor}"
|
||||
Margin="0,0,8,0"/>
|
||||
|
||||
<TextBlock x:Name="KofiLink"
|
||||
@@ -124,11 +124,17 @@
|
||||
|
||||
<!-- Button Panel -->
|
||||
<Border Grid.Row="2" Style="{DynamicResource ModalFooterBorderStyle}">
|
||||
<StackPanel x:Name="ButtonPanel" Style="{DynamicResource ModalFooterButtonsRightStyle}">
|
||||
<Button x:Name="CloseButton"
|
||||
Content="Close"
|
||||
Style="{DynamicResource ModalSecondaryActionButtonStyle}"/>
|
||||
</StackPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Button x:Name="CloseButton"
|
||||
Grid.Column="1"
|
||||
Content="Close"
|
||||
Style="{DynamicResource ModalSecondaryStretchedButtonStyle}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
Foreground="{DynamicResource FgColor}">
|
||||
Foreground="{DynamicResource AppFgColor}">
|
||||
<Window.Resources>
|
||||
<!-- Title Bar Button Style -->
|
||||
<Style x:Key="TitleBarButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Width" Value="46"/>
|
||||
<Setter Property="Height" Value="32"/>
|
||||
@@ -31,17 +31,17 @@
|
||||
<Style x:Key="CloseButton" TargetType="Button" BasedOn="{StaticResource TitleBarButton}">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource CloseHover}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource TitleBarCloseHoverColor}"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Border BorderBrush="{DynamicResource BorderColor}"
|
||||
<Border BorderBrush="{DynamicResource AppBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Background="{DynamicResource BgColor}"
|
||||
Background="{DynamicResource AppBgColor}"
|
||||
Margin="25">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="Black"
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
<!-- Custom Title Bar -->
|
||||
<Grid Grid.Row="0" x:Name="TitleBar">
|
||||
<Border Background="{DynamicResource BgColor}" CornerRadius="8,8,0,0">
|
||||
<Border Background="{DynamicResource AppBgColor}" CornerRadius="8,8,0,0">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
@@ -66,7 +66,7 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="Win11Debloat Application Selection"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
Foreground="{DynamicResource AppFgColor}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="12,0,0,0"
|
||||
FontSize="12"/>
|
||||
@@ -90,27 +90,27 @@
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="Check apps that you wish to remove, uncheck apps that you wish to keep"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
Foreground="{DynamicResource AppFgColor}"
|
||||
Margin="0,0,0,10"/>
|
||||
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,8">
|
||||
<CheckBox x:Name="CheckAllBox" Content="Check/Uncheck all" Margin="8,0,15,0" Foreground="{DynamicResource FgColor}" AutomationProperties.Name="Check or Uncheck all"/>
|
||||
<CheckBox x:Name="CheckAllBox" Content="Check/Uncheck all" Margin="8,0,15,0" Foreground="{DynamicResource AppFgColor}" AutomationProperties.Name="Check or Uncheck all"/>
|
||||
</StackPanel>
|
||||
|
||||
<Border Grid.Row="2" BorderBrush="{DynamicResource BorderColor}" CornerRadius="4" BorderThickness="1" Margin="0,0,0,10" Background="{DynamicResource CardBgColor}">
|
||||
<Border Grid.Row="2" BorderBrush="{DynamicResource AppBorderColor}" CornerRadius="4" BorderThickness="1" Margin="0,0,0,10" Background="{DynamicResource CardBgColor}">
|
||||
<Grid>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel x:Name="AppsPanel" Margin="5"/>
|
||||
</ScrollViewer>
|
||||
<Border x:Name="LoadingAppsIndicator" CornerRadius="4" Background="{DynamicResource ScrollBarThumbColor}" Opacity="0.8" Visibility="Collapsed">
|
||||
<TextBlock Text="Loading apps..." FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource FgColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="Loading apps..." FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource AppFgColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Grid Margin="0,8,10,10">
|
||||
<CheckBox x:Name="OnlyInstalledBox" Content="Only show installed apps" Foreground="{DynamicResource FgColor}" AutomationProperties.Name="Only show installed apps"/>
|
||||
<CheckBox x:Name="OnlyInstalledBox" Content="Only show installed apps" Foreground="{DynamicResource AppFgColor}" AutomationProperties.Name="Only show installed apps"/>
|
||||
</Grid>
|
||||
<Button x:Name="ConfirmBtn" Width="80" Height="32" Margin="0,0,10,0" Content="Confirm" Style="{DynamicResource PrimaryButtonStyle}" AutomationProperties.Name="Confirm"/>
|
||||
<Button x:Name="CancelBtn" Width="80" Height="32" Content="Cancel" Style="{DynamicResource SecondaryButtonStyle}" IsCancel="True" AutomationProperties.Name="Cancel"/>
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource CloseHover}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource TitleBarCloseHoverColor}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
@@ -56,7 +56,7 @@
|
||||
Text=""
|
||||
FontFamily="Segoe Fluent Icons"
|
||||
FontSize="36"
|
||||
Foreground="{DynamicResource ButtonBg}"
|
||||
Foreground="{DynamicResource ButtonBgColor}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,16"
|
||||
RenderTransformOrigin="0.5,0.5">
|
||||
@@ -102,7 +102,7 @@
|
||||
<TextBlock x:Name="ApplyStepCounter"
|
||||
Text="Step 0 of 0"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
Foreground="{DynamicResource AppFgColor}"
|
||||
HorizontalAlignment="Right"
|
||||
Opacity="0.8"/>
|
||||
</StackPanel>
|
||||
@@ -117,7 +117,7 @@
|
||||
Text=""
|
||||
FontFamily="Segoe Fluent Icons"
|
||||
FontSize="40"
|
||||
Foreground="{DynamicResource ButtonBg}"
|
||||
Foreground="{DynamicResource ButtonBgColor}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,12"/>
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
<!-- Reboot required section -->
|
||||
<Border x:Name="ApplyRebootPanel"
|
||||
Visibility="Collapsed"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderBrush="{DynamicResource AppBorderColor}"
|
||||
HorizontalAlignment="Center"
|
||||
BorderThickness="0,1,0,1"
|
||||
Padding="24,12,24,14">
|
||||
@@ -149,7 +149,7 @@
|
||||
<TextBlock Text="A reboot is required for these changes to take effect:"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
Foreground="{DynamicResource AppFgColor}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<StackPanel x:Name="ApplyRebootList" Margin="22,0,0,0"/>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
Text="View the selected changes here"
|
||||
TextWrapping="Wrap"
|
||||
MaxWidth="260"
|
||||
Foreground="{DynamicResource FgColor}"/>
|
||||
Foreground="{DynamicResource AppFgColor}"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -29,12 +29,12 @@
|
||||
|
||||
<!-- Content -->
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Padding="0,0,8,0">
|
||||
<StackPanel x:Name="ContentPanel" Margin="20,4,20,8">
|
||||
<StackPanel x:Name="ContentPanel" Margin="20,4">
|
||||
<TextBlock x:Name="PromptText"
|
||||
TextWrapping="Wrap"
|
||||
FontSize="14"
|
||||
LineHeight="20"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
Foreground="{DynamicResource AppFgColor}"
|
||||
Margin="0,0,0,14"/>
|
||||
<!-- Checkboxes are added dynamically at runtime -->
|
||||
<StackPanel x:Name="CheckboxPanel"/>
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
|
||||
Title="Win11Debloat"
|
||||
MinWidth="860" MinHeight="600"
|
||||
MinWidth="860" MinHeight="640"
|
||||
ResizeMode="CanResize"
|
||||
SnapsToDevicePixels="True"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="False"
|
||||
Background="{DynamicResource BgColor}"
|
||||
Foreground="{DynamicResource FgColor}">
|
||||
Background="{DynamicResource AppBorderColor}"
|
||||
Foreground="{DynamicResource AppFgColor}">
|
||||
<shell:WindowChrome.WindowChrome>
|
||||
<shell:WindowChrome ResizeBorderThickness="5"
|
||||
CaptionHeight="32"
|
||||
@@ -30,8 +30,8 @@
|
||||
</Style>
|
||||
<!-- TextBlock Label Style -->
|
||||
<Style x:Key="LabelStyle" TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="Margin" Value="0,0,0,2"/>
|
||||
</Style>
|
||||
|
||||
@@ -46,28 +46,30 @@
|
||||
|
||||
<!-- Category card border style -->
|
||||
<Style x:Key="CategoryCardBorderStyle" TargetType="Border">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderColor}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="4"/>
|
||||
<Setter Property="Background" Value="{DynamicResource CardBgColor}"/>
|
||||
<Setter Property="Padding" Value="16,12,16,2"/>
|
||||
<Setter Property="Padding" Value="16,13,16,4"/>
|
||||
<Setter Property="Margin" Value="0,0,0,16"/>
|
||||
</Style>
|
||||
|
||||
<!-- Category header text style -->
|
||||
<Style x:Key="CategoryHeaderTextBlock" TargetType="TextBlock">
|
||||
<Setter Property="FontWeight" Value="Bold"/>
|
||||
<Setter Property="FontSize" Value="16"/>
|
||||
<Setter Property="Margin" Value="0,0,0,13"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="FontSize" Value="18"/>
|
||||
<Setter Property="LineHeight" Value="20"/>
|
||||
<Setter Property="Margin" Value="0,0,0,16"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
</Style>
|
||||
|
||||
<!-- Category header icon style -->
|
||||
<Style x:Key="CategoryHeaderIcon" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="Segoe Fluent Icons"/>
|
||||
<Setter Property="FontSize" Value="19"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="Margin" Value="0,0,8,12"/>
|
||||
<Setter Property="FontSize" Value="20"/>
|
||||
<Setter Property="LineHeight" Value="20"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="Margin" Value="0,0,8,16"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
|
||||
@@ -83,7 +85,7 @@
|
||||
<Setter Property="IsTabStop" Value="False"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonBg}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonBgColor}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
@@ -95,17 +97,18 @@
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonHover}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonHoverColor}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonPressed}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonPressedColor}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="CategoryHelpLinkTextStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="FontWeight" Value="Bold"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Margin" Value="0,0,0,5"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="Foreground">
|
||||
<Setter.Value>
|
||||
@@ -116,14 +119,14 @@
|
||||
|
||||
<!-- Overview changes text style -->
|
||||
<Style x:Key="OverviewNoChangesTextStyle" TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="Margin" Value="0,0,0,8"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="FontStyle" Value="Italic"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="OverviewChangeBulletStyle" TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="Margin" Value="0,0,0,8"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||
@@ -131,45 +134,7 @@
|
||||
|
||||
<!-- TextBox Style for search -->
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="CaretBrush" Value="{DynamicResource FgColor}"/>
|
||||
</Style>
|
||||
|
||||
<!-- User TextBox Style with disabled state -->
|
||||
<Style x:Key="UserTextBoxStyle" TargetType="TextBox">
|
||||
<Setter Property="CaretBrush" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="Margin" Value="1,0,0,1"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Background" Value="{DynamicResource ButtonDisabled}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonTextDisabled}"/>
|
||||
<Setter Property="Opacity" Value="0.6"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- User TextBox Border Style with disabled state -->
|
||||
<Style x:Key="UserTextBoxBorderStyle" TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource ComboBgColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="4"/>
|
||||
<Setter Property="Padding" Value="8,6"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Background" Value="{DynamicResource ButtonDisabled}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
||||
<Setter Property="Opacity" Value="0.6"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource ComboHoverColor}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
<Setter Property="CaretBrush" Value="{DynamicResource AppFgColor}"/>
|
||||
</Style>
|
||||
|
||||
<!-- TextBlock style for App ID column in apps table -->
|
||||
@@ -194,7 +159,7 @@
|
||||
|
||||
<!-- Column widths for the app table rows and header (dot | name | description | id) -->
|
||||
<GridLength x:Key="AppTableDotColWidth">16</GridLength>
|
||||
<GridLength x:Key="AppTableNameColWidth">151</GridLength>
|
||||
<GridLength x:Key="AppTableNameColWidth">180</GridLength>
|
||||
<GridLength x:Key="AppTableDescColWidth">1*</GridLength>
|
||||
<GridLength x:Key="AppTableIdColWidth">261</GridLength>
|
||||
|
||||
@@ -219,10 +184,15 @@
|
||||
<SolidColorBrush x:Key="ValidationErrorColor" Color="#c42b1c"/>
|
||||
<SolidColorBrush x:Key="ValidationSuccessColor" Color="#28a745"/>
|
||||
|
||||
<!-- App recommendation dot / legend colors -->
|
||||
<SolidColorBrush x:Key="AppRecommendationSafeColor" Color="#4CAF50"/>
|
||||
<SolidColorBrush x:Key="AppRecommendationOptionalColor" Color="#FFC107"/>
|
||||
<SolidColorBrush x:Key="AppRecommendationUnsafeColor" Color="#F44336"/>
|
||||
|
||||
<!-- Title Bar Button Style -->
|
||||
<Style x:Key="TitleBarButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Width" Value="46"/>
|
||||
<Setter Property="Height" Value="32"/>
|
||||
@@ -237,11 +207,21 @@
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsActive, RelativeSource={RelativeSource AncestorType=Window}}" Value="False">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TitleBarUnfocusedFgColor}"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
<Style x:Key="CloseButton" TargetType="Button" BasedOn="{StaticResource TitleBarButton}">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource CloseHover}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource TitleBarCloseHoverColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource TitleBarClosePressedColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
@@ -257,25 +237,12 @@
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource TitlebarButtonHover}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource TitleBarButtonHoverColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource TitlebarButtonPressed}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- SearchBox Border Style -->
|
||||
<Style x:Key="SearchBoxBorderStyle" TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource ComboBgColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="4"/>
|
||||
<Setter Property="Width" Value="300"/>
|
||||
<Setter Property="Padding" Value="8,6"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource ComboHoverColor}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource TitleBarButtonPressedColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
@@ -283,7 +250,7 @@
|
||||
<!-- ContextMenu Style -->
|
||||
<Style TargetType="ContextMenu">
|
||||
<Setter Property="Background" Value="{DynamicResource CardBgColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderColor}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="4"/>
|
||||
<Setter Property="HasDropShadow" Value="True"/>
|
||||
@@ -311,7 +278,7 @@
|
||||
|
||||
<!-- MenuItem Style -->
|
||||
<Style TargetType="MenuItem">
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Padding" Value="8,6"/>
|
||||
<Setter Property="Height" Value="32"/>
|
||||
@@ -337,10 +304,10 @@
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsHighlighted" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource ComboItemHoverColor}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource ComboBoxItemHoverColor}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource ComboItemSelectedColor}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource ComboBoxItemSelectedColor}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
@@ -349,10 +316,10 @@
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Border x:Name="MainBorder" BorderBrush="{DynamicResource BorderColor}"
|
||||
<Border x:Name="MainBorder" BorderBrush="{DynamicResource AppBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Background="{DynamicResource BgColor}"
|
||||
Background="{DynamicResource AppBgColor}"
|
||||
Margin="0">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
@@ -372,7 +339,7 @@
|
||||
|
||||
<!-- Custom Title Bar -->
|
||||
<Grid Grid.Row="0" x:Name="TitleBar">
|
||||
<Border x:Name="TitleBarBackground" Background="{DynamicResource BgColor}" CornerRadius="8,8,0,0">
|
||||
<Border x:Name="TitleBarBackground" Background="{DynamicResource AppBgColor}" CornerRadius="8,8,0,0">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
@@ -380,10 +347,20 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="Win11Debloat"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="12,0,0,0"
|
||||
FontSize="12"/>
|
||||
FontSize="12">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsActive, RelativeSource={RelativeSource AncestorType=Window}}" Value="False">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TitleBarUnfocusedFgColor}"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
<Button x:Name="KofiBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="" FontFamily="Segoe Fluent Icons" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="Support the creator" AutomationProperties.Name="Support the creator"/>
|
||||
<Button x:Name="MenuBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="" FontFamily="Segoe Fluent Icons" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="Options" AutomationProperties.Name="Options">
|
||||
@@ -395,7 +372,7 @@
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Separator">
|
||||
<Border Height="1" Background="{DynamicResource BorderColor}" SnapsToDevicePixels="True" HorizontalAlignment="Stretch"/>
|
||||
<Border Height="1" Background="{DynamicResource AppBorderColor}" SnapsToDevicePixels="True" HorizontalAlignment="Stretch"/>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
@@ -403,38 +380,38 @@
|
||||
</ContextMenu.Resources>
|
||||
<MenuItem x:Name="ImportConfigBtn" Header="Import config" AutomationProperties.Name="Import configuration">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="ExportConfigBtn" Header="Export config" AutomationProperties.Name="Export configuration">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="RestoreBackupBtn" Header="Restore backup" AutomationProperties.Name="Restore registry backup">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem x:Name="MenuDocumentation" Header="Documentation" AutomationProperties.Name="Documentation">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="16" Foreground="{DynamicResource FgColor}"/>
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="MenuReportBug" Header="Report a bug" AutomationProperties.Name="Report a bug">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="MenuLogs" Header="Logs" AutomationProperties.Name="Logs">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="MenuAbout" Header="About" AutomationProperties.Name="About">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
@@ -459,16 +436,16 @@
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TabItem">
|
||||
<Border Name="Border" Background="{DynamicResource SecondaryButtonBg}" BorderThickness="0" CornerRadius="0" Padding="10,10" Margin="0">
|
||||
<Border Name="Border" Background="{DynamicResource SecondaryButtonBgColor}" BorderThickness="0" CornerRadius="0" Padding="10,10" Margin="0">
|
||||
<ContentPresenter ContentSource="Header" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="{DynamicResource ButtonBg}"/>
|
||||
<Setter TargetName="Border" Property="Background" Value="{DynamicResource ButtonBgColor}"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="False">
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
@@ -485,48 +462,74 @@
|
||||
<!-- Home Tab -->
|
||||
<TabItem Header="Home" x:Name="HomeTab">
|
||||
<Grid>
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Width="600">
|
||||
<StackPanel x:Name="HomeContentPanel" HorizontalAlignment="Center" VerticalAlignment="Top">
|
||||
<!-- Logo -->
|
||||
<Viewbox Width="220" Height="220" Margin="0,32,0,10" HorizontalAlignment="Center">
|
||||
<Grid Width="220" Height="220">
|
||||
<Viewbox Width="250" Height="250" Margin="0,0,0,16" HorizontalAlignment="Center">
|
||||
<Grid Width="250" Height="250">
|
||||
<!-- 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"
|
||||
Fill="{DynamicResource ButtonBg}" Stretch="Uniform" Margin="10"/>
|
||||
Fill="{DynamicResource ButtonBgColor}" Stretch="Uniform" Margin="10"/>
|
||||
<!-- 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"
|
||||
Fill="{DynamicResource AccentColor}" Width="38" Height="38" Stretch="Uniform"/>
|
||||
Fill="{DynamicResource AppAccentColor}" 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"
|
||||
Fill="{DynamicResource AccentColor}" Width="20" Height="20" Stretch="Uniform"/>
|
||||
Fill="{DynamicResource AppAccentColor}" 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"
|
||||
Fill="{DynamicResource AccentColor}" Width="15" Height="15" Stretch="Uniform"/>
|
||||
Fill="{DynamicResource AppAccentColor}" Width="17" Height="17" Stretch="Uniform"/>
|
||||
</Canvas>
|
||||
<!-- Actual logo image if available -->
|
||||
<Image x:Name="LogoImage" Stretch="Uniform"/>
|
||||
</Grid>
|
||||
</Viewbox>
|
||||
|
||||
<!-- Title -->
|
||||
<TextBlock Text="Welcome to Win11Debloat" FontSize="28" 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 Text="Welcome to Win11Debloat" FontSize="40" FontWeight="SemiBold" Foreground="{DynamicResource AppFgColor}" HorizontalAlignment="Center"/>
|
||||
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource AppFgColor}" FontSize="20" HorizontalAlignment="Center" Margin="0,4,0,48">
|
||||
<Run Text="Your clean Windows experience is just a few clicks away!"/>
|
||||
</TextBlock>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,20,0,0">
|
||||
<Button x:Name="HomeDefaultModeBtn" Width="180" Height="50" Style="{DynamicResource PrimaryButtonStyle}" Margin="0,0,12,0" AutomationProperties.Name="Default Mode">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBlock Text="Default Mode" ToolTip="Quickly select the recommended settings" FontWeight="SemiBold" VerticalAlignment="Center" FontSize="17" Margin="0,0,0,2"/>
|
||||
|
||||
<Border HorizontalAlignment="Center" BorderBrush="{DynamicResource AppBorderColor}" BorderThickness="1" CornerRadius="4" Background="{DynamicResource CardBgColor}" Padding="16,12" Width="500">
|
||||
<StackPanel>
|
||||
<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 TitleBarCloseHoverColor}" FontSize="11" Margin="3,0,0,4" TextWrapping="Wrap"/>
|
||||
<Border Style="{DynamicResource TextBoxBorderStyle}">
|
||||
<Border Style="{DynamicResource TextBoxBottomBorderStyle}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock x:Name="UsernameTextBoxPlaceholder" Grid.Column="0" Text="Enter username" Foreground="{DynamicResource AppFgColor}" Opacity="0.7" FontSize="13" Margin="3,0,0,1" VerticalAlignment="Center" IsHitTestVisible="False"/>
|
||||
<TextBox x:Name="OtherUsernameTextBox" Grid.Column="0" Style="{DynamicResource TextBoxInputStyle}" Text="" AutomationProperties.Name="Enter username"/>
|
||||
<TextBlock Grid.Column="1" Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="8,0,4,0" Foreground="{DynamicResource AppFgColor}" Opacity="0.7"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button x:Name="HomeStartBtn" Width="180" Height="50" Style="{DynamicResource SecondaryButtonStyle}" AutomationProperties.Name="Custom Setup">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBlock Text="Custom Setup" ToolTip="Manually select your preferred settings" FontWeight="SemiBold" VerticalAlignment="Center" FontSize="17" Margin="0,0,0,2"/>
|
||||
|
||||
<Separator Margin="0,10,0,8" Background="{DynamicResource AppBorderColor}"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,8,0,4">
|
||||
<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">
|
||||
<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,1"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button x:Name="HomeStartBtn" Width="227" Height="50" Style="{DynamicResource SecondaryButtonStyle}" AutomationProperties.Name="Custom Setup">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<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,1"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
@@ -534,10 +537,10 @@
|
||||
<!-- App Removal Tab -->
|
||||
<TabItem Header="App Removal">
|
||||
<DockPanel>
|
||||
<Border DockPanel.Dock="Top" Padding="20,10,20,0">
|
||||
<Border DockPanel.Dock="Top" Padding="20,4,20,0">
|
||||
<StackPanel>
|
||||
<TextBlock Text="App Removal" FontWeight="Bold" FontSize="20" Margin="0,0,0,5" Foreground="{DynamicResource FgColor}"/>
|
||||
<TextBlock Text="Select which apps you want to remove from your system" FontSize="13" Margin="0,0,0,20" Foreground="{DynamicResource FgColor}" TextWrapping="Wrap"/>
|
||||
<TextBlock Text="App Removal" FontWeight="SemiBold" FontSize="24" Foreground="{DynamicResource AppFgColor}"/>
|
||||
<TextBlock Text="Select which apps you want to remove" FontSize="14" Margin="0,5,0,20" Foreground="{DynamicResource AppFgColor}" TextWrapping="Wrap"/>
|
||||
|
||||
<!-- Filter Options -->
|
||||
<Grid Margin="0,0,0,12">
|
||||
@@ -550,11 +553,11 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Grid.Column="0">
|
||||
<ToggleButton x:Name="PresetsBtn" ToolTip="Select or clear app presets" Height="32" Padding="10,0" Margin="0,0,10,0" AutomationProperties.Name="App Presets">
|
||||
<ToggleButton x:Name="PresetsBtn" Height="32" Padding="10,0" Margin="0,0,10,0" AutomationProperties.Name="App Presets">
|
||||
<ToggleButton.Style>
|
||||
<Style TargetType="ToggleButton">
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBg}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBgColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
@@ -570,20 +573,20 @@
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHover}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHoverColor}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressed}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressedColor}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHover}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHoverColor}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</ToggleButton.Style>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="Quick Select" FontSize="13" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,1,6,0"/>
|
||||
<TextBlock Text="Quick Select" FontSize="13" VerticalAlignment="Center" Margin="0,0,6,1"/>
|
||||
<TextBlock x:Name="PresetsArrow" Text="" FontFamily="Segoe Fluent Icons" FontSize="10" VerticalAlignment="Center" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock.RenderTransform>
|
||||
<RotateTransform x:Name="PresetsArrowRotation" Angle="0"/>
|
||||
@@ -593,46 +596,41 @@
|
||||
</ToggleButton>
|
||||
<Button x:Name="ClearAppSelectionBtn" ToolTip="Clear all selected apps" Style="{DynamicResource SecondaryButtonStyle}" Height="32" Padding="10,0" Margin="0,0,10,0" AutomationProperties.Name="Clear Selection">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="15" VerticalAlignment="Center" Margin="0,3,6,0"/>
|
||||
<TextBlock Text="Clear Selection" FontSize="13" VerticalAlignment="Center" Margin="0,0,0,1"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Popup x:Name="PresetsPopup" PlacementTarget="{Binding ElementName=PresetsBtn}" Placement="Bottom" StaysOpen="True" AllowsTransparency="True" VerticalOffset="2">
|
||||
<Border Background="{DynamicResource CardBgColor}" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" CornerRadius="6" Padding="4,6" Margin="12">
|
||||
<Border Background="{DynamicResource CardBgColor}" BorderBrush="{DynamicResource AppBorderColor}" BorderThickness="1" CornerRadius="6" Padding="4,6" Margin="12">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="12" Opacity="0.25" ShadowDepth="4"/>
|
||||
</Border.Effect>
|
||||
<StackPanel x:Name="PresetsPanel" MinWidth="220">
|
||||
<CheckBox x:Name="PresetDefaultApps" Content="Default apps" IsThreeState="True" ToolTip="Select the apps that are safe to remove for most users" Foreground="{DynamicResource FgColor}" Margin="8,4" AutomationProperties.Name="Default selection"/>
|
||||
<CheckBox x:Name="PresetLastUsed" Content="Last used apps" IsThreeState="True" ToolTip="Select the apps that were removed the last time Win11Debloat was run" Foreground="{DynamicResource FgColor}" Margin="8,4" AutomationProperties.Name="Last used selection"/>
|
||||
<Separator Margin="4,6" Background="{DynamicResource BorderColor}"/>
|
||||
<CheckBox x:Name="PresetDefaultApps" Content="Default apps" IsThreeState="True" ToolTip="Select the apps that are safe to remove for most users" Foreground="{DynamicResource AppFgColor}" Margin="8,4" AutomationProperties.Name="Default selection"/>
|
||||
<CheckBox x:Name="PresetLastUsed" Content="Previously selected apps" IsThreeState="True" ToolTip="Select the apps that were removed the last time Win11Debloat was run" Foreground="{DynamicResource AppFgColor}" Margin="8,4" AutomationProperties.Name="Previously selected apps"/>
|
||||
<Separator Margin="4,6" Background="{DynamicResource AppBorderColor}"/>
|
||||
<StackPanel x:Name="JsonPresetsPanel"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Popup>
|
||||
</StackPanel>
|
||||
|
||||
<CheckBox x:Name="OnlyInstalledAppsBox" Grid.Column="2" Content="Only show installed apps" IsChecked="False" Foreground="{DynamicResource FgColor}" VerticalAlignment="Center" AutomationProperties.Name="Only show installed apps"/>
|
||||
<CheckBox x:Name="OnlyInstalledAppsBox" Grid.Column="2" Content="Only show installed apps" ToolTip="Only show applications that are currently installed on this system" IsChecked="False" Foreground="{DynamicResource AppFgColor}" VerticalAlignment="Center" AutomationProperties.Name="Only show installed apps"/>
|
||||
|
||||
<Border x:Name="AppSearchBorder" Grid.Column="4">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border" BasedOn="{StaticResource SearchBoxBorderStyle}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ElementName=AppSearchBox, 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="AppSearchPlaceholder" Grid.Column="1" Text="Search app" Foreground="{DynamicResource FgColor}" Opacity="0.5" FontSize="13" Margin="3,0,0,1" VerticalAlignment="Center" IsHitTestVisible="False"/>
|
||||
<TextBox x:Name="AppSearchBox" Grid.Column="1" Background="Transparent" Foreground="{DynamicResource FgColor}" BorderThickness="0" FontSize="13" Margin="1,0,0,1" VerticalAlignment="Center" Text="" AutomationProperties.Name="Search app"/>
|
||||
</Grid>
|
||||
<Border x:Name="AppSearchBorder" Grid.Column="4" Width="300">
|
||||
<Border Style="{DynamicResource TextBoxBorderStyle}">
|
||||
<Border Style="{DynamicResource TextBoxBottomBorderStyle}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock x:Name="AppSearchPlaceholder" Grid.Column="0" Text="Search apps..." Foreground="{DynamicResource AppFgColor}" Opacity="0.7" FontSize="13" Margin="3,0,0,1" VerticalAlignment="Center" IsHitTestVisible="False"/>
|
||||
<TextBox x:Name="AppSearchBox" Grid.Column="0" Style="{DynamicResource TextBoxInputStyle}" Text="" AutomationProperties.Name="Search app"/>
|
||||
<TextBlock Grid.Column="1" Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="8,0,4,0" Foreground="{DynamicResource AppFgColor}" Opacity="0.7"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Border>
|
||||
</Border>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
@@ -650,7 +648,7 @@
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<!-- Column Headers -->
|
||||
<Border Grid.Row="0" Background="{DynamicResource TableHeaderColor}" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1,1,1,0" CornerRadius="4,4,0,0">
|
||||
<Border Grid.Row="0" Background="{DynamicResource TableHeaderColor}" BorderBrush="{DynamicResource AppBorderColor}" BorderThickness="1,1,1,0" CornerRadius="4,4,0,0">
|
||||
<Grid Margin="42,6,23,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="{StaticResource AppTableDotColWidth}"/>
|
||||
@@ -659,34 +657,34 @@
|
||||
<ColumnDefinition Width="{StaticResource AppTableIdColWidth}"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel x:Name="HeaderNameBtn" Grid.Column="1" Orientation="Horizontal" Cursor="Hand" VerticalAlignment="Center" Style="{StaticResource SortHeaderBtnStyle}">
|
||||
<TextBlock Text="Name" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource FgColor}"/>
|
||||
<TextBlock x:Name="SortArrowName" Text="" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource FgColor}" VerticalAlignment="Center" Margin="5,1,0,0" Opacity="0.3" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock Text="Name" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
<TextBlock x:Name="SortArrowName" Text="" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource AppFgColor}" VerticalAlignment="Center" Margin="5,1,0,0" Opacity="0.3" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock.RenderTransform><RotateTransform Angle="0"/></TextBlock.RenderTransform>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<StackPanel x:Name="HeaderDescriptionBtn" Grid.Column="2" Orientation="Horizontal" Cursor="Hand" VerticalAlignment="Center" Margin="8,0,0,0" Style="{StaticResource SortHeaderBtnStyle}">
|
||||
<TextBlock Text="Description" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource FgColor}"/>
|
||||
<TextBlock x:Name="SortArrowDescription" Text="" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource FgColor}" VerticalAlignment="Center" Margin="5,1,0,0" Opacity="0.3" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock Text="Description" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
<TextBlock x:Name="SortArrowDescription" Text="" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource AppFgColor}" VerticalAlignment="Center" Margin="5,1,0,0" Opacity="0.3" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock.RenderTransform><RotateTransform Angle="0"/></TextBlock.RenderTransform>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<StackPanel x:Name="HeaderAppIdBtn" Grid.Column="3" Orientation="Horizontal" Cursor="Hand" VerticalAlignment="Center" Style="{StaticResource SortHeaderBtnStyle}">
|
||||
<TextBlock Text="App ID" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource FgColor}"/>
|
||||
<TextBlock x:Name="SortArrowAppId" Text="" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource FgColor}" VerticalAlignment="Center" Margin="5,1,0,0" Opacity="0.3" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock Text="App ID" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
<TextBlock x:Name="SortArrowAppId" Text="" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource AppFgColor}" VerticalAlignment="Center" Margin="5,1,0,0" Opacity="0.3" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock.RenderTransform><RotateTransform Angle="0"/></TextBlock.RenderTransform>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<!-- Apps content -->
|
||||
<Border Grid.Row="1" BorderBrush="{DynamicResource BorderColor}" CornerRadius="0,0,4,4" BorderThickness="1" Background="{DynamicResource CardBgColor}" Padding="0,1,1,1">
|
||||
<Border Grid.Row="1" BorderBrush="{DynamicResource AppBorderColor}" CornerRadius="0,0,4,4" BorderThickness="1" Background="{DynamicResource CardBgColor}" Padding="0,1,1,1">
|
||||
<Grid>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Visible">
|
||||
<StackPanel x:Name="AppSelectionPanel" Margin="10,4,0,4"/>
|
||||
</ScrollViewer>
|
||||
<Border x:Name="LoadingAppsIndicator" CornerRadius="0,0,4,4" Background="{DynamicResource CardBgColor}" Opacity="0.8" Visibility="Collapsed">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="28" Foreground="{DynamicResource FgColor}" HorizontalAlignment="Center" Margin="0,0,0,8" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="28" Foreground="{DynamicResource AppFgColor}" HorizontalAlignment="Center" Margin="0,0,0,8" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock.RenderTransform>
|
||||
<RotateTransform Angle="0"/>
|
||||
</TextBlock.RenderTransform>
|
||||
@@ -703,17 +701,39 @@
|
||||
</EventTrigger>
|
||||
</TextBlock.Triggers>
|
||||
</TextBlock>
|
||||
<TextBlock Text="Loading apps..." FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource FgColor}" HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="Loading apps..." FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource AppFgColor}" HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Status Info -->
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="20,0,20,0">
|
||||
<TextBlock x:Name="AppSelectionStatus" Text="" Foreground="{DynamicResource FgColor}" Margin="10,0,0,5" HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
<!-- Status Info & Color Legend -->
|
||||
<Grid Grid.Row="1" Margin="20,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock x:Name="AppSelectionStatus" Grid.Column="0" Text="" Foreground="{DynamicResource AppFgColor}" Margin="10,0,0,5" HorizontalAlignment="Left" VerticalAlignment="Bottom"/>
|
||||
<StackPanel x:Name="AppColorLegend" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,0,10,5">
|
||||
<TextBlock Text="Legend:" Foreground="{DynamicResource AppFgColor}" FontSize="12" FontWeight="SemiBold" VerticalAlignment="Center" Margin="0,0,10,0" Opacity="0.8"/>
|
||||
<!-- Recommended (safe) -->
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,16,0" ToolTip="[Recommended] Safe to remove for most users">
|
||||
<Ellipse Width="9" Height="9" Fill="{DynamicResource AppRecommendationSafeColor}" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="Recommended" Foreground="{DynamicResource AppFgColor}" FontSize="12" VerticalAlignment="Center" Opacity="0.8"/>
|
||||
</StackPanel>
|
||||
<!-- Optional (default) -->
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,16,0" ToolTip="[Optional] Can be safely removed if you don't need this app">
|
||||
<Ellipse Width="9" Height="9" Fill="{DynamicResource AppRecommendationOptionalColor}" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="Optional" Foreground="{DynamicResource AppFgColor}" FontSize="12" VerticalAlignment="Center" Opacity="0.8"/>
|
||||
</StackPanel>
|
||||
<!-- Not Recommended (unsafe) -->
|
||||
<StackPanel Orientation="Horizontal" ToolTip="[Not Recommended] Only remove if you know what you are doing">
|
||||
<Ellipse Width="9" Height="9" Fill="{DynamicResource AppRecommendationUnsafeColor}" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="Not Recommended" Foreground="{DynamicResource AppFgColor}" FontSize="12" VerticalAlignment="Center" Opacity="0.8"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
@@ -721,10 +741,10 @@
|
||||
<!-- Tweaks Tab -->
|
||||
<TabItem Header="Tweaks">
|
||||
<DockPanel>
|
||||
<Border DockPanel.Dock="Top" Padding="20,10,20,0" Background="{DynamicResource BgColor}">
|
||||
<Border DockPanel.Dock="Top" Padding="20,4,20,0" Background="{DynamicResource AppBgColor}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="System Tweaks" FontWeight="Bold" FontSize="20" Margin="0,0,0,5" Foreground="{DynamicResource FgColor}"/>
|
||||
<TextBlock Text="Select which tweaks you want to apply to your system, hover over settings for more information" FontSize="13" Margin="0,0,0,20" Foreground="{DynamicResource FgColor}" TextWrapping="Wrap"/>
|
||||
<TextBlock Text="System Tweaks" FontWeight="SemiBold" FontSize="24" Foreground="{DynamicResource AppFgColor}"/>
|
||||
<TextBlock Text="Select what changes you want to make, you can hover over settings for more information" FontSize="14" Margin="0,5,0,20" Foreground="{DynamicResource AppFgColor}" TextWrapping="Wrap"/>
|
||||
|
||||
<!-- Filter Options -->
|
||||
<Grid Margin="0,0,0,12">
|
||||
@@ -732,14 +752,16 @@
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="10"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal">
|
||||
<ToggleButton x:Name="TweaksPresetsBtn" ToolTip="Select tweak presets" Height="32" Padding="10,0" Margin="0,0,10,0" AutomationProperties.Name="Tweak Presets">
|
||||
<ToggleButton x:Name="TweaksPresetsBtn" Height="32" Padding="10,0" Margin="0,0,10,0" AutomationProperties.Name="Tweak Presets">
|
||||
<ToggleButton.Style>
|
||||
<Style TargetType="ToggleButton">
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBg}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBgColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
@@ -755,20 +777,20 @@
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHover}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHoverColor}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressed}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressedColor}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHover}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHoverColor}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</ToggleButton.Style>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,1,6,0"/>
|
||||
<TextBlock Text="Quick Select" FontSize="13" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="Quick Select" FontSize="13" VerticalAlignment="Center" Margin="0,0,6,1"/>
|
||||
<TextBlock x:Name="TweaksPresetsArrow" Text="" FontFamily="Segoe Fluent Icons" FontSize="10" VerticalAlignment="Center" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock.RenderTransform>
|
||||
<RotateTransform Angle="0"/>
|
||||
@@ -778,45 +800,42 @@
|
||||
</ToggleButton>
|
||||
<Button x:Name="ClearAllTweaksBtn" ToolTip="Clear all selected tweaks" Style="{DynamicResource SecondaryButtonStyle}" Padding="10,0" Height="32" Margin="0,0,10,0" AutomationProperties.Name="Clear Selection">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="15" VerticalAlignment="Center" Margin="0,3,6,0"/>
|
||||
<TextBlock Text="Clear Selection" FontSize="13" VerticalAlignment="Center" Margin="0,0,0,1"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Popup x:Name="TweaksPresetsPopup" PlacementTarget="{Binding ElementName=TweaksPresetsBtn}" Placement="Bottom" StaysOpen="True" AllowsTransparency="True" VerticalOffset="2">
|
||||
<Border Background="{DynamicResource CardBgColor}" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" CornerRadius="6" Padding="4,6" Margin="12">
|
||||
<Border Background="{DynamicResource CardBgColor}" BorderBrush="{DynamicResource AppBorderColor}" BorderThickness="1" CornerRadius="6" Padding="4,6" Margin="12">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="12" Opacity="0.25" ShadowDepth="4"/>
|
||||
</Border.Effect>
|
||||
<StackPanel MinWidth="220">
|
||||
<CheckBox x:Name="PresetDefaultTweaksBtn" Content="Default settings" IsThreeState="True" ToolTip="Select the settings that are recommended for most people" Foreground="{DynamicResource FgColor}" Style="{DynamicResource PresetCheckBoxStyle}" AutomationProperties.Name="Default settings"/>
|
||||
<CheckBox x:Name="PresetLastUsedTweaksBtn" Content="Last used settings" IsThreeState="True" ToolTip="Select the settings that were used the last time Win11Debloat was run" Foreground="{DynamicResource FgColor}" Style="{DynamicResource PresetCheckBoxStyle}" AutomationProperties.Name="Last used settings"/>
|
||||
<Separator Margin="4,6" Background="{DynamicResource BorderColor}"/>
|
||||
<CheckBox x:Name="PresetPrivacyTweaksBtn" Content="Privacy & Suggested Content" IsThreeState="True" ToolTip="Select all Privacy & Suggested Content tweaks" Foreground="{DynamicResource FgColor}" Style="{DynamicResource PresetCheckBoxStyle}" AutomationProperties.Name="All Privacy and Suggested Content"/>
|
||||
<CheckBox x:Name="PresetAITweaksBtn" Content="AI features" IsThreeState="True" ToolTip="Select all AI feature tweaks" Foreground="{DynamicResource FgColor}" Style="{DynamicResource PresetCheckBoxStyle}" AutomationProperties.Name="All AI features"/>
|
||||
<CheckBox x:Name="PresetDefaultTweaksBtn" Content="Default settings" IsThreeState="True" ToolTip="Select the settings that are recommended for most people" Foreground="{DynamicResource AppFgColor}" Style="{DynamicResource PresetCheckBoxStyle}" AutomationProperties.Name="Default settings"/>
|
||||
<CheckBox x:Name="PresetLastUsedTweaksBtn" Content="Previously selected settings" IsThreeState="True" ToolTip="Select the settings that were applied the last time Win11Debloat was run" Foreground="{DynamicResource AppFgColor}" Style="{DynamicResource PresetCheckBoxStyle}" AutomationProperties.Name="Previously selected settings"/>
|
||||
<Separator Margin="4,6" Background="{DynamicResource AppBorderColor}"/>
|
||||
<CheckBox x:Name="PresetPrivacyTweaksBtn" Content="Privacy & suggested content" IsThreeState="True" ToolTip="Select all tweaks related to privacy & suggested content" Foreground="{DynamicResource AppFgColor}" Style="{DynamicResource PresetCheckBoxStyle}" AutomationProperties.Name="All Privacy and Suggested Content"/>
|
||||
<CheckBox x:Name="PresetAITweaksBtn" Content="AI features" IsThreeState="True" ToolTip="Select all AI related tweaks" Foreground="{DynamicResource AppFgColor}" Style="{DynamicResource PresetCheckBoxStyle}" AutomationProperties.Name="All AI features"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Popup>
|
||||
</StackPanel>
|
||||
|
||||
<Border x:Name="TweakSearchBorder" Grid.Column="2">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border" BasedOn="{StaticResource SearchBoxBorderStyle}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ElementName=TweakSearchBox, 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="TweakSearchPlaceholder" Grid.Column="1" Text="Search setting" Foreground="{DynamicResource FgColor}" Opacity="0.5" FontSize="13" Margin="3,0,0,1" VerticalAlignment="Center" IsHitTestVisible="False"/>
|
||||
<TextBox x:Name="TweakSearchBox" Grid.Column="1" Background="Transparent" Foreground="{DynamicResource FgColor}" BorderThickness="0" FontSize="13" Margin="1,0,0,1" VerticalAlignment="Center" Text=""/>
|
||||
</Grid>
|
||||
<CheckBox x:Name="ShowCurrentlyAppliedTweaksCheckBox" Grid.Column="2" Content="Detect applied tweaks" IsChecked="True" Foreground="{DynamicResource AppFgColor}" VerticalAlignment="Center" AutomationProperties.Name="Detect applied tweaks" ToolTip="Detect all tweaks currently applied for the current user"/>
|
||||
|
||||
<Border x:Name="TweakSearchBorder" Grid.Column="4" Width="300">
|
||||
<Border Style="{DynamicResource TextBoxBorderStyle}">
|
||||
<Border Style="{DynamicResource TextBoxBottomBorderStyle}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock x:Name="TweakSearchPlaceholder" Grid.Column="0" Text="Search tweaks..." Foreground="{DynamicResource AppFgColor}" Opacity="0.7" FontSize="13" Margin="3,0,0,1" VerticalAlignment="Center" IsHitTestVisible="False"/>
|
||||
<TextBox x:Name="TweakSearchBox" Grid.Column="0" Style="{DynamicResource TextBoxInputStyle}" Text="" AutomationProperties.Name="Search tweaks"/>
|
||||
<TextBlock Grid.Column="1" Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="8,0,4,0" Foreground="{DynamicResource AppFgColor}" Opacity="0.7"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Border>
|
||||
</Border>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
@@ -841,7 +860,7 @@
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
<!-- Top fade gradient -->
|
||||
<Border IsHitTestVisible="False" VerticalAlignment="Top" Height="10" Margin="0,-12,20,0" Background="{DynamicResource BgColor}">
|
||||
<Border IsHitTestVisible="False" VerticalAlignment="Top" Height="10" Margin="0,-12,20,0" Background="{DynamicResource AppBgColor}">
|
||||
<Border.OpacityMask>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop Color="Black" Offset="0"/>
|
||||
@@ -851,7 +870,7 @@
|
||||
</Border.OpacityMask>
|
||||
</Border>
|
||||
<!-- Bottom fade gradient -->
|
||||
<Border IsHitTestVisible="False" VerticalAlignment="Bottom" Height="20" Margin="0,0,20,0" Background="{DynamicResource BgColor}">
|
||||
<Border IsHitTestVisible="False" VerticalAlignment="Bottom" Height="20" Margin="0,0,20,0" Background="{DynamicResource AppBgColor}">
|
||||
<Border.OpacityMask>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop Color="Transparent" Offset="0"/>
|
||||
@@ -866,10 +885,10 @@
|
||||
<!-- Deployment Settings Tab -->
|
||||
<TabItem Header="Deployment Settings" x:Name="DeploymentSettingsTab">
|
||||
<DockPanel>
|
||||
<Border DockPanel.Dock="Top" Padding="20,10,20,0">
|
||||
<Border DockPanel.Dock="Top" Padding="20,4,20,0">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Deployment Settings" FontWeight="Bold" FontSize="20" Margin="0,0,0,5" Foreground="{DynamicResource FgColor}"/>
|
||||
<TextBlock Text="Configure how your changes will be applied and more" FontSize="13" Margin="0,0,0,20" Foreground="{DynamicResource FgColor}" TextWrapping="Wrap"/>
|
||||
<TextBlock Text="Deployment Settings" FontWeight="SemiBold" FontSize="24" Foreground="{DynamicResource AppFgColor}"/>
|
||||
<TextBlock Text="Configure how the selected changes will be applied to your system" FontSize="14" Margin="0,5,0,20" Foreground="{DynamicResource AppFgColor}" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
@@ -889,56 +908,28 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 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 AppBorderColor}" BorderThickness="1" CornerRadius="4" Background="{DynamicResource CardBgColor}" Padding="16,12" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="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>
|
||||
<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"/>
|
||||
<TextBlock Text="Changes will be applied to" Style="{StaticResource CategoryHeaderTextBlock}"/>
|
||||
<TextBlock x:Name="UserSelectionDescription" Text="The currently logged-in user profile." Foreground="{DynamicResource AppFgColor}" FontSize="13" TextWrapping="Wrap" Margin="0,-2,0,3"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 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 AppBorderColor}" BorderThickness="1" CornerRadius="4" Background="{DynamicResource CardBgColor}" Padding="16,12" Margin="0,0,0,16">
|
||||
<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">
|
||||
<ComboBoxItem x:Name="AppRemovalScopeAllUsers" Content="All users" IsSelected="True"/>
|
||||
<ComboBoxItem x:Name="AppRemovalScopeCurrentUser" Content="Current user only"/>
|
||||
<ComboBoxItem x:Name="AppRemovalScopeTargetUser" Content="Target user only" Visibility="Collapsed"/>
|
||||
</ComboBox>
|
||||
<TextBlock x:Name="AppRemovalScopeDescription" Text="Apps will be removed for all users and from the Windows image to prevent reinstallation for new users." Foreground="{DynamicResource FgColor}" FontSize="12" TextWrapping="Wrap" Margin="0,6,0,3"/>
|
||||
<TextBlock x:Name="AppRemovalScopeDescription" Text="Apps will be removed for all users and from the Windows image to prevent reinstallation for new users." Foreground="{DynamicResource AppFgColor}" FontSize="13" TextWrapping="Wrap" Margin="0,6,0,3"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Options -->
|
||||
<Border Grid.Row="2" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" CornerRadius="4" Background="{DynamicResource CardBgColor}" Padding="16,12,16,3" Margin="0,0,0,16">
|
||||
<Border Grid.Row="2" BorderBrush="{DynamicResource AppBorderColor}" BorderThickness="1" CornerRadius="4" Background="{DynamicResource CardBgColor}" Padding="16,12,16,3" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Options" Style="{StaticResource CategoryHeaderTextBlock}"/>
|
||||
|
||||
@@ -956,7 +947,7 @@
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
<!-- Top fade gradient -->
|
||||
<Border IsHitTestVisible="False" VerticalAlignment="Top" Height="10" Margin="0,-12,20,0" Background="{DynamicResource BgColor}">
|
||||
<Border IsHitTestVisible="False" VerticalAlignment="Top" Height="10" Margin="0,-12,20,0" Background="{DynamicResource AppBgColor}">
|
||||
<Border.OpacityMask>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop Color="Black" Offset="0"/>
|
||||
@@ -966,7 +957,7 @@
|
||||
</Border.OpacityMask>
|
||||
</Border>
|
||||
<!-- Bottom fade gradient -->
|
||||
<Border IsHitTestVisible="False" VerticalAlignment="Bottom" Height="20" Margin="0,0,20,0" Background="{DynamicResource BgColor}">
|
||||
<Border IsHitTestVisible="False" VerticalAlignment="Bottom" Height="20" Margin="0,0,20,0" Background="{DynamicResource AppBgColor}">
|
||||
<Border.OpacityMask>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop Color="Transparent" Offset="0"/>
|
||||
@@ -977,23 +968,23 @@
|
||||
</Grid>
|
||||
|
||||
<!-- Review & Apply Section -->
|
||||
<StackPanel Grid.Row="1" HorizontalAlignment="Stretch" Background="{DynamicResource BgColor}">
|
||||
<StackPanel Grid.Row="1" HorizontalAlignment="Stretch" Background="{DynamicResource AppBgColor}">
|
||||
<Button x:Name="ReviewChangesBtn" Background="Transparent" BorderThickness="0" Cursor="Hand" HorizontalAlignment="Center" Margin="0,4,0,10" AutomationProperties.Name="Review selected changes">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<TextBlock x:Name="LinkText" Text="Review selected changes" FontSize="14" Foreground="{DynamicResource ButtonBg}" FontWeight="SemiBold" HorizontalAlignment="Center"/>
|
||||
<TextBlock x:Name="LinkText" Text="Review selected changes" FontSize="16" Foreground="{DynamicResource ButtonBgColor}" FontWeight="SemiBold" HorizontalAlignment="Center"/>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="LinkText" Property="Foreground" Value="{DynamicResource ButtonHover}"/>
|
||||
<Setter TargetName="LinkText" Property="Foreground" Value="{DynamicResource ButtonHoverColor}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="LinkText" Property="Foreground" Value="{DynamicResource ButtonPressed}"/>
|
||||
<Setter TargetName="LinkText" Property="Foreground" Value="{DynamicResource ButtonPressedColor}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
<Button x:Name="DeploymentApplyBtn" Style="{DynamicResource PrimaryButtonStyle}" Width="190" Height="44" HorizontalAlignment="Center" AutomationProperties.Name="Apply Changes">
|
||||
<Button x:Name="DeploymentApplyBtn" Style="{DynamicResource PrimaryButtonStyle}" Width="200" Height="44" HorizontalAlignment="Center" AutomationProperties.Name="Apply Changes">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="20" FontWeight="SemiBold" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="Apply Changes" VerticalAlignment="Center" FontSize="18" FontWeight="SemiBold" Margin="8,0,0,4"/>
|
||||
@@ -1013,17 +1004,17 @@
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button x:Name="PreviousBtn" Grid.Column="0" Width="120" Height="36" Style="{DynamicResource SecondaryButtonStyle}" Visibility="Collapsed" Margin="10,0,0,0" AutomationProperties.Name="Back">
|
||||
<Button x:Name="PreviousBtn" Grid.Column="0" Width="120" Height="36" Style="{DynamicResource SecondaryButtonStyle}" Visibility="Collapsed" AutomationProperties.Name="Back">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="12" Margin="0,0,8,0" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="Back" VerticalAlignment="Center" FontSize="14" Margin="0,0,0,1"/>
|
||||
<TextBlock Text="Back" VerticalAlignment="Center" FontSize="14" Margin="0,0,0,3"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button x:Name="NextBtn" Width="120" Height="36" Margin="0,0,10,0" Style="{DynamicResource PrimaryButtonStyle}" AutomationProperties.Name="Next">
|
||||
<Button x:Name="NextBtn" Width="120" Height="36" Style="{DynamicResource PrimaryButtonStyle}" AutomationProperties.Name="Next">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="Next" VerticalAlignment="Center" FontSize="14" Margin="0,0,0,1"/>
|
||||
<TextBlock Text="Next" VerticalAlignment="Center" FontSize="14" Margin="0,0,0,3"/>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="12" Margin="8,0,0,0" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
Topmost="False"
|
||||
ShowInTaskbar="False">
|
||||
|
||||
<Border BorderBrush="{DynamicResource BorderColor}"
|
||||
<Border BorderBrush="{DynamicResource AppBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Background="{DynamicResource CardBgColor}"
|
||||
@@ -33,14 +33,10 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Title Bar -->
|
||||
<Grid Grid.Row="0" x:Name="TitleBar" Height="40" Background="Transparent">
|
||||
<Grid Grid.Row="0" x:Name="TitleBar" Style="{DynamicResource ModalTitleBarStyle}">
|
||||
<TextBlock x:Name="TitleText"
|
||||
Text="Message"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center"
|
||||
Margin="16,0,0,0"/>
|
||||
Style="{DynamicResource ModalTitleTextStyle}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Message Content -->
|
||||
@@ -55,7 +51,7 @@
|
||||
Grid.Column="0"
|
||||
FontFamily="Segoe Fluent Icons"
|
||||
FontSize="24"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
Foreground="{DynamicResource AppFgColor}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="4,0,14,0"
|
||||
Visibility="Collapsed"/>
|
||||
@@ -67,7 +63,7 @@
|
||||
TextWrapping="Wrap"
|
||||
FontSize="14"
|
||||
LineHeight="20"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
Foreground="{DynamicResource AppFgColor}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,20,0"/>
|
||||
</ScrollViewer>
|
||||
@@ -75,24 +71,27 @@
|
||||
|
||||
<!-- Button Panel -->
|
||||
<Border Grid.Row="2"
|
||||
Background="{DynamicResource BgColor}"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
Background="{DynamicResource AppBgColor}"
|
||||
BorderBrush="{DynamicResource AppBorderColor}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="16,12"
|
||||
CornerRadius="0,0,8,8">
|
||||
<StackPanel x:Name="ButtonPanel"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right">
|
||||
<Button x:Name="Button1"
|
||||
Content="OK"
|
||||
Height="32" MinWidth="80" Margin="4,0"
|
||||
Style="{DynamicResource PrimaryButtonStyle}"/>
|
||||
<Button x:Name="Button2"
|
||||
Content="Cancel"
|
||||
Height="32" MinWidth="80" Margin="4,0"
|
||||
Style="{DynamicResource SecondaryButtonStyle}"
|
||||
<Grid x:Name="ButtonPanel">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Button x:Name="Button1"
|
||||
Grid.Column="0"
|
||||
Content="OK"
|
||||
Style="{DynamicResource ModalPrimaryStretchedButtonStyle}"/>
|
||||
<Button x:Name="Button2"
|
||||
Grid.Column="1"
|
||||
Content="Cancel"
|
||||
Style="{DynamicResource ModalSecondaryStretchedButtonStyle}"
|
||||
Visibility="Collapsed"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
<Window.Resources>
|
||||
<Style x:Key="RestoreOptionTileStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBg}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBgColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="12,10"/>
|
||||
@@ -36,16 +36,16 @@
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonDisabled}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource SecondaryButtonTextDisabled}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonDisabledColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource SecondaryButtonTextDisabledColor}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHover}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHoverColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressed}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressedColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
@@ -84,10 +84,10 @@
|
||||
<Button.Style>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource CloseHover}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource TitleBarCloseHoverColor}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
@@ -96,7 +96,7 @@
|
||||
</Button>
|
||||
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Margin="0">
|
||||
<Grid Margin="20,4,20,18">
|
||||
<Grid Margin="20,4,20,16">
|
||||
|
||||
<TabControl x:Name="RestoreModeTabs"
|
||||
SelectedIndex="0"
|
||||
@@ -121,7 +121,7 @@
|
||||
TextWrapping="Wrap"
|
||||
FontSize="14"
|
||||
LineHeight="20"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
Foreground="{DynamicResource AppFgColor}"
|
||||
Text="Choose what changes you want to restore."/>
|
||||
|
||||
<Button x:Name="ChooseRegistryBtn"
|
||||
@@ -203,14 +203,14 @@
|
||||
TextWrapping="Wrap"
|
||||
FontSize="14"
|
||||
LineHeight="20"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
Text="This will restore any system registry changes made by Win11Debloat to their previous state. You can review the changes after selecting a backup file. Apps will need to be reinstalled manually."/>
|
||||
Foreground="{DynamicResource AppFgColor}"
|
||||
Text="This will restore any system registry changes made by Win11Debloat to the previous state. You can review the changes after selecting a backup file. Removed applications will need to be reinstalled manually."/>
|
||||
|
||||
<TextBlock Grid.Row="1"
|
||||
Margin="0,12,0,2"
|
||||
TextWrapping="Wrap"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
Foreground="{DynamicResource AppFgColor}"
|
||||
Opacity="0.75"
|
||||
Text="Warning: Only use backup files generated by Win11Debloat."/>
|
||||
</Grid>
|
||||
@@ -222,6 +222,8 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0" Margin="0,0,0,16">
|
||||
@@ -245,7 +247,7 @@
|
||||
<TextBlock x:Name="BackupTargetText" Grid.Row="2" Grid.Column="1" Text="N/A" Style="{DynamicResource ModalInfoValueTextStyle}" Margin="0"/>
|
||||
</Grid>
|
||||
|
||||
<Border Grid.Row="1" Height="1" Background="{DynamicResource BorderColor}" Margin="0,0,0,12"/>
|
||||
<Border Grid.Row="1" Height="1" Background="{DynamicResource AppBorderColor}" Margin="0,0,0,12"/>
|
||||
|
||||
<Grid x:Name="OverviewFeaturesSection" Grid.Row="2">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -256,7 +258,7 @@
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="The following changes will be reverted:"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
Foreground="{DynamicResource AppFgColor}"
|
||||
FontWeight="SemiBold"
|
||||
Margin="0,0,0,8"/>
|
||||
|
||||
@@ -268,7 +270,7 @@
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding DisplayText}" FontSize="12" Foreground="{DynamicResource FgColor}" TextWrapping="Wrap" Margin="0,0,0,6"/>
|
||||
<TextBlock Text="{Binding DisplayText}" FontSize="12" Foreground="{DynamicResource AppFgColor}" TextWrapping="Wrap" Margin="0,0,0,6"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
@@ -277,14 +279,43 @@
|
||||
<TextBlock x:Name="OverviewSummaryText"
|
||||
Grid.Row="2"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
Foreground="{DynamicResource AppFgColor}"
|
||||
TextWrapping="Wrap"
|
||||
Visibility="Collapsed"
|
||||
Text="This will restore the Start Menu pinned apps layout for the current user."/>
|
||||
|
||||
<Border x:Name="NonRevertibleSeparator" Grid.Row="3" Height="1" Background="{DynamicResource BorderColor}" Margin="0,12,0,12" Visibility="Collapsed"/>
|
||||
<Border x:Name="ReappliedSeparator" Grid.Row="3" Height="1" Background="{DynamicResource AppBorderColor}" 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 AppFgColor}"
|
||||
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 AppFgColor}" TextWrapping="Wrap" Margin="0,0,0,6"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
|
||||
<Border x:Name="NonRevertibleSeparator" Grid.Row="5" Height="1" Background="{DynamicResource AppBorderColor}" Margin="0,12,0,12" Visibility="Collapsed"/>
|
||||
|
||||
<Grid x:Name="NonRevertiblePanel" Grid.Row="6" Visibility="Collapsed">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
@@ -294,7 +325,7 @@
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="The following changes won't be reverted:"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
Foreground="{DynamicResource AppFgColor}"
|
||||
FontWeight="SemiBold"
|
||||
Margin="0,0,0,8"/>
|
||||
|
||||
@@ -306,7 +337,7 @@
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding DisplayText}" FontSize="12" Foreground="{DynamicResource FgColor}" Opacity="0.85" TextWrapping="Wrap" Margin="0,0,0,6"/>
|
||||
<TextBlock Text="{Binding DisplayText}" FontSize="12" Foreground="{DynamicResource AppFgColor}" Opacity="0.85" TextWrapping="Wrap" Margin="0,0,0,6"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
@@ -342,8 +373,8 @@
|
||||
FontSize="14"
|
||||
LineHeight="20"
|
||||
Margin="0,0,0,12"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
Text="This will restore the Start Menu pinned apps layout for the selected user(s) using a backup. Win11Debloat can automatically find the backup created by the script."/>
|
||||
Foreground="{DynamicResource AppFgColor}"
|
||||
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"
|
||||
Grid.Row="1"
|
||||
@@ -361,7 +392,7 @@
|
||||
IsChecked="True"
|
||||
FontSize="12"
|
||||
Margin="0,10,0,0"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
Foreground="{DynamicResource AppFgColor}"
|
||||
Opacity="0.85"/>
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
<!-- Primary Button Style -->
|
||||
<Style x:Key="PrimaryButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="{DynamicResource ButtonBg}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource ButtonBgColor}"/>
|
||||
<Setter Property="Foreground" Value="white"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBg}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBgColor}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
@@ -25,25 +25,25 @@
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Background" Value="{DynamicResource ButtonDisabled}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonDisabled}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonTextDisabled}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource ButtonDisabledColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonDisabledColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonTextDisabledColor}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource ButtonHover}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonHover}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource ButtonHoverColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonHoverColor}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource ButtonPressed}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonPressed}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource ButtonPressedColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonPressedColor}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- Secondary Button Style -->
|
||||
<Style x:Key="SecondaryButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBg}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBgColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
@@ -64,16 +64,16 @@
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonDisabled}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource SecondaryButtonTextDisabled}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonDisabledColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource SecondaryButtonTextDisabledColor}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHover}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHoverColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressed}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressedColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
@@ -81,11 +81,11 @@
|
||||
|
||||
<!-- Hyperlink Style -->
|
||||
<Style x:Key="HyperlinkStyle" TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonBg}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonBgColor}"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonHover}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonHoverColor}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
@@ -93,7 +93,7 @@
|
||||
<!-- ProgressBar Style -->
|
||||
<Style x:Key="ApplyProgressBarStyle" TargetType="ProgressBar">
|
||||
<Setter Property="Background" Value="{DynamicResource ButtonBorderColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonBg}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonBgColor}"/>
|
||||
<Setter Property="Height" Value="6"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
@@ -109,9 +109,9 @@
|
||||
|
||||
<!-- Modal Title Style -->
|
||||
<Style x:Key="ModalTitleStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontWeight" Value="Bold"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="FontSize" Value="20"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||
<Setter Property="Margin" Value="0,0,0,8"/>
|
||||
</Style>
|
||||
@@ -119,7 +119,7 @@
|
||||
<!-- Modal Subtext Style -->
|
||||
<Style x:Key="ModalSubtextStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="Opacity" Value="0.8"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||
<Setter Property="TextAlignment" Value="Center"/>
|
||||
@@ -127,7 +127,7 @@
|
||||
|
||||
<!-- Shared modal window shell styles -->
|
||||
<Style x:Key="ModalCardBorderStyle" TargetType="Border">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderColor}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="8"/>
|
||||
<Setter Property="Background" Value="{DynamicResource CardBgColor}"/>
|
||||
@@ -149,16 +149,16 @@
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ModalTitleTextStyle" TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="FontSize" Value="16"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="FontSize" Value="20"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="Margin" Value="20,0,0,0"/>
|
||||
<Setter Property="Margin" Value="16,0,0,0"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ModalFooterBorderStyle" TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource BgColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource AppBgColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderColor}"/>
|
||||
<Setter Property="BorderThickness" Value="0,1,0,0"/>
|
||||
<Setter Property="Padding" Value="16,12"/>
|
||||
<Setter Property="CornerRadius" Value="0,0,8,8"/>
|
||||
@@ -195,20 +195,20 @@
|
||||
|
||||
<Style x:Key="ModalInfoLabelTextStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Margin" Value="0,0,16,8"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ModalInfoValueTextStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="Margin" Value="0,0,0,8"/>
|
||||
</Style>
|
||||
|
||||
<!-- Shared ComboBox style used across windows -->
|
||||
<Style TargetType="ComboBoxItem">
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Padding" Value="10,8"/>
|
||||
<Setter Property="Margin" Value="0,0,0,4"/>
|
||||
@@ -223,14 +223,15 @@
|
||||
Padding="{TemplateBinding Padding}"
|
||||
CornerRadius="4">
|
||||
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Margin="0,0,0,1"/>
|
||||
</Border>
|
||||
<Border x:Name="AccentLine"
|
||||
Width="3"
|
||||
Height="15"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="{DynamicResource ButtonBg}"
|
||||
Background="{DynamicResource ButtonBgColor}"
|
||||
CornerRadius="1.5"
|
||||
Margin="0"
|
||||
Visibility="Collapsed"/>
|
||||
@@ -238,10 +239,10 @@
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="AccentLine" Property="Visibility" Value="Visible"/>
|
||||
<Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource ComboItemSelectedColor}"/>
|
||||
<Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource ComboBoxItemSelectedColor}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsHighlighted" Value="True">
|
||||
<Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource ComboItemHoverColor}"/>
|
||||
<Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource ComboBoxItemHoverColor}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
@@ -250,17 +251,18 @@
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ComboBox">
|
||||
<Setter Property="Background" Value="{DynamicResource ComboBgColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource ComboBoxBgColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderColor}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="Margin" Value="0,4,0,12"/>
|
||||
<Setter Property="MinHeight" Value="33"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ComboBox">
|
||||
<Grid>
|
||||
<Border x:Name="ClosedAccentLine" Width="3" Height="18" HorizontalAlignment="Left" VerticalAlignment="Stretch" Background="{DynamicResource ButtonBg}" CornerRadius="1.5" Panel.ZIndex="2"/>
|
||||
<Border x:Name="ClosedAccentLine" Width="3" Height="18" HorizontalAlignment="Left" VerticalAlignment="Stretch" Background="{DynamicResource ButtonBgColor}" CornerRadius="1.5" Panel.ZIndex="2"/>
|
||||
<ToggleButton x:Name="ToggleButton" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Focusable="False" IsChecked="{Binding Path=IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" ClickMode="Press">
|
||||
<ToggleButton.Style>
|
||||
<Style TargetType="ToggleButton">
|
||||
@@ -275,7 +277,7 @@
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,8,0"
|
||||
Foreground="{DynamicResource FgColor}"
|
||||
Foreground="{DynamicResource AppFgColor}"
|
||||
RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock.RenderTransform>
|
||||
<RotateTransform Angle="0"/>
|
||||
@@ -284,7 +286,7 @@
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="{DynamicResource ComboHoverColor}"/>
|
||||
<Setter TargetName="Border" Property="Background" Value="{DynamicResource ComboBoxHoverColor}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Trigger.EnterActions>
|
||||
@@ -319,7 +321,7 @@
|
||||
</ToggleButton>
|
||||
<ContentPresenter x:Name="ContentPresenter"
|
||||
IsHitTestVisible="False"
|
||||
Margin="10,0,20,0"
|
||||
Margin="10,0,20,2"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding SelectionBoxItem}"
|
||||
@@ -337,8 +339,8 @@
|
||||
HorizontalOffset="0">
|
||||
<Grid x:Name="DropDown" MinWidth="{TemplateBinding ActualWidth}" MaxHeight="{TemplateBinding MaxDropDownHeight}" Margin="12">
|
||||
<Border x:Name="DropDownBorder"
|
||||
Background="{DynamicResource ComboItemBgColor}"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
Background="{DynamicResource ComboBoxItemBgColor}"
|
||||
BorderBrush="{DynamicResource AppBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4"
|
||||
Padding="5,4,5,1">
|
||||
@@ -363,9 +365,9 @@
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="ClosedAccentLine" Property="Visibility" Value="Collapsed"/>
|
||||
<Setter Property="Background" Value="{DynamicResource ButtonDisabled}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonTextDisabled}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource ButtonDisabledColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonTextDisabledColor}"/>
|
||||
<Setter Property="Opacity" Value="0.6"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
@@ -376,8 +378,9 @@
|
||||
|
||||
<!-- Base CheckBox style used across windows -->
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="Padding" Value="4,2"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
@@ -388,16 +391,16 @@
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border x:Name="CheckBoxBorder" Grid.Column="0" Width="18" Height="18" Background="{DynamicResource CheckBoxBgColor}" BorderBrush="{DynamicResource CheckBoxBorderColor}" BorderThickness="1" CornerRadius="4" Margin="0,0,8,0">
|
||||
<Border x:Name="CheckBoxBorder" Grid.Column="0" Width="20" Height="20" Background="{DynamicResource CheckBoxBgColor}" BorderBrush="{DynamicResource CheckBoxBorderColor}" BorderThickness="1" CornerRadius="4" Margin="0,0,8,0">
|
||||
<Grid>
|
||||
<TextBlock x:Name="CheckMark" Text="" FontFamily="Segoe Fluent Icons" FontSize="12" Foreground="{DynamicResource ButtonBg}" HorizontalAlignment="Center" VerticalAlignment="Center" Opacity="0">
|
||||
<TextBlock x:Name="CheckMark" Text="" FontFamily="Segoe Fluent Icons" FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource ButtonBgColor}" HorizontalAlignment="Center" VerticalAlignment="Center" Opacity="0">
|
||||
<TextBlock.Clip>
|
||||
<RectangleGeometry x:Name="CheckMarkClip" Rect="0,0,0,16"/>
|
||||
</TextBlock.Clip>
|
||||
</TextBlock>
|
||||
<TextBlock x:Name="IndeterminateMark" Text="" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource ButtonBg}" HorizontalAlignment="Center" VerticalAlignment="Center" Opacity="0" Margin="1,1,0,0">
|
||||
<TextBlock x:Name="IndeterminateMark" Text="" FontFamily="Segoe Fluent Icons" FontSize="13" FontWeight="Bold" Foreground="{DynamicResource ButtonBgColor}" HorizontalAlignment="Center" VerticalAlignment="Center" Opacity="0" Margin="1,0,0,1">
|
||||
<TextBlock.Clip>
|
||||
<RectangleGeometry x:Name="IndeterminateMarkClip" Rect="0,0,0,16"/>
|
||||
<RectangleGeometry x:Name="IndeterminateMarkClip" Rect="0,0,0,12"/>
|
||||
</TextBlock.Clip>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
@@ -410,8 +413,8 @@
|
||||
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource CheckBoxHoverColor}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonBg}"/>
|
||||
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonBg}"/>
|
||||
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonBgColor}"/>
|
||||
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonBgColor}"/>
|
||||
<Setter TargetName="CheckMark" Property="Foreground" Value="White"/>
|
||||
<Setter TargetName="CheckMark" Property="Opacity" Value="1"/>
|
||||
<Setter TargetName="IndeterminateMark" Property="Opacity" Value="0"/>
|
||||
@@ -433,9 +436,8 @@
|
||||
</Trigger.ExitActions>
|
||||
</Trigger>
|
||||
<Trigger Property="IsChecked" Value="{x:Null}">
|
||||
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonBg}"/>
|
||||
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonBg}"/>
|
||||
<Setter TargetName="CheckBoxBorder" Property="Opacity" Value="0.8"/>
|
||||
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonBgColor}"/>
|
||||
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonBgColor}"/>
|
||||
<Setter TargetName="IndeterminateMark" Property="Foreground" Value="White"/>
|
||||
<Setter TargetName="IndeterminateMark" Property="Opacity" Value="1"/>
|
||||
<Setter TargetName="CheckMark" Property="Opacity" Value="0"/>
|
||||
@@ -461,8 +463,8 @@
|
||||
<Setter TargetName="IndeterminateMark" Property="Opacity" Value="0"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonDisabled}"/>
|
||||
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
||||
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonDisabledColor}"/>
|
||||
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource AppBorderColor}"/>
|
||||
<Setter Property="Opacity" Value="0.4"/>
|
||||
</Trigger>
|
||||
<MultiTrigger>
|
||||
@@ -470,16 +472,16 @@
|
||||
<Condition Property="IsMouseOver" Value="True"/>
|
||||
<Condition Property="IsChecked" Value="True"/>
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonHover}"/>
|
||||
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonHover}"/>
|
||||
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonHoverColor}"/>
|
||||
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonHoverColor}"/>
|
||||
</MultiTrigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsMouseOver" Value="True"/>
|
||||
<Condition Property="IsChecked" Value="{x:Null}"/>
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonHover}"/>
|
||||
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonHover}"/>
|
||||
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonHoverColor}"/>
|
||||
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonHoverColor}"/>
|
||||
<Setter TargetName="CheckBoxBorder" Property="Opacity" Value="0.8"/>
|
||||
</MultiTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
@@ -543,4 +545,47 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- TextBox outer border: side/top borders in side color, background with hover/focus states -->
|
||||
<Style x:Key="TextBoxBorderStyle" TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource TextBoxBgColor}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource TextBoxSideBorderColor}"/>
|
||||
<Setter Property="BorderThickness" Value="1,1,1,0"/>
|
||||
<Setter Property="CornerRadius" Value="4"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource TextBoxHoverColor}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsKeyboardFocusWithin" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource TextBoxFocusColor}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- TextBox inner border: bottom accent line that thickens and changes color on focus -->
|
||||
<Style x:Key="TextBoxBottomBorderStyle" TargetType="Border">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource TextBoxBorderColor}"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1"/>
|
||||
<Setter Property="Padding" Value="8,6"/>
|
||||
<Setter Property="CornerRadius" Value="0,0,4,4"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsKeyboardFocusWithin" Value="True">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBgColor}"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,0,2"/>
|
||||
<Setter Property="Padding" Value="8,6,8,5"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- TextBox input (transparent, used inside TextBoxBorderStyle) -->
|
||||
<Style x:Key="TextBoxInputStyle" TargetType="TextBox">
|
||||
<Setter Property="CaretBrush" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="Margin" Value="1,0,0,1"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Returns a list of installed apps from winget as structured objects.
|
||||
|
||||
# Run winget list and return installed apps.
|
||||
# Use -NonBlocking to keep the UI responsive (GUI mode) via Invoke-NonBlocking.
|
||||
.DESCRIPTION
|
||||
Runs `winget list` and parses the output into PSCustomObject arrays.
|
||||
Use -NonBlocking to keep the UI responsive in GUI mode; otherwise
|
||||
runs synchronously with an optional timeout.
|
||||
|
||||
.PARAMETER TimeOut
|
||||
Maximum seconds to wait for winget to complete. Default is 10.
|
||||
|
||||
.PARAMETER NonBlocking
|
||||
When set, runs via Invoke-NonBlocking so the GUI thread stays responsive.
|
||||
|
||||
.OUTPUTS
|
||||
PSCustomObject[] with Name and Id properties. Returns $null on
|
||||
failure, or an empty array when winget succeeds but lists no apps.
|
||||
#>
|
||||
function GetInstalledAppsViaWinget {
|
||||
param (
|
||||
[int]$TimeOut = 10,
|
||||
@@ -11,13 +27,76 @@ function GetInstalledAppsViaWinget {
|
||||
|
||||
$fetchBlock = {
|
||||
param($timeOut)
|
||||
$job = Start-Job { return winget list --accept-source-agreements --disable-interactivity }
|
||||
$job = Start-Job {
|
||||
$rawOutput = $null
|
||||
try {
|
||||
$originalEncoding = [Console]::OutputEncoding
|
||||
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()
|
||||
try {
|
||||
$rawOutput = winget list --accept-source-agreements --disable-interactivity
|
||||
}
|
||||
finally {
|
||||
[Console]::OutputEncoding = $originalEncoding
|
||||
}
|
||||
return $rawOutput
|
||||
}
|
||||
catch {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
$done = $job | Wait-Job -Timeout $timeOut
|
||||
if ($done) {
|
||||
$result = Receive-Job -Job $job
|
||||
Remove-Job -Job $job -ErrorAction SilentlyContinue
|
||||
return $result
|
||||
|
||||
if (-not $result) { return $null }
|
||||
|
||||
# winget list outputs:
|
||||
# [progress line] / [blank] / header / --- separator / data rows
|
||||
$textOutput = $result -join "`n"
|
||||
$lines = $textOutput -split "`r`n|`n"
|
||||
|
||||
# Find the separator line to know where data starts
|
||||
$dataStart = -1
|
||||
for ($i = 0; $i -lt $lines.Count; $i++) {
|
||||
if ($lines[$i] -match '^-{3,}') {
|
||||
$dataStart = $i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($dataStart -lt 0 -or $dataStart -ge $lines.Count) { return @() }
|
||||
|
||||
$apps = [System.Collections.Generic.List[object]]::new()
|
||||
|
||||
for ($i = $dataStart; $i -lt $lines.Count; $i++) {
|
||||
$line = $lines[$i]
|
||||
if ($line.Trim() -eq '') { continue }
|
||||
|
||||
try {
|
||||
# Split on 2+ spaces; extract Name and Id columns.
|
||||
$fields = [regex]::Split($line.Trim(), '\s{2,}')
|
||||
if ($fields.Count -lt 2) { continue }
|
||||
|
||||
$name = $fields[0].Trim()
|
||||
$id = $fields[1].Trim()
|
||||
|
||||
if (-not $id) { continue }
|
||||
|
||||
$null = $apps.Add([PSCustomObject]@{
|
||||
Name = $name
|
||||
Id = $id
|
||||
})
|
||||
}
|
||||
catch {
|
||||
# Skip lines that can't be parsed
|
||||
}
|
||||
}
|
||||
|
||||
return @($apps)
|
||||
}
|
||||
|
||||
Remove-Job -Job $job -Force -ErrorAction SilentlyContinue
|
||||
return $null
|
||||
}
|
||||
|
||||
@@ -1,135 +1,392 @@
|
||||
# Removes apps specified during function call based on the target scope.
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Removes one or more Windows app packages based on the target scope.
|
||||
|
||||
.DESCRIPTION
|
||||
Iterates over the provided list of app identifiers and removes each one.
|
||||
The removal method (winget vs. Appx cmdlets) is determined per-app from
|
||||
Apps.json. Microsoft Edge is deferred to the end of the loop so that all
|
||||
winget attempts run before any force-remove prompt. A scheduled task is
|
||||
only created when the User or Sysprep parameter was passed.
|
||||
After each winget removal, the system is checked to confirm whether the
|
||||
app is still installed before reporting an error.
|
||||
|
||||
.PARAMETER appsList
|
||||
An array of app package identifiers to remove (e.g. 'Microsoft.BingNews').
|
||||
|
||||
.EXAMPLE
|
||||
RemoveApps @('Microsoft.BingNews', 'Microsoft.BingWeather')
|
||||
|
||||
.EXAMPLE
|
||||
RemoveApps -appsList (GenerateAppsList)
|
||||
#>
|
||||
function RemoveApps {
|
||||
param (
|
||||
$appslist
|
||||
)
|
||||
|
||||
# Determine target from script-level params, defaulting to AllUsers
|
||||
$targetUser = GetTargetUserForAppRemoval
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
foreach ($app in $appslist) {
|
||||
Write-Host "[WhatIf] Remove App Package: $app" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
$appIndex = 0
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
|
||||
$targetUser = GetTargetUserForAppRemoval
|
||||
$appCount = @($appsList).Count
|
||||
$appIndex = 0
|
||||
|
||||
$edgeIds = @('Microsoft.Edge', 'XPFFTQ037JWMHS')
|
||||
$edgeUninstallSucceeded = $false
|
||||
$edgeScheduledTaskAdded = $false
|
||||
$edgeAppsInList = @()
|
||||
$wingetRemovedApps = @()
|
||||
|
||||
Foreach ($app in $appsList) {
|
||||
if ($script:CancelRequested) {
|
||||
return
|
||||
}
|
||||
if ($script:CancelRequested) { return }
|
||||
|
||||
$appIndex++
|
||||
|
||||
# Update step name and sub-progress to show which app is being removed (only for bulk removal)
|
||||
if ($script:ApplySubStepCallback -and $appCount -gt 1) {
|
||||
& $script:ApplySubStepCallback "Removing apps ($appIndex/$appCount)" $appIndex $appCount
|
||||
}
|
||||
|
||||
Write-Host "Attempting to remove $app..."
|
||||
|
||||
# Use WinGet only to remove OneDrive and Edge
|
||||
if (($app -eq "Microsoft.OneDrive") -or ($edgeIds -contains $app)) {
|
||||
if ($script:WingetInstalled -eq $false) {
|
||||
Write-Host "WinGet is either not installed or is outdated, $app could not be removed" -ForegroundColor Red
|
||||
continue
|
||||
}
|
||||
|
||||
$isEdgeId = $edgeIds -contains $app
|
||||
$appName = if ($isEdgeId) { 'Microsoft_Edge' } else { $app -replace '\.', '_' }
|
||||
|
||||
# Uninstall app via WinGet, or create a scheduled task to uninstall it later
|
||||
if ($script:Params.ContainsKey("User")) {
|
||||
if (-not ($isEdgeId -and $edgeScheduledTaskAdded)) {
|
||||
ImportRegistryFile "Adding scheduled task to uninstall $app for user $(GetUserName)..." "Uninstall_$($appName).reg"
|
||||
if ($isEdgeId) { $edgeScheduledTaskAdded = $true }
|
||||
}
|
||||
}
|
||||
elseif ($script:Params.ContainsKey("Sysprep")) {
|
||||
if (-not ($isEdgeId -and $edgeScheduledTaskAdded)) {
|
||||
ImportRegistryFile "Adding scheduled task to uninstall $app after for new users..." "Uninstall_$($appName).reg"
|
||||
if ($isEdgeId) { $edgeScheduledTaskAdded = $true }
|
||||
}
|
||||
}
|
||||
else {
|
||||
# Uninstall app via WinGet
|
||||
$wingetOutput = Invoke-NonBlocking -ScriptBlock {
|
||||
param($appId)
|
||||
winget uninstall --accept-source-agreements --disable-interactivity --id $appId
|
||||
} -ArgumentList $app
|
||||
|
||||
$wingetFailed = Select-String -InputObject $wingetOutput -Pattern "Uninstall failed with exit code|No installed package found matching input criteria|No package found matching input criteria" -SimpleMatch:$false
|
||||
if ($isEdgeId) {
|
||||
if (-not $wingetFailed) {
|
||||
$edgeUninstallSucceeded = $true
|
||||
}
|
||||
|
||||
# Prompt immediately after the final selected Edge ID attempt (if all attempts failed)
|
||||
$hasRemainingEdgeIds = $false
|
||||
if ($appIndex -lt $appCount) {
|
||||
$remainingApps = @($appsList)[($appIndex)..($appCount - 1)]
|
||||
$hasRemainingEdgeIds = @($remainingApps | Where-Object { $edgeIds -contains $_ }).Count -gt 0
|
||||
}
|
||||
|
||||
if (-not $hasRemainingEdgeIds -and -not $edgeUninstallSucceeded) {
|
||||
Write-Host "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red
|
||||
|
||||
if ($script:GuiWindow) {
|
||||
$result = Show-MessageBox -Message 'Unable to uninstall Microsoft Edge via WinGet. Would you like to forcefully uninstall it? NOT RECOMMENDED!' -Title 'Force Uninstall Microsoft Edge?' -Button 'YesNo' -Icon 'Warning'
|
||||
|
||||
if ($result -eq 'Yes') {
|
||||
Write-Host ""
|
||||
ForceRemoveEdge
|
||||
}
|
||||
}
|
||||
elseif ($( Read-Host -Prompt "Would you like to forcefully uninstall Microsoft Edge? NOT RECOMMENDED! (y/n)" ) -eq 'y') {
|
||||
Write-Host ""
|
||||
ForceRemoveEdge
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Microsoft Edge is handled after the loop to avoid duplicate scheduled tasks and allow fallback if winget fails
|
||||
if ($edgeIds -contains $app) {
|
||||
$edgeAppsInList += $app
|
||||
continue
|
||||
}
|
||||
|
||||
# Use Remove-AppxPackage to remove all other apps
|
||||
$appPattern = '*' + $app + '*'
|
||||
Write-Host "Removing $app"
|
||||
|
||||
try {
|
||||
switch ($targetUser) {
|
||||
"AllUsers" {
|
||||
# Remove installed app for all existing users, and from OS image
|
||||
Invoke-NonBlocking -ScriptBlock {
|
||||
param($pattern)
|
||||
Get-AppxPackage -Name $pattern -AllUsers | Remove-AppxPackage -AllUsers -ErrorAction Continue
|
||||
Get-AppxProvisionedPackage -Online | Where-Object { $_.PackageName -like $pattern } | ForEach-Object { Remove-ProvisionedAppxPackage -Online -AllUsers -PackageName $_.PackageName }
|
||||
} -ArgumentList $appPattern
|
||||
}
|
||||
"CurrentUser" {
|
||||
# Remove installed app for current user only
|
||||
Invoke-NonBlocking -ScriptBlock {
|
||||
param($pattern)
|
||||
Get-AppxPackage -Name $pattern | Remove-AppxPackage -ErrorAction Continue
|
||||
} -ArgumentList $appPattern
|
||||
}
|
||||
default {
|
||||
# Target is a specific username - remove app for that user only
|
||||
Invoke-NonBlocking -ScriptBlock {
|
||||
param($pattern, $user)
|
||||
$userAccount = New-Object System.Security.Principal.NTAccount($user)
|
||||
$userSid = $userAccount.Translate([System.Security.Principal.SecurityIdentifier]).Value
|
||||
Get-AppxPackage -Name $pattern -User $userSid | Remove-AppxPackage -User $userSid -ErrorAction Continue
|
||||
} -ArgumentList @($appPattern, $targetUser)
|
||||
}
|
||||
if ((Get-AppRemovalMethod $app) -eq 'WinGet') {
|
||||
Remove-WinGetApp -app $app
|
||||
$wingetRemovedApps += $app
|
||||
}
|
||||
else {
|
||||
Remove-AppxApp -app $app -targetUser $targetUser
|
||||
}
|
||||
}
|
||||
|
||||
# Remove Microsoft Edge
|
||||
if ($edgeAppsInList.Count -gt 0) {
|
||||
Remove-EdgeApp -edgeAppsInList $edgeAppsInList
|
||||
}
|
||||
|
||||
# Check whether any winget-removed apps are still present, and report errors for each one.
|
||||
if ($wingetRemovedApps.Count -gt 0 -or $edgeAppsInList.Count -gt 0) {
|
||||
$postRemovalList = if ($script:WingetInstalled) { GetInstalledAppsViaWinget -TimeOut 10 -NonBlocking } else { $null }
|
||||
foreach ($app in $wingetRemovedApps) {
|
||||
if (Test-AppStillInstalled -appId $app -InstalledList $postRemovalList) {
|
||||
Write-Host "Unable to uninstall $app via WinGet" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
catch {
|
||||
if ($DebugPreference -ne "SilentlyContinue") {
|
||||
Write-Host "Something went wrong while trying to remove $app" -ForegroundColor Yellow
|
||||
Write-Host $psitem.Exception.StackTrace -ForegroundColor Gray
|
||||
|
||||
# Verify Edge separately (triggers its own force-remove path if still installed)
|
||||
$edgeStillInstalled = $false
|
||||
foreach ($edgeApp in $edgeAppsInList) {
|
||||
if (Test-AppStillInstalled -appId $edgeApp -InstalledList $postRemovalList) {
|
||||
$edgeStillInstalled = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($edgeStillInstalled) {
|
||||
Write-Host "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red
|
||||
Request-EdgeForceRemove
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Uninstalls a non-Edge app via WinGet and/or schedules its removal.
|
||||
|
||||
.DESCRIPTION
|
||||
Runs winget uninstall for a single app. If the User or Sysprep
|
||||
parameter was passed, also schedules removal for future logins.
|
||||
After uninstall, the system is checked to confirm whether the app
|
||||
is still present — winget output is not trusted on its
|
||||
own, as it sometimes reports failure after a successful removal.
|
||||
Edge apps are handled separately after the main loop.
|
||||
|
||||
.PARAMETER app
|
||||
The WinGet package ID to uninstall (e.g. 'Microsoft.BingNews').
|
||||
#>
|
||||
function Remove-WinGetApp {
|
||||
param([string]$app)
|
||||
|
||||
if (-not $script:WingetInstalled) {
|
||||
Write-Host "ERROR: WinGet is either not installed or is outdated, $app could not be removed" -ForegroundColor Red
|
||||
return
|
||||
}
|
||||
|
||||
if ($script:Params.ContainsKey("User")) {
|
||||
Write-Host "Adding scheduled task to uninstall $app for user $(GetUserName)..."
|
||||
Set-RunOnceWingetTask -appId $app
|
||||
}
|
||||
elseif ($script:Params.ContainsKey("Sysprep")) {
|
||||
Write-Host "Adding scheduled task to uninstall $app for new users..."
|
||||
Set-RunOnceWingetTask -appId $app
|
||||
}
|
||||
|
||||
Invoke-NonBlocking -ScriptBlock {
|
||||
param($appId)
|
||||
winget uninstall --accept-source-agreements --disable-interactivity --id $appId
|
||||
} -ArgumentList $app
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Removes Microsoft Edge via WinGet (both AppIds), with fallback to force-remove.
|
||||
|
||||
.DESCRIPTION
|
||||
Edge has multiple package IDs. Runs winget uninstall for each one,
|
||||
then creates a single scheduled task if the User or Sysprep parameter
|
||||
was passed. After all attempts, the system is checked to confirm
|
||||
whether Edge is still present. The force-remove prompt only
|
||||
appears if Edge remains installed — winget false positives are ignored.
|
||||
|
||||
.PARAMETER edgeAppsInList
|
||||
The Edge AppIds that appear in the removal list (one or both).
|
||||
#>
|
||||
function Remove-EdgeApp {
|
||||
param([string[]]$edgeAppsInList)
|
||||
|
||||
if (-not $script:WingetInstalled) {
|
||||
Write-Host "ERROR: WinGet is either not installed or is outdated, Microsoft Edge could not be removed" -ForegroundColor Red
|
||||
return
|
||||
}
|
||||
|
||||
if ($script:Params.ContainsKey("User")) {
|
||||
Write-Host "Adding scheduled task to uninstall Microsoft Edge for user $(GetUserName)..."
|
||||
Set-RunOnceWingetTask -appId 'Microsoft.Edge'
|
||||
}
|
||||
elseif ($script:Params.ContainsKey("Sysprep")) {
|
||||
Write-Host "Adding scheduled task to uninstall Microsoft Edge for new users..."
|
||||
Set-RunOnceWingetTask -appId 'Microsoft.Edge'
|
||||
}
|
||||
|
||||
foreach ($edgeApp in $edgeAppsInList) {
|
||||
Write-Host "Removing $edgeApp"
|
||||
Invoke-NonBlocking -ScriptBlock {
|
||||
param($appId)
|
||||
winget uninstall --accept-source-agreements --disable-interactivity --id $appId
|
||||
} -ArgumentList $edgeApp
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Removes an app via Remove-AppxPackage / Remove-ProvisionedAppxPackage.
|
||||
|
||||
.PARAMETER app
|
||||
The package identifier to remove (e.g. 'Clipchamp.Clipchamp').
|
||||
|
||||
.PARAMETER targetUser
|
||||
Target scope: "AllUsers", "CurrentUser", or a specific username.
|
||||
#>
|
||||
function Remove-AppxApp {
|
||||
param([string]$app, [string]$targetUser)
|
||||
|
||||
$appPattern = '*' + $app + '*'
|
||||
|
||||
try {
|
||||
switch ($targetUser) {
|
||||
"AllUsers" {
|
||||
Invoke-NonBlocking -ScriptBlock {
|
||||
param($pattern)
|
||||
Get-AppxPackage -Name $pattern -AllUsers | Remove-AppxPackage -AllUsers -ErrorAction Continue
|
||||
Get-AppxProvisionedPackage -Online | Where-Object { $_.PackageName -like $pattern } | ForEach-Object { Remove-ProvisionedAppxPackage -Online -AllUsers -PackageName $_.PackageName }
|
||||
} -ArgumentList $appPattern
|
||||
}
|
||||
"CurrentUser" {
|
||||
Invoke-NonBlocking -ScriptBlock {
|
||||
param($pattern)
|
||||
Get-AppxPackage -Name $pattern | Remove-AppxPackage -ErrorAction Continue
|
||||
} -ArgumentList $appPattern
|
||||
}
|
||||
default {
|
||||
Invoke-NonBlocking -ScriptBlock {
|
||||
param($pattern, $user)
|
||||
$userAccount = New-Object System.Security.Principal.NTAccount($user)
|
||||
$userSid = $userAccount.Translate([System.Security.Principal.SecurityIdentifier]).Value
|
||||
Get-AppxPackage -Name $pattern -User $userSid | Remove-AppxPackage -User $userSid -ErrorAction Continue
|
||||
} -ArgumentList @($appPattern, $targetUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Verbose "Something went wrong while trying to remove $($app): $_"
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Checks whether an app package is still installed after a removal attempt.
|
||||
|
||||
.DESCRIPTION
|
||||
Checks Get-AppxPackage across all users first (fast, no process launch),
|
||||
then falls back to a pre-fetched or live winget list for non-Appx packages.
|
||||
Uses Test-AppInWingetList which provides exact-match-first with substring
|
||||
fallback against the parsed winget objects.
|
||||
Returns $true if the app is still present, $false otherwise.
|
||||
|
||||
.PARAMETER appId
|
||||
The package identifier to check (e.g. 'Microsoft.BingNews').
|
||||
|
||||
.PARAMETER InstalledList
|
||||
Optional pre-fetched array of winget objects from GetInstalledAppsViaWinget.
|
||||
When provided, used directly; otherwise a live winget call is made.
|
||||
#>
|
||||
function Test-AppStillInstalled {
|
||||
param(
|
||||
[string]$appId,
|
||||
[object[]]$InstalledList
|
||||
)
|
||||
|
||||
# Check Get-AppxPackage for all users first (fast, covers all Store apps).
|
||||
if (Get-AppxPackage -Name "$appId" -AllUsers -ErrorAction SilentlyContinue) {
|
||||
return $true
|
||||
}
|
||||
|
||||
# Use the pre-fetched list if provided; otherwise fall back to a live winget call.
|
||||
if ($InstalledList) {
|
||||
return (Test-AppInWingetList -appId $appId -InstalledList $InstalledList)
|
||||
}
|
||||
|
||||
if ($script:WingetInstalled) {
|
||||
$liveList = GetInstalledAppsViaWinget -TimeOut 10 -NonBlocking
|
||||
if (Test-AppInWingetList -appId $appId -InstalledList $liveList) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Warning "Unable to verify whether '$appId' is still installed (WinGet is unavailable)"
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Returns the removal method for an app identifier.
|
||||
|
||||
.DESCRIPTION
|
||||
Parses Apps.json once (cached in script scope) to build a lookup of
|
||||
AppId -> RemovalMethod. Returns 'WinGet' if the app should be removed
|
||||
via winget, or 'Appx' if via Remove-AppxPackage. Defaults to 'Appx'
|
||||
for unknown IDs.
|
||||
|
||||
.PARAMETER appId
|
||||
The package identifier (e.g. 'Clipchamp.Clipchamp').
|
||||
#>
|
||||
function Get-AppRemovalMethod {
|
||||
param([string]$appId)
|
||||
|
||||
if (-not $script:AppRemovalMethodCache) {
|
||||
$script:AppRemovalMethodCache = @{}
|
||||
try {
|
||||
if (Test-Path $script:AppsListFilePath) {
|
||||
$appsJson = Get-Content -Path $script:AppsListFilePath -Raw | ConvertFrom-Json
|
||||
foreach ($appData in $appsJson.Apps) {
|
||||
$rawMethod = $appData.RemovalMethod
|
||||
$method = if ($rawMethod -and $rawMethod -eq 'WinGet') { 'WinGet' } else { 'Appx' }
|
||||
if ($appData.AppId -is [array]) {
|
||||
foreach ($id in $appData.AppId) { $script:AppRemovalMethodCache[$id.Trim()] = $method }
|
||||
}
|
||||
else {
|
||||
$script:AppRemovalMethodCache[$appData.AppId.Trim()] = $method
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to load app removal methods from '$script:AppsListFilePath'. Defaulting unknown apps to Appx. Error: $_"
|
||||
}
|
||||
}
|
||||
|
||||
if ($script:AppRemovalMethodCache.ContainsKey($appId)) {
|
||||
return $script:AppRemovalMethodCache[$appId]
|
||||
}
|
||||
return 'Appx'
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Prompts the user to forcefully remove Microsoft Edge when winget cannot uninstall it.
|
||||
|
||||
.DESCRIPTION
|
||||
Only invoked after it has been confirmed that Edge is still present
|
||||
following all winget uninstall attempts. In GUI mode, displays a
|
||||
warning message box; in CLI mode, prompts via Read-Host. On
|
||||
confirmation, performs a force-remove of the Edge package.
|
||||
#>
|
||||
function Request-EdgeForceRemove {
|
||||
if ($script:GuiWindow) {
|
||||
$result = Show-MessageBox -Message 'Unable to uninstall Microsoft Edge via WinGet. Would you like to forcefully uninstall it? NOT RECOMMENDED!' -Title 'Force Uninstall Microsoft Edge?' -Button 'YesNo' -Icon 'Warning'
|
||||
if ($result -eq 'Yes') {
|
||||
Write-Host ""
|
||||
ForceRemoveEdge
|
||||
}
|
||||
}
|
||||
elseif ($(Read-Host -Prompt "Would you like to forcefully uninstall Microsoft Edge? NOT RECOMMENDED! (y/n)") -eq 'y') {
|
||||
Write-Host ""
|
||||
ForceRemoveEdge
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Dynamically sets a RunOnce registry key to schedule a winget uninstall.
|
||||
|
||||
.DESCRIPTION
|
||||
Writes directly to HKEY_USERS\Default\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce
|
||||
via the PowerShell registry API within Invoke-WithTargetUserHive,
|
||||
which handles hive loading and HKEY_USERS\Default → SID remapping.
|
||||
Used instead of static .reg files to avoid file dependency for each WinGet app.
|
||||
|
||||
The winget command is Base64-encoded and invoked via powershell.exe -EncodedCommand
|
||||
rather than interpolated directly into cmd.exe /c. This prevents shell metacharacters
|
||||
(such as &, |, <, >, ^, ") in the app ID from being interpreted as command syntax,
|
||||
even if future catalog updates introduce IDs containing those characters.
|
||||
|
||||
.PARAMETER appId
|
||||
The winget package ID to schedule for uninstall (e.g. 'XP9CXNGPPJ97XX').
|
||||
#>
|
||||
function Set-RunOnceWingetTask {
|
||||
param([string]$appId)
|
||||
|
||||
$targetUserName = if ($script:Params.ContainsKey("Sysprep")) { "Default" } else { $script:Params.Item("User") }
|
||||
|
||||
# Sanitize appId for use in registry value names (backslashes are path separators)
|
||||
$safeAppId = $appId.Replace('\', '_')
|
||||
|
||||
$taskName = "Uninstall_$safeAppId"
|
||||
|
||||
# Escape single quotes in appId, then wrap in single quotes so cmd/pwsh metacharacters
|
||||
# like & | < > ^ " are treated as literals. Base64-encode the whole command so the
|
||||
# RunOnce value contains only [A-Za-z0-9+/=] — safe in any shell parser.
|
||||
$escapedAppId = $appId.Replace("'", "''")
|
||||
$wingetCommand = "winget uninstall --accept-source-agreements --disable-interactivity --id '$escapedAppId'"
|
||||
$encodedWingetCommand = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($wingetCommand))
|
||||
|
||||
$operation = [PSCustomObject]@{
|
||||
KeyPath = 'HKEY_USERS\Default\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce'
|
||||
ValueName = $taskName
|
||||
ValueType = 'String'
|
||||
ValueData = "powershell.exe -NoProfile -EncodedCommand $encodedWingetCommand"
|
||||
OperationType = 'SetValue'
|
||||
}
|
||||
|
||||
try {
|
||||
Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock {
|
||||
param($op)
|
||||
Invoke-RegistryOperation -Operation $op -RegFilePath '<dynamic>'
|
||||
} -ArgumentObject $operation
|
||||
}
|
||||
catch {
|
||||
Write-Host "Failed to schedule uninstall task for $($appId): $_" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
43
Scripts/AppRemoval/Test-AppInWingetList.ps1
Normal file
43
Scripts/AppRemoval/Test-AppInWingetList.ps1
Normal file
@@ -0,0 +1,43 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Checks whether an app ID appears in a parsed winget installed list.
|
||||
|
||||
.DESCRIPTION
|
||||
Tries an exact match against the .Id property first. When that
|
||||
fails, falls back to a substring search guarded by a word-boundary
|
||||
regex so that short IDs don't accidentally match longer ones
|
||||
(e.g. 'Microsoft.Edge' will not match 'Microsoft.EdgeDev').
|
||||
|
||||
.PARAMETER appId
|
||||
The identifier to search for (e.g. 'Microsoft.Copilot').
|
||||
|
||||
.PARAMETER InstalledList
|
||||
An array of PSCustomObject from GetInstalledAppsViaWinget.
|
||||
#>
|
||||
function Test-AppInWingetList {
|
||||
param(
|
||||
[string]$appId,
|
||||
[object[]]$InstalledList
|
||||
)
|
||||
|
||||
if (-not $InstalledList) { return $false }
|
||||
|
||||
# Normalize to array
|
||||
$list = @($InstalledList)
|
||||
|
||||
# Exact match first (fast and precise)
|
||||
if ($list.Id -contains $appId) {
|
||||
return $true
|
||||
}
|
||||
|
||||
# Substring fallback with word-boundary guard
|
||||
$boundaryPattern = '(?<![a-zA-Z0-9])' + [regex]::Escape($appId) + '(?![a-zA-Z0-9])'
|
||||
|
||||
foreach ($entry in $list) {
|
||||
if ($entry.Id -like "*$appId*" -and $entry.Id -match $boundaryPattern) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
@@ -1,4 +1,17 @@
|
||||
# Prints all pending changes that will be made by the script
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Prints a summary of all pending changes to the console for the user to review.
|
||||
|
||||
.DESCRIPTION
|
||||
Iterates over every non-control parameter in $script:Params and emits a
|
||||
human-readable line for each change that will be applied. For the
|
||||
'RemoveApps' parameter the list of targeted app names is displayed
|
||||
inline. Feature labels are resolved from Features.json when available;
|
||||
otherwise the raw parameter name is used as a fallback.
|
||||
|
||||
After printing the summary the function pauses until the user presses
|
||||
Enter, giving them an opportunity to review and cancel via Ctrl+C.
|
||||
#>
|
||||
function PrintPendingChanges {
|
||||
Write-Output "Win11Debloat will make the following changes:"
|
||||
|
||||
@@ -31,28 +44,9 @@ function PrintPendingChanges {
|
||||
Write-Host $appsList -ForegroundColor DarkGray
|
||||
continue
|
||||
}
|
||||
'RemoveAppsCustom' {
|
||||
$appsList = LoadAppsFromFile $script:CustomAppsListFilePath
|
||||
|
||||
if ($appsList.Count -eq 0) {
|
||||
Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow
|
||||
Write-Output ""
|
||||
continue
|
||||
}
|
||||
|
||||
Write-Output "- Remove $($appsList.Count) apps:"
|
||||
Write-Host $appsList -ForegroundColor DarkGray
|
||||
continue
|
||||
}
|
||||
default {
|
||||
if ($script:Features -and $script:Features.ContainsKey($parameterName)) {
|
||||
$message = $script:Features[$parameterName].Label
|
||||
Write-Output "- $message"
|
||||
}
|
||||
else {
|
||||
# Fallback: show the parameter name if no feature description is available
|
||||
Write-Output "- $parameterName"
|
||||
}
|
||||
$message = $script:Features[$parameterName].Label
|
||||
Write-Output "- $message"
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ function ShowCLIAppRemoval {
|
||||
|
||||
if ($result -eq $true) {
|
||||
Write-Output "You have selected $($script:SelectedApps.Count) apps for removal"
|
||||
AddParameter 'RemoveAppsCustom'
|
||||
AddParameter 'RemoveApps'
|
||||
AddParameter 'Apps' ($script:SelectedApps -join ',')
|
||||
|
||||
SaveSettings
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ function ShowCLIDefaultModeOptions {
|
||||
AddParameter 'Apps' 'Default'
|
||||
}
|
||||
'2' {
|
||||
AddParameter 'RemoveAppsCustom'
|
||||
AddParameter 'RemoveApps'
|
||||
AddParameter 'Apps' ($script:SelectedApps -join ',')
|
||||
|
||||
if ($DisableGameBarIntegrationInput) {
|
||||
AddParameter 'DisableDVR'
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
function Get-FeatureId {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
$Feature
|
||||
)
|
||||
|
||||
$featureId = [string]$Feature.FeatureId
|
||||
if ([string]::IsNullOrWhiteSpace($featureId)) {
|
||||
throw 'Selected feature is missing required FeatureId.'
|
||||
}
|
||||
|
||||
return $featureId
|
||||
}
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Filters a list of features to those that have a non-empty RegistryKey.
|
||||
|
||||
.PARAMETER Features
|
||||
An array of feature objects to filter.
|
||||
#>
|
||||
function Get-RegistryBackedFeatures {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[object[]]$Features
|
||||
[object[]]$Features = @()
|
||||
)
|
||||
|
||||
return @($Features | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey) })
|
||||
|
||||
@@ -1,47 +1,44 @@
|
||||
function Get-RegistryBackupCapturePlans {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[object[]]$SelectedRegistryFeatures,
|
||||
[object[]]$SelectedRegistryFeatures = @(),
|
||||
[object[]]$UndoRegistryFeatures = @(),
|
||||
[switch]$UseSysprepRegFiles
|
||||
)
|
||||
|
||||
$planMap = @{}
|
||||
|
||||
foreach ($feature in $SelectedRegistryFeatures) {
|
||||
$regFilePath = Get-RegistryFilePathForFeature -Feature $feature -UseSysprepRegFiles:$UseSysprepRegFiles
|
||||
$regFilePath = Get-RegistryFilePathForFeature -RegistryKey $feature.RegistryKey -UseSysprepRegFiles:$UseSysprepRegFiles
|
||||
if (-not (Test-Path $regFilePath)) {
|
||||
throw "Unable to find registry file for backup: $($feature.RegistryKey) ($regFilePath)"
|
||||
}
|
||||
|
||||
foreach ($operation in @(Get-RegFileOperations -regFilePath $regFilePath)) {
|
||||
if (-not $operation.KeyPath) { continue }
|
||||
Add-RegistryPlanOperation -PlanMap $planMap -Operation $operation
|
||||
}
|
||||
}
|
||||
|
||||
$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)
|
||||
}
|
||||
foreach ($feature in $UndoRegistryFeatures) {
|
||||
$regFilePath = Resolve-RegistryBackupUndoFilePath -Feature $feature
|
||||
if ([string]::IsNullOrWhiteSpace($regFilePath)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not (Test-Path $regFilePath)) {
|
||||
$undoKeyDescription = if (-not [string]::IsNullOrWhiteSpace([string]$feature.RegistryUndoKey)) {
|
||||
[string]$feature.RegistryUndoKey
|
||||
}
|
||||
else {
|
||||
[string]$feature.RegistryKey
|
||||
}
|
||||
|
||||
$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)
|
||||
}
|
||||
}
|
||||
}
|
||||
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(
|
||||
[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) {
|
||||
@@ -92,31 +147,14 @@ function Invoke-WithLoadedBackupHive {
|
||||
$ArgumentObject = $null
|
||||
)
|
||||
|
||||
$hiveDatPath = if ($script:Params.ContainsKey('Sysprep')) {
|
||||
GetUserDirectory -userName 'Default' -fileName 'NTUSER.DAT'
|
||||
$targetUserName = if ($script:Params.ContainsKey('Sysprep')) {
|
||||
'Default'
|
||||
}
|
||||
else {
|
||||
GetUserDirectory -userName $script:Params.Item('User') -fileName 'NTUSER.DAT'
|
||||
$script:Params.Item('User')
|
||||
}
|
||||
|
||||
$global:LASTEXITCODE = 0
|
||||
reg load 'HKU\Default' "$hiveDatPath" | Out-Null
|
||||
$loadExitCode = $LASTEXITCODE
|
||||
if ($loadExitCode -ne 0) {
|
||||
throw "Failed to load user hive for registry backup at '$hiveDatPath' (exit code: $loadExitCode)"
|
||||
}
|
||||
|
||||
try {
|
||||
return & $ScriptBlock $ArgumentObject
|
||||
}
|
||||
finally {
|
||||
$global:LASTEXITCODE = 0
|
||||
reg unload 'HKU\Default' | Out-Null
|
||||
$unloadExitCode = $LASTEXITCODE
|
||||
if ($unloadExitCode -ne 0) {
|
||||
throw "Failed to unload registry hive 'HKU\Default' (exit code: $unloadExitCode)"
|
||||
}
|
||||
}
|
||||
return Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $ScriptBlock -ArgumentObject $ArgumentObject
|
||||
}
|
||||
|
||||
function Get-RegistryKeySnapshot {
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Creates a timestamped JSON backup of registry state for selected features.
|
||||
|
||||
.DESCRIPTION
|
||||
Resolves selected and undo features from the provided keys, captures their
|
||||
registry state, and saves the result as a JSON file in the Backups/ folder.
|
||||
Returns the file path on success, $null if no registry-backed features exist.
|
||||
|
||||
.PARAMETER ActionableKeys
|
||||
Param keys from $script:Params to resolve into apply features.
|
||||
|
||||
.PARAMETER ExtraFeatures
|
||||
Additional synthetic feature objects (e.g. undo features) to include.
|
||||
#>
|
||||
function New-RegistrySettingsBackup {
|
||||
param(
|
||||
[string[]]$ActionableKeys
|
||||
[string[]]$ActionableKeys,
|
||||
[object[]]$ExtraFeatures = @()
|
||||
)
|
||||
|
||||
$ActionableKeys = @($ActionableKeys)
|
||||
$selectedFeatures = Get-SelectedFeatures -ActionableKeys $ActionableKeys
|
||||
if (@($selectedFeatures | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey) }).Count -eq 0) {
|
||||
$selectedFeatures = @(Get-SelectedFeatures -ActionableKeys $ActionableKeys)
|
||||
$undoFeatures = @($ExtraFeatures | Where-Object { $_ -ne $null })
|
||||
$allFeatures = @($selectedFeatures) + @($undoFeatures)
|
||||
if (@($allFeatures | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey) }).Count -eq 0) {
|
||||
return $null
|
||||
}
|
||||
|
||||
@@ -18,7 +36,7 @@ function New-RegistrySettingsBackup {
|
||||
$backupFileName = 'Win11Debloat-RegistryBackup-{0}.json' -f $timestamp.ToString('yyyyMMdd_HHmmss')
|
||||
$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)) {
|
||||
throw "Failed to save registry backup to '$backupFilePath'"
|
||||
}
|
||||
@@ -29,6 +47,13 @@ function New-RegistrySettingsBackup {
|
||||
return $backupFilePath
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resolves param keys into deduplicated feature objects from the catalog.
|
||||
|
||||
.PARAMETER ActionableKeys
|
||||
Param keys to look up in $script:Features.
|
||||
#>
|
||||
function Get-SelectedFeatures {
|
||||
param(
|
||||
[string[]]$ActionableKeys
|
||||
@@ -43,8 +68,7 @@ function Get-SelectedFeatures {
|
||||
$feature = $script:Features[$paramKey]
|
||||
if (-not $feature) { continue }
|
||||
|
||||
$featureId = Get-FeatureId -Feature $feature
|
||||
|
||||
$featureId = [string]$feature.FeatureId
|
||||
if ($selectedFeatureIds.Add($featureId)) {
|
||||
$selectedFeatures.Add($feature)
|
||||
}
|
||||
@@ -53,10 +77,27 @@ function Get-SelectedFeatures {
|
||||
return @($selectedFeatures.ToArray())
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Builds the full backup payload object from selected and undo features.
|
||||
|
||||
.DESCRIPTION
|
||||
Deduplicates feature IDs, resolves registry capture plans, snapshots all
|
||||
registry keys, and assembles the final backup hashtable with metadata.
|
||||
|
||||
.PARAMETER SelectedFeatures
|
||||
Feature objects from the apply side.
|
||||
|
||||
.PARAMETER UndoFeatures
|
||||
Synthetic feature objects from the undo side.
|
||||
|
||||
.PARAMETER CreatedAt
|
||||
Timestamp recorded in the backup metadata.
|
||||
#>
|
||||
function Get-RegistryBackupPayload {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[object[]]$SelectedFeatures,
|
||||
[object[]]$SelectedFeatures = @(),
|
||||
[object[]]$UndoFeatures = @(),
|
||||
[Parameter(Mandatory)]
|
||||
[datetime]$CreatedAt
|
||||
)
|
||||
@@ -64,18 +105,29 @@ function Get-RegistryBackupPayload {
|
||||
$selectedFeatureIds = New-Object System.Collections.Generic.List[string]
|
||||
$seenSelectedFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($feature in $SelectedFeatures) {
|
||||
$featureId = Get-FeatureId -Feature $feature
|
||||
|
||||
$featureId = [string]$feature.FeatureId
|
||||
if ($seenSelectedFeatureIds.Add($featureId)) {
|
||||
$selectedFeatureIds.Add($featureId)
|
||||
}
|
||||
}
|
||||
|
||||
$selectedRegistryFeatures = Get-RegistryBackedFeatures -Features $SelectedFeatures
|
||||
$capturePlans = Get-RegistryBackupCapturePlans -SelectedRegistryFeatures $SelectedRegistryFeatures
|
||||
$selectedUndoFeatureIds = New-Object System.Collections.Generic.List[string]
|
||||
$seenUndoFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($feature in $UndoFeatures) {
|
||||
$featureId = [string]$feature.FeatureId
|
||||
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)
|
||||
|
||||
return @{
|
||||
$backupPayload = @{
|
||||
Version = '1.0'
|
||||
BackupType = 'RegistryState'
|
||||
CreatedAt = $CreatedAt.ToString('o')
|
||||
@@ -85,4 +137,10 @@ function Get-RegistryBackupPayload {
|
||||
SelectedFeatures = @($selectedFeatureIds)
|
||||
RegistryKeys = @($registryKeys)
|
||||
}
|
||||
|
||||
if ($selectedUndoFeatureIds.Count -gt 0) {
|
||||
$backupPayload['SelectedUndoFeatures'] = @($selectedUndoFeatureIds)
|
||||
}
|
||||
|
||||
return $backupPayload
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
# Disables Microsoft Store search suggestions in the start menu for all users by denying access to the Store app database file for each user
|
||||
function DisableStoreSearchSuggestionsForAllUsers {
|
||||
# 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 disable start search suggestions
|
||||
ForEach ($storeDbPath in $usersStoreDbPaths) {
|
||||
DisableStoreSearchSuggestions ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
|
||||
}
|
||||
|
||||
# Also disable start search suggestions for the default user profile
|
||||
$defaultStoreDbPath = GetUserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false
|
||||
DisableStoreSearchSuggestions $defaultStoreDbPath
|
||||
}
|
||||
|
||||
|
||||
# Disables Microsoft Store search suggestions in the start menu by denying access to the Store app database file
|
||||
function DisableStoreSearchSuggestions {
|
||||
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
|
||||
|
||||
# This file doesn't exist in EEA (No Store app suggestions).
|
||||
if (-not (Test-Path -Path $StoreAppsDatabase))
|
||||
{
|
||||
Write-Host "Unable to find Store app database for user $userName, creating it now to prevent Windows from creating it later..." -ForegroundColor Yellow
|
||||
|
||||
$storeDbDir = Split-Path -Path $StoreAppsDatabase -Parent
|
||||
|
||||
if (-not (Test-Path -Path $storeDbDir)) {
|
||||
New-Item -Path $storeDbDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
New-Item -Path $StoreAppsDatabase -ItemType File -Force | Out-Null
|
||||
}
|
||||
|
||||
$AccountSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0') # 'EVERYONE' group
|
||||
$Acl = Get-Acl -Path $StoreAppsDatabase
|
||||
$Ace = [System.Security.AccessControl.FileSystemAccessRule]::new($AccountSid, 'FullControl', 'Deny')
|
||||
$Acl.SetAccessRule($Ace) | Out-Null
|
||||
Set-Acl -Path $StoreAppsDatabase -AclObject $Acl | Out-Null
|
||||
|
||||
Write-Host "Disabled Microsoft Store search suggestions for user $userName"
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
# Executes a single parameter/feature based on its key
|
||||
# Parameters:
|
||||
# $paramKey - The parameter name to execute
|
||||
function ExecuteParameter {
|
||||
param (
|
||||
[string]$paramKey
|
||||
)
|
||||
|
||||
# Check if this feature has metadata in Features.json
|
||||
$feature = $null
|
||||
if ($script:Features.ContainsKey($paramKey)) {
|
||||
$feature = $script:Features[$paramKey]
|
||||
}
|
||||
|
||||
# If feature has RegistryKey and ApplyText, use dynamic ImportRegistryFile
|
||||
if ($feature -and $feature.RegistryKey -and $feature.ApplyText) {
|
||||
ImportRegistryFile "> $($feature.ApplyText)" $feature.RegistryKey
|
||||
|
||||
# Handle special cases that have additional logic after ImportRegistryFile
|
||||
switch ($paramKey) {
|
||||
'DisableBing' {
|
||||
# Also remove the app package for Bing search
|
||||
RemoveApps 'Microsoft.BingSearch'
|
||||
}
|
||||
'DisableCopilot' {
|
||||
# Also remove the app package for Copilot
|
||||
RemoveApps 'Microsoft.Copilot'
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
# Handle features without RegistryKey or with special logic
|
||||
switch ($paramKey) {
|
||||
'RemoveApps' {
|
||||
Write-Host "> Removing selected apps for $(GetFriendlyTargetUserName)..."
|
||||
$appsList = GenerateAppsList
|
||||
|
||||
if ($appsList.Count -eq 0) {
|
||||
Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "$($appsList.Count) apps selected for removal"
|
||||
RemoveApps $appsList
|
||||
}
|
||||
'RemoveAppsCustom' {
|
||||
Write-Host "> Removing selected apps..."
|
||||
$appsList = LoadAppsFromFile $script:CustomAppsListFilePath
|
||||
|
||||
if ($appsList.Count -eq 0) {
|
||||
Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "$($appsList.Count) apps selected for removal"
|
||||
RemoveApps $appsList
|
||||
}
|
||||
'RemoveCommApps' {
|
||||
$appsList = 'Microsoft.windowscommunicationsapps', 'Microsoft.People'
|
||||
Write-Host "> Removing Mail, Calendar and People apps..."
|
||||
RemoveApps $appsList
|
||||
return
|
||||
}
|
||||
'RemoveW11Outlook' {
|
||||
$appsList = 'Microsoft.OutlookForWindows'
|
||||
Write-Host "> Removing new Outlook for Windows app..."
|
||||
RemoveApps $appsList
|
||||
return
|
||||
}
|
||||
'RemoveGamingApps' {
|
||||
$appsList = 'Microsoft.GamingApp', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay'
|
||||
Write-Host "> Removing gaming related apps..."
|
||||
RemoveApps $appsList
|
||||
return
|
||||
}
|
||||
'RemoveHPApps' {
|
||||
$appsList = 'AD2F1837.HPAIExperienceCenter', 'AD2F1837.HPJumpStarts', 'AD2F1837.HPPCHardwareDiagnosticsWindows', 'AD2F1837.HPPowerManager', 'AD2F1837.HPPrivacySettings', 'AD2F1837.HPSupportAssistant', 'AD2F1837.HPSureShieldAI', 'AD2F1837.HPSystemInformation', 'AD2F1837.HPQuickDrop', 'AD2F1837.HPWorkWell', 'AD2F1837.myHP', 'AD2F1837.HPDesktopSupportUtilities', 'AD2F1837.HPQuickTouch', 'AD2F1837.HPEasyClean', 'AD2F1837.HPConnectedMusic', 'AD2F1837.HPFileViewer', 'AD2F1837.HPRegistration', 'AD2F1837.HPWelcome', 'AD2F1837.HPConnectedPhotopoweredbySnapfish', 'AD2F1837.HPPrinterControl'
|
||||
Write-Host "> Removing HP apps..."
|
||||
RemoveApps $appsList
|
||||
return
|
||||
}
|
||||
'DisableWidgets' {
|
||||
Write-Host "> Disabling widgets on the taskbar & lock screen..."
|
||||
# Stop widgets related processes before removing the app packages to prevent potential issues
|
||||
Get-Process *Widget* | Stop-Process
|
||||
|
||||
RemoveApps 'Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime'
|
||||
}
|
||||
"EnableWindowsSandbox" {
|
||||
Write-Host "> Enabling Windows Sandbox..."
|
||||
EnableWindowsFeature "Containers-DisposableClientVM"
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
"EnableWindowsSubsystemForLinux" {
|
||||
Write-Host "> Enabling Windows Subsystem for Linux..."
|
||||
EnableWindowsFeature "VirtualMachinePlatform"
|
||||
EnableWindowsFeature "Microsoft-Windows-Subsystem-Linux"
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
'ClearStart' {
|
||||
Write-Host "> Removing all pinned apps from the start menu for user $(GetUserName)..."
|
||||
ReplaceStartMenu
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
'ReplaceStart' {
|
||||
Write-Host "> Replacing the start menu for user $(GetUserName)..."
|
||||
ReplaceStartMenu $script:Params.Item("ReplaceStart")
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
'ClearStartAllUsers' {
|
||||
ReplaceStartMenuForAllUsers
|
||||
return
|
||||
}
|
||||
'ReplaceStartAllUsers' {
|
||||
ReplaceStartMenuForAllUsers $script:Params.Item("ReplaceStartAllUsers")
|
||||
return
|
||||
}
|
||||
'DisableStoreSearchSuggestions' {
|
||||
if ($script:Params.ContainsKey("Sysprep")) {
|
||||
Write-Host "> Disabling Microsoft Store search suggestions in the start menu for all users..."
|
||||
DisableStoreSearchSuggestionsForAllUsers
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "> Disabling Microsoft Store search suggestions for user $(GetUserName)..."
|
||||
DisableStoreSearchSuggestions
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Executes all selected parameters/features
|
||||
function ExecuteAllChanges {
|
||||
$script:RegistryImportFailures = 0
|
||||
|
||||
# Build list of actionable parameters (skip control params and data-only params)
|
||||
$actionableKeys = @()
|
||||
foreach ($paramKey in $script:Params.Keys) {
|
||||
if ($script:ControlParams -contains $paramKey) { continue }
|
||||
if ($paramKey -eq 'Apps') { continue }
|
||||
if ($paramKey -eq 'CreateRestorePoint') { continue }
|
||||
$actionableKeys += $paramKey
|
||||
}
|
||||
|
||||
$hasRegistryBackedFeature = $false
|
||||
foreach ($paramKey in $actionableKeys) {
|
||||
if (-not $script:Features.ContainsKey($paramKey)) { continue }
|
||||
|
||||
$feature = $script:Features[$paramKey]
|
||||
if ($feature -and -not [string]::IsNullOrWhiteSpace([string]$feature.RegistryKey)) {
|
||||
$hasRegistryBackedFeature = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
$totalSteps = $actionableKeys.Count
|
||||
if ($hasRegistryBackedFeature) { $totalSteps++ }
|
||||
if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ }
|
||||
$currentStep = 0
|
||||
|
||||
if ($hasRegistryBackedFeature) {
|
||||
$currentStep++
|
||||
if ($script:ApplyProgressCallback) {
|
||||
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating registry backup..."
|
||||
}
|
||||
|
||||
Write-Host "> Creating registry backup..."
|
||||
try {
|
||||
New-RegistrySettingsBackup -ActionableKeys $actionableKeys | Out-Null
|
||||
}
|
||||
catch {
|
||||
throw "Registry backup failed before applying changes. $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
# Create restore point if requested (CLI only - GUI handles this separately)
|
||||
if ($script:Params.ContainsKey("CreateRestorePoint")) {
|
||||
$currentStep++
|
||||
if ($script:ApplyProgressCallback) {
|
||||
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating system restore point, this may take a moment..."
|
||||
}
|
||||
Write-Host "> Creating a system restore point..."
|
||||
CreateSystemRestorePoint
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Execute all parameters
|
||||
foreach ($paramKey in $actionableKeys) {
|
||||
if ($script:CancelRequested) {
|
||||
return
|
||||
}
|
||||
|
||||
$currentStep++
|
||||
|
||||
# Get friendly name for the step
|
||||
$stepName = $paramKey
|
||||
if ($script:Features.ContainsKey($paramKey)) {
|
||||
$feature = $script:Features[$paramKey]
|
||||
if ($feature.ApplyText) {
|
||||
# Prefer explicit ApplyText when provided
|
||||
$stepName = $feature.ApplyText
|
||||
} elseif ($feature.Label) {
|
||||
# Fallback: use label from Features.json
|
||||
$stepName = $feature.Label
|
||||
}
|
||||
}
|
||||
|
||||
if ($script:ApplyProgressCallback) {
|
||||
& $script:ApplyProgressCallback $currentStep $totalSteps $stepName
|
||||
}
|
||||
|
||||
ExecuteParameter -paramKey $paramKey
|
||||
}
|
||||
|
||||
if ($script:RegistryImportFailures -gt 0) {
|
||||
Write-Host ""
|
||||
Write-Host "$($script:RegistryImportFailures) registry import change(s) failed. See output above for details." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
198
Scripts/Features/GetCurrentTweakState.ps1
Normal file
198
Scripts/Features/GetCurrentTweakState.ps1
Normal file
@@ -0,0 +1,198 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Maps a .reg file value type string to its RegistryValueKind enum.
|
||||
|
||||
.PARAMETER Operation
|
||||
A parsed .reg operation object containing a ValueType property.
|
||||
#>
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Tests whether a feature's registry operations currently match the live registry.
|
||||
|
||||
.DESCRIPTION
|
||||
Returns $true when ALL operations in the apply .reg file match current system
|
||||
state. Returns $false if the feature has no RegistryKey, the reg file is
|
||||
missing, or any operation mismatches. Special-cased features (Widgets, Store
|
||||
suggestions, Windows Sandbox, WSL) bypass .reg checking entirely.
|
||||
|
||||
.PARAMETER FeatureId
|
||||
The feature identifier to test.
|
||||
#>
|
||||
function Test-FeatureApplied {
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
[string]$FeatureId
|
||||
)
|
||||
|
||||
$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 = GetStoreAppsDatabasePathForUser -UserName (GetUserName)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Returns the 1-based index of the UiGroup option whose features all match
|
||||
current system state.
|
||||
|
||||
.DESCRIPTION
|
||||
Returns 0 if no option fully matches, meaning the current state is unknown
|
||||
or represents "No Change".
|
||||
|
||||
.PARAMETER Group
|
||||
A UiGroup object whose Values array contains options with FeatureIds.
|
||||
#>
|
||||
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,13 +8,7 @@ function ImportRegistryFile {
|
||||
Write-Host $message
|
||||
|
||||
$usesOfflineHive = $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User")
|
||||
$regFileDirectory = if ($usesOfflineHive) {
|
||||
Join-Path $script:RegfilesPath "Sysprep"
|
||||
}
|
||||
else {
|
||||
$script:RegfilesPath
|
||||
}
|
||||
$regFilePath = Join-Path $regFileDirectory $path
|
||||
$regFilePath = Get-RegistryFilePathForFeature -RegistryKey $path
|
||||
|
||||
if (-not (Test-Path $regFilePath)) {
|
||||
$errorMessage = "Unable to find registry file: $path ($regFilePath)"
|
||||
@@ -24,24 +18,25 @@ function ImportRegistryFile {
|
||||
throw $errorMessage
|
||||
}
|
||||
|
||||
$regResult = $null
|
||||
$offlineHiveLoaded = $false
|
||||
$importScript = {
|
||||
param($targetRegFilePath, $hiveContext)
|
||||
|
||||
try {
|
||||
if ($usesOfflineHive) {
|
||||
# Sysprep targets Default user, User targets the specified user
|
||||
$targetUserName = if ($script:Params.ContainsKey("Sysprep")) { "Default" } else { $script:Params.Item("User") }
|
||||
$hiveDatPath = GetUserDirectory -userName $targetUserName -fileName "NTUSER.DAT"
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
|
||||
$global:LASTEXITCODE = 0
|
||||
reg load "HKU\Default" $hiveDatPath | Out-Null
|
||||
$loadExitCode = $LASTEXITCODE
|
||||
# When the target user's hive is already loaded under their SID, the .reg file's
|
||||
# HKEY_USERS\Default paths won't match. Use the PowerShell registry writer instead,
|
||||
# which remaps Default → SID via Split-RegistryPath.
|
||||
$usePowerShellFallbackOnly = $hiveContext -and [bool]$hiveContext.WasAlreadyLoaded
|
||||
|
||||
if ($loadExitCode -ne 0) {
|
||||
throw "Failed importing registry file '$path'. Offline hive load failed: Failed to load user hive at '$hiveDatPath' (exit code: $loadExitCode)"
|
||||
}
|
||||
|
||||
$offlineHiveLoaded = $true
|
||||
if ($usePowerShellFallbackOnly) {
|
||||
Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath
|
||||
Write-Host "The operation completed successfully via PowerShell registry writer."
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
|
||||
$regResult = Invoke-NonBlocking -ScriptBlock {
|
||||
@@ -72,7 +67,7 @@ function ImportRegistryFile {
|
||||
}
|
||||
|
||||
return $result
|
||||
} -ArgumentList $regFilePath
|
||||
} -ArgumentList $targetRegFilePath
|
||||
|
||||
$regOutput = @($regResult.Output)
|
||||
$hasSuccess = ($regResult.ExitCode -eq 0) -and -not $regResult.Error
|
||||
@@ -94,26 +89,26 @@ function ImportRegistryFile {
|
||||
if (-not $hasSuccess) {
|
||||
$details = if ($regResult.Error) { $regResult.Error } else { "Exit code: $($regResult.ExitCode)" }
|
||||
Write-Warning "reg import failed for '$path'. Falling back to PowerShell registry writer. Details: $details"
|
||||
Invoke-RegistryOperationsFromRegFile -RegFilePath $regFilePath
|
||||
Write-Host "Fallback import succeeded for '$path'." -ForegroundColor Yellow
|
||||
Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath
|
||||
Write-Host "The operation completed successfully via PowerShell registry writer."
|
||||
}
|
||||
|
||||
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 ""
|
||||
}
|
||||
finally {
|
||||
if ($offlineHiveLoaded) {
|
||||
$global:LASTEXITCODE = 0
|
||||
reg unload "HKU\Default" | Out-Null
|
||||
$unloadExitCode = $LASTEXITCODE
|
||||
|
||||
if ($unloadExitCode -ne 0) {
|
||||
Write-Warning "Failed to unload registry hive HKU\Default after importing '$path' (exit code: $unloadExitCode)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
425
Scripts/Features/InvokeChanges.ps1
Normal file
425
Scripts/Features/InvokeChanges.ps1
Normal file
@@ -0,0 +1,425 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Applies a single feature/debloat operation.
|
||||
|
||||
.DESCRIPTION
|
||||
Handles two categories of features:
|
||||
- Registry-backed: imports the .reg file via ImportRegistryFile, then runs
|
||||
any post-import side effects (e.g., removing companion app packages).
|
||||
- Custom logic: app removal, Windows optional features, start menu
|
||||
replacement, and other special-case features.
|
||||
#>
|
||||
function Invoke-FeatureApply {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$FeatureId
|
||||
)
|
||||
|
||||
# Resolve feature metadata from Features.json
|
||||
$feature = $script:Features[$FeatureId]
|
||||
$applyText = $feature.ApplyText
|
||||
|
||||
# ---- Registry-backed features: import .reg file, then handle side effects ----
|
||||
if ($feature.RegistryKey) {
|
||||
ImportRegistryFile "> $applyText..." $feature.RegistryKey
|
||||
|
||||
# Post-import side effects for specific features
|
||||
switch ($FeatureId) {
|
||||
'DisableBing' {
|
||||
# Also remove the app package for Bing search
|
||||
RemoveApps @('Microsoft.BingSearch')
|
||||
}
|
||||
'DisableCopilot' {
|
||||
# Also remove the app packages for Copilot
|
||||
RemoveApps @('Microsoft.Copilot', 'XP9CXNGPPJ97XX')
|
||||
}
|
||||
'DisableTelemetry' {
|
||||
# Also disable telemetry scheduled tasks
|
||||
Disable-TelemetryScheduledTasks
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
# ---- Custom features (no registry backing, or special handling required) ----
|
||||
switch ($FeatureId) {
|
||||
'RemoveApps' {
|
||||
Write-Host "> $applyText for $(GetFriendlyTargetUserName)..."
|
||||
$appsList = GenerateAppsList
|
||||
|
||||
if ($appsList.Count -eq 0) {
|
||||
Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "$($appsList.Count) apps selected for removal"
|
||||
RemoveApps $appsList
|
||||
return
|
||||
}
|
||||
'RemoveGamingApps' {
|
||||
$appsList = @('Microsoft.GamingApp', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay')
|
||||
Write-Host "> $applyText..."
|
||||
RemoveApps $appsList
|
||||
return
|
||||
}
|
||||
'RemoveHPApps' {
|
||||
$appsList = @('AD2F1837.HPAIExperienceCenter', 'AD2F1837.HPJumpStarts', 'AD2F1837.HPPCHardwareDiagnosticsWindows', 'AD2F1837.HPPowerManager', 'AD2F1837.HPPrivacySettings', 'AD2F1837.HPSupportAssistant', 'AD2F1837.HPSureShieldAI', 'AD2F1837.HPSystemInformation', 'AD2F1837.HPQuickDrop', 'AD2F1837.HPWorkWell', 'AD2F1837.myHP', 'AD2F1837.HPDesktopSupportUtilities', 'AD2F1837.HPQuickTouch', 'AD2F1837.HPEasyClean', 'AD2F1837.HPConnectedMusic', 'AD2F1837.HPFileViewer', 'AD2F1837.HPRegistration', 'AD2F1837.HPWelcome', 'AD2F1837.HPConnectedPhotopoweredbySnapfish', 'AD2F1837.HPPrinterControl')
|
||||
Write-Host "> $applyText..."
|
||||
RemoveApps $appsList
|
||||
return
|
||||
}
|
||||
'DisableWidgets' {
|
||||
Write-Host "> $applyText..."
|
||||
# Stop widgets related processes before removing the app packages to prevent potential issues
|
||||
if (-not $script:Params.ContainsKey("WhatIf")) {
|
||||
Get-Process *Widget* -ErrorAction SilentlyContinue | Stop-Process
|
||||
}
|
||||
|
||||
RemoveApps @('Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime')
|
||||
return
|
||||
}
|
||||
'EnableWindowsSandbox' {
|
||||
Write-Host "> $applyText..."
|
||||
EnableWindowsFeature "Containers-DisposableClientVM"
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
'EnableWindowsSubsystemForLinux' {
|
||||
Write-Host "> $applyText..."
|
||||
EnableWindowsFeature "VirtualMachinePlatform"
|
||||
EnableWindowsFeature "Microsoft-Windows-Subsystem-Linux"
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
'ClearStart' {
|
||||
Write-Host "> $applyText for user $(GetUserName)..."
|
||||
$startMenuBinFile = GetStartMenuBinPathForUser -UserName (GetUserName)
|
||||
if (-not [string]::IsNullOrWhiteSpace($startMenuBinFile)) {
|
||||
ReplaceStartMenu -startMenuBinFile $startMenuBinFile
|
||||
}
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
'ReplaceStart' {
|
||||
Write-Host "> $applyText for user $(GetUserName)..."
|
||||
$startMenuBinFile = GetStartMenuBinPathForUser -UserName (GetUserName)
|
||||
if (-not [string]::IsNullOrWhiteSpace($startMenuBinFile)) {
|
||||
ReplaceStartMenu -startMenuBinFile $startMenuBinFile -startMenuTemplate $script:Params.Item("ReplaceStart")
|
||||
}
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
'ClearStartAllUsers' {
|
||||
ReplaceStartMenuForAllUsers
|
||||
return
|
||||
}
|
||||
'ReplaceStartAllUsers' {
|
||||
ReplaceStartMenuForAllUsers -startMenuTemplate $script:Params.Item("ReplaceStartAllUsers")
|
||||
return
|
||||
}
|
||||
'DisableStoreSearchSuggestions' {
|
||||
if ($script:Params.ContainsKey("Sysprep")) {
|
||||
Write-Host "> Disabling Microsoft Store search suggestions in the start menu for all users..."
|
||||
DisableStoreSearchSuggestionsForAllUsers
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "> Disabling Microsoft Store search suggestions for user $(GetUserName)..."
|
||||
$storeDb = GetStoreAppsDatabasePathForUser -UserName (GetUserName)
|
||||
if ($storeDb) {
|
||||
DisableStoreSearchSuggestions -StoreAppsDatabase $storeDb
|
||||
}
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Undoes a single feature that has no RegistryUndoKey.
|
||||
|
||||
.DESCRIPTION
|
||||
Handles undo for features that require custom logic rather than a simple
|
||||
.reg file import. Features with a RegistryUndoKey are handled directly
|
||||
via ImportRegistryFile in Invoke-UndoFeatures.
|
||||
#>
|
||||
function Invoke-FeatureUndo {
|
||||
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)..."
|
||||
$storeDb = GetStoreAppsDatabasePathForUser -UserName (GetUserName)
|
||||
if ($storeDb) {
|
||||
EnableStoreSearchSuggestions -StoreAppsDatabase $storeDb
|
||||
}
|
||||
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
|
||||
}
|
||||
'DisableTelemetry' {
|
||||
# Also re-enable telemetry scheduled tasks
|
||||
Enable-TelemetryScheduledTasks
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resolves the path of an undo .reg file relative to $script:RegfilesPath.
|
||||
|
||||
.DESCRIPTION
|
||||
Checks the Undo/ subfolder first, then falls back to the root Regfiles/
|
||||
folder. This allows undo files to be organized separately from apply files.
|
||||
#>
|
||||
function Resolve-UndoRegFilePath {
|
||||
param([string]$FileName)
|
||||
|
||||
$undoSubPath = Join-Path 'Undo' $FileName
|
||||
if (Test-Path (Join-Path $script:RegfilesPath $undoSubPath)) {
|
||||
return $undoSubPath
|
||||
}
|
||||
return $FileName
|
||||
}
|
||||
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Applies a list of features, reporting progress for each.
|
||||
|
||||
.DESCRIPTION
|
||||
Iterates through the provided feature IDs and calls Invoke-FeatureApply
|
||||
for each. Handles progress callbacks (GUI mode) and cancellation checks.
|
||||
This is called by Invoke-AllChanges during the apply phase.
|
||||
#>
|
||||
function Invoke-ApplyFeatures {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string[]]$FeatureIds,
|
||||
[Parameter(Mandatory)]
|
||||
[int]$StartStep,
|
||||
[Parameter(Mandatory)]
|
||||
[int]$TotalSteps
|
||||
)
|
||||
|
||||
if ($FeatureIds.Count -eq 0) { return }
|
||||
|
||||
$step = $StartStep
|
||||
foreach ($featureId in $FeatureIds) {
|
||||
if ($script:CancelRequested) { return }
|
||||
|
||||
# Resolve display name for the progress indicator
|
||||
$f = $script:Features[$featureId]
|
||||
$displayName = $f.ApplyText
|
||||
|
||||
if ($script:ApplyProgressCallback) {
|
||||
& $script:ApplyProgressCallback $step $TotalSteps $displayName
|
||||
}
|
||||
|
||||
Invoke-FeatureApply -FeatureId $featureId
|
||||
$step++
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Undoes a list of features, reporting progress for each.
|
||||
|
||||
.DESCRIPTION
|
||||
Iterates through the provided feature IDs. Features with a RegistryUndoKey
|
||||
are handled by importing the undo .reg file; all others delegate to
|
||||
Invoke-FeatureUndo for custom undo logic.
|
||||
This is called by Invoke-AllChanges during the undo phase.
|
||||
#>
|
||||
function Invoke-UndoFeatures {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string[]]$FeatureIds,
|
||||
[Parameter(Mandatory)]
|
||||
[int]$StartStep,
|
||||
[Parameter(Mandatory)]
|
||||
[int]$TotalSteps
|
||||
)
|
||||
|
||||
if ($FeatureIds.Count -eq 0) { return }
|
||||
|
||||
$step = $StartStep
|
||||
foreach ($featureId in $FeatureIds) {
|
||||
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 }
|
||||
$undoText = if ($f -and $f.ApplyUndoText) { $f.ApplyUndoText } else { $undoLabel }
|
||||
|
||||
if ($script:ApplyProgressCallback) {
|
||||
& $script:ApplyProgressCallback $step $TotalSteps $undoText
|
||||
}
|
||||
|
||||
if ($f -and $f.RegistryUndoKey) {
|
||||
ImportRegistryFile "> $undoText" (Resolve-UndoRegFilePath $f.RegistryUndoKey)
|
||||
}
|
||||
|
||||
Invoke-FeatureUndo -FeatureId $featureId
|
||||
$step++
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Main orchestrator: applies and undoes all selected features.
|
||||
|
||||
.DESCRIPTION
|
||||
Sequenced in four phases:
|
||||
1. Registry backup
|
||||
2. System restore point
|
||||
3. Apply phase - applies all selected features via Invoke-ApplyFeatures
|
||||
4. Undo phase - undoes selected features via Invoke-UndoFeatures
|
||||
|
||||
Progress is reported through $script:ApplyProgressCallback when set
|
||||
(used by the GUI modal). Cancellation is checked between each step.
|
||||
#>
|
||||
function Invoke-AllChanges {
|
||||
# Guard: prevent running as SYSTEM account without explicit target 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
|
||||
|
||||
# ---- Gather work items ----
|
||||
$applyIds = @()
|
||||
foreach ($key in $script:Params.Keys) {
|
||||
if ($script:ControlParams -contains $key) { continue }
|
||||
if ($key -eq 'Apps') { continue }
|
||||
if ($key -eq 'CreateRestorePoint') { continue }
|
||||
$applyIds += $key
|
||||
}
|
||||
$undoIds = @($script:UndoParams.Keys)
|
||||
|
||||
# ---- Determine if registry backup is needed ----
|
||||
$needsBackup = $false
|
||||
foreach ($id in $applyIds) {
|
||||
$f = $script:Features[$id]
|
||||
if ($f -and -not [string]::IsNullOrWhiteSpace([string]$f.RegistryKey)) {
|
||||
$needsBackup = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $needsBackup) {
|
||||
foreach ($id in $undoIds) {
|
||||
$f = if ($script:Features.ContainsKey($id)) { $script:Features[$id] } else { $null }
|
||||
if ($f -and $f.RegistryUndoKey) { $needsBackup = $true; break }
|
||||
}
|
||||
}
|
||||
|
||||
# ---- Calculate total progress steps ----
|
||||
$totalSteps = $applyIds.Count + $undoIds.Count
|
||||
if ($needsBackup) { $totalSteps++ }
|
||||
if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ }
|
||||
$step = 0
|
||||
|
||||
# ================================================================
|
||||
# Phase 1: Registry backup
|
||||
# ================================================================
|
||||
if ($needsBackup) {
|
||||
$step++
|
||||
if ($script:ApplyProgressCallback) {
|
||||
& $script:ApplyProgressCallback $step $totalSteps "Creating registry backup..."
|
||||
}
|
||||
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Write-Host "[WhatIf] Create registry backup" -ForegroundColor Cyan
|
||||
}
|
||||
else {
|
||||
Write-Host "> Creating registry backup..."
|
||||
try {
|
||||
$undoSyntheticFeatures = @($undoIds | 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 $applyIds -ExtraFeatures $undoSyntheticFeatures | Out-Null
|
||||
}
|
||||
catch {
|
||||
throw "Registry backup failed before applying changes. $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ================================================================
|
||||
# Phase 2: System restore point
|
||||
# ================================================================
|
||||
if ($script:Params.ContainsKey("CreateRestorePoint")) {
|
||||
$step++
|
||||
if ($script:ApplyProgressCallback) {
|
||||
& $script:ApplyProgressCallback $step $totalSteps "Creating system restore point, this may take a moment..."
|
||||
}
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Write-Host "[WhatIf] Create system restore point" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
}
|
||||
else {
|
||||
Write-Host "> Creating a system restore point..."
|
||||
CreateSystemRestorePoint
|
||||
Write-Host ""
|
||||
}
|
||||
}
|
||||
|
||||
# ================================================================
|
||||
# Phase 3: Apply features
|
||||
# ================================================================
|
||||
if ($applyIds.Count -gt 0) {
|
||||
Invoke-ApplyFeatures -FeatureIds $applyIds -StartStep ($step + 1) -TotalSteps $totalSteps
|
||||
$step += $applyIds.Count
|
||||
}
|
||||
|
||||
# ================================================================
|
||||
# Phase 4: Undo features
|
||||
# ================================================================
|
||||
if ($undoIds.Count -gt 0) {
|
||||
Invoke-UndoFeatures -FeatureIds $undoIds -StartStep ($step + 1) -TotalSteps $totalSteps
|
||||
$step += $undoIds.Count
|
||||
}
|
||||
|
||||
# ================================================================
|
||||
# Final: Report registry import failures
|
||||
# ================================================================
|
||||
if ($script:RegistryImportFailures -gt 0) {
|
||||
Write-Host ""
|
||||
Write-Host "$($script:RegistryImportFailures) registry import change(s) failed. See output above for details." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
@@ -33,12 +33,49 @@ function Get-NormalizedSelectedFeatureIdsFromBackup {
|
||||
$errors.Add('SelectedFeatures must contain non-empty string feature IDs.')
|
||||
}
|
||||
|
||||
if ($selectedFeatures.Count -eq 0) {
|
||||
$errors.Add('SelectedFeatures must contain at least one feature ID.')
|
||||
return [PSCustomObject]@{
|
||||
SelectedFeatures = $selectedFeatures.ToArray()
|
||||
Errors = $errors.ToArray()
|
||||
}
|
||||
}
|
||||
|
||||
function 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]@{
|
||||
SelectedFeatures = $selectedFeatures.ToArray()
|
||||
SelectedUndoFeatures = $selectedUndoFeatures.ToArray()
|
||||
Errors = $errors.ToArray()
|
||||
}
|
||||
}
|
||||
@@ -96,6 +133,9 @@ function Test-RegistryBackupMatchesSelectedFeatures {
|
||||
[AllowEmptyCollection()]
|
||||
[string[]]$SelectedFeatureIds,
|
||||
[Parameter(Mandatory)]
|
||||
[AllowEmptyCollection()]
|
||||
[string[]]$SelectedUndoFeatureIds,
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Target,
|
||||
[Parameter(Mandatory)]
|
||||
[AllowEmptyCollection()]
|
||||
@@ -109,18 +149,19 @@ function Test-RegistryBackupMatchesSelectedFeatures {
|
||||
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:*')
|
||||
|
||||
$capturePlans = @()
|
||||
if ($errors.Count -eq 0 -and $selectedRegistryFeatures.Count -gt 0) {
|
||||
$capturePlans = @(Get-RegistryBackupCapturePlans -SelectedRegistryFeatures @($selectedRegistryFeatures) -UseSysprepRegFiles:$useSysprepRegFiles)
|
||||
if ($errors.Count -eq 0 -and ($selectedRegistryFeatures.Count -gt 0 -or $undoRegistryFeatures.Count -gt 0)) {
|
||||
$capturePlans = @(Get-RegistryBackupCapturePlans -SelectedRegistryFeatures @($selectedRegistryFeatures) -UndoRegistryFeatures @($undoRegistryFeatures) -UseSysprepRegFiles:$useSysprepRegFiles)
|
||||
}
|
||||
|
||||
$planMap = New-RegistryBackupAllowListPlanMap -CapturePlans @($capturePlans)
|
||||
|
||||
if ($planMap.Count -eq 0 -and @($RegistryKeys).Count -gt 0) {
|
||||
$errors.Add('Backup contains registry snapshots but no allowed registry paths were derived from SelectedFeatures.')
|
||||
$errors.Add('Backup contains registry snapshots but no allowed registry paths were derived from the selected features.')
|
||||
}
|
||||
|
||||
foreach ($rootSnapshot in @($RegistryKeys)) {
|
||||
@@ -136,6 +177,8 @@ function Get-SelectedRegistryFeaturesForBackupValidation {
|
||||
[AllowEmptyCollection()]
|
||||
[string[]]$SelectedFeatureIds,
|
||||
[Parameter(Mandatory)]
|
||||
[bool]$IsUndoFeature,
|
||||
[Parameter(Mandatory)]
|
||||
[AllowEmptyCollection()]
|
||||
$Errors
|
||||
)
|
||||
@@ -152,7 +195,26 @@ function Get-SelectedRegistryFeaturesForBackupValidation {
|
||||
}
|
||||
|
||||
$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)
|
||||
}
|
||||
}
|
||||
@@ -323,7 +385,7 @@ function Find-RegistryAllowListPlanMatch {
|
||||
continue
|
||||
}
|
||||
|
||||
$subKeyPrefix = "$($plan.NormalizedPath)\\"
|
||||
$subKeyPrefix = "$($plan.NormalizedPath)\"
|
||||
if ($NormalizedPath.StartsWith($subKeyPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
return [PSCustomObject]@{
|
||||
IsDescendant = $true
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
# Replace the startmenu for all users, when using the default startmenuTemplate this clears all pinned apps
|
||||
# Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Replaces the start menu layout for all user profiles.
|
||||
|
||||
.DESCRIPTION
|
||||
Iterates over every existing user profile and the Default user profile,
|
||||
replacing each user's start2.bin file with the specified template. When
|
||||
using the default template, this clears all pinned apps from the start menu.
|
||||
|
||||
Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/
|
||||
|
||||
.PARAMETER startMenuTemplate
|
||||
Path to the .bin template file to apply. Defaults to the blank template
|
||||
bundled with the script (Assets/Start/start2.bin).
|
||||
|
||||
.EXAMPLE
|
||||
ReplaceStartMenuForAllUsers
|
||||
|
||||
.EXAMPLE
|
||||
ReplaceStartMenuForAllUsers -startMenuTemplate "C:\CustomLayout.bin"
|
||||
#>
|
||||
function ReplaceStartMenuForAllUsers {
|
||||
param (
|
||||
$startMenuTemplate = "$script:AssetsPath\Start\start2.bin"
|
||||
[string]$startMenuTemplate = "$script:AssetsPath\Start\start2.bin"
|
||||
)
|
||||
|
||||
Write-Host "> Removing all pinned apps from the start menu for all users..."
|
||||
@@ -20,12 +39,17 @@ function ReplaceStartMenuForAllUsers {
|
||||
|
||||
# Go through all users and replace the start menu file
|
||||
ForEach ($startMenuPath in $usersStartMenuPaths) {
|
||||
ReplaceStartMenu $startMenuTemplate "$($startMenuPath.Fullname)\start2.bin"
|
||||
ReplaceStartMenu -startMenuBinFile "$($startMenuPath.Fullname)\start2.bin" -startMenuTemplate $startMenuTemplate
|
||||
}
|
||||
|
||||
# Also replace the start menu file for the default user profile
|
||||
$defaultStartMenuPath = GetUserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState" -exitIfPathNotFound $false
|
||||
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Write-Host "[WhatIf] Replace Start Menu for Default user profile with template $startMenuTemplate" -ForegroundColor Cyan
|
||||
return
|
||||
}
|
||||
|
||||
# Create folder if it doesn't exist
|
||||
if (-not (Test-Path $defaultStartMenuPath)) {
|
||||
new-item $defaultStartMenuPath -ItemType Directory -Force | Out-Null
|
||||
@@ -33,43 +57,71 @@ function ReplaceStartMenuForAllUsers {
|
||||
}
|
||||
|
||||
# Copy template to default profile
|
||||
Copy-Item -Path $startMenuTemplate -Destination $defaultStartMenuPath -Force
|
||||
ReplaceStartMenu -startMenuBinFile "$($defaultStartMenuPath)\start2.bin" -startMenuTemplate $startMenuTemplate
|
||||
Write-Host "Replaced start menu for the default user profile"
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
|
||||
# Replace the startmenu for all users, when using the default startmenuTemplate this clears all pinned apps
|
||||
# Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Replaces the start menu layout for a single user.
|
||||
|
||||
.DESCRIPTION
|
||||
Backs up the current start2.bin file (if it exists), then copies the
|
||||
specified template over it. When using the default template this clears
|
||||
all pinned apps from the start menu. Validates that the template file
|
||||
exists and has a .bin extension before proceeding.
|
||||
|
||||
Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/
|
||||
|
||||
.PARAMETER startMenuBinFile
|
||||
The full path to the user's start2.bin file to replace.
|
||||
|
||||
.PARAMETER startMenuTemplate
|
||||
Path to the .bin template file to apply. Defaults to the blank template
|
||||
bundled with the script (Assets/Start/start2.bin).
|
||||
|
||||
.EXAMPLE
|
||||
ReplaceStartMenu -startMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
|
||||
|
||||
.EXAMPLE
|
||||
ReplaceStartMenu -startMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -startMenuTemplate "C:\CustomLayout.bin"
|
||||
#>
|
||||
function ReplaceStartMenu {
|
||||
param (
|
||||
$startMenuTemplate = "$script:AssetsPath\Start\start2.bin",
|
||||
$startMenuBinFile = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
|
||||
[Parameter(Mandatory)]
|
||||
[string]$startMenuBinFile,
|
||||
[string]$startMenuTemplate = "$script:AssetsPath\Start\start2.bin"
|
||||
)
|
||||
|
||||
# Change path to correct user if a user was specified
|
||||
if ($script:Params.ContainsKey("User")) {
|
||||
$startMenuBinFile = GetStartMenuBinPathForUser -UserName (GetUserName)
|
||||
}
|
||||
|
||||
# Check if template bin file exists
|
||||
if (-not (Test-Path $startMenuTemplate)) {
|
||||
Write-Host "Error: Unable to replace start menu, template file not found" -ForegroundColor Red
|
||||
return
|
||||
}
|
||||
|
||||
if ([IO.Path]::GetExtension($startMenuTemplate) -ne ".bin" ) {
|
||||
if ([IO.Path]::GetExtension($startMenuTemplate) -ne ".bin") {
|
||||
Write-Host "Error: Unable to replace start menu, template file is not a valid .bin file" -ForegroundColor Red
|
||||
return
|
||||
}
|
||||
|
||||
$userName = GetStartMenuUserNameFromPath -StartMenuBinFile $startMenuBinFile
|
||||
|
||||
$backupBinFile = $startMenuBinFile + ".bak"
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Write-Host "[WhatIf] Replace Start Menu for user $userName with template $startMenuTemplate" -ForegroundColor Cyan
|
||||
return
|
||||
}
|
||||
|
||||
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
|
||||
$backupFileName = "Win11Debloat-StartBackup-$timestamp.bak"
|
||||
$startMenuDir = Split-Path $startMenuBinFile -Parent
|
||||
$backupBinFile = Join-Path $startMenuDir $backupFileName
|
||||
|
||||
if (Test-Path $startMenuBinFile) {
|
||||
# Backup current start menu file
|
||||
Move-Item -Path $startMenuBinFile -Destination $backupBinFile -Force
|
||||
Copy-Item -Path $startMenuBinFile -Destination $backupBinFile -Force
|
||||
Write-Verbose "Start menu backup for user $userName saved to $backupFileName"
|
||||
}
|
||||
else {
|
||||
Write-Host "Unable to find original start2.bin file for user $userName, no backup was created for this user" -ForegroundColor Yellow
|
||||
@@ -82,6 +134,24 @@ function ReplaceStartMenu {
|
||||
Write-Host "Replaced start menu for user $userName"
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Returns the full path to the start menu bin file for a given user.
|
||||
|
||||
.DESCRIPTION
|
||||
Resolves the path to the start2.bin file for the specified username.
|
||||
When no username is provided or the value is empty, falls back to
|
||||
the current user's local app data path via $env:LOCALAPPDATA.
|
||||
|
||||
.PARAMETER UserName
|
||||
The target username. Pass an empty string or omit to resolve for the current user.
|
||||
|
||||
.EXAMPLE
|
||||
GetStartMenuBinPathForUser -UserName "Jeff"
|
||||
|
||||
.EXAMPLE
|
||||
GetStartMenuBinPathForUser -UserName "Default"
|
||||
#>
|
||||
function GetStartMenuBinPathForUser {
|
||||
param(
|
||||
[string]$UserName
|
||||
@@ -94,6 +164,21 @@ function GetStartMenuBinPathForUser {
|
||||
return (GetUserDirectory -userName $UserName -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -exitIfPathNotFound $false)
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Extracts the username from a start2.bin file path.
|
||||
|
||||
.DESCRIPTION
|
||||
Parses a typical C:\Users\<UserName>\AppData\... path and returns the
|
||||
username portion. Returns 'unknown' if the path does not match the
|
||||
expected pattern.
|
||||
|
||||
.PARAMETER StartMenuBinFile
|
||||
The full path to a start2.bin file.
|
||||
|
||||
.EXAMPLE
|
||||
GetStartMenuUserNameFromPath -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
|
||||
#>
|
||||
function GetStartMenuUserNameFromPath {
|
||||
param(
|
||||
[string]$StartMenuBinFile
|
||||
@@ -108,23 +193,119 @@ function GetStartMenuUserNameFromPath {
|
||||
}
|
||||
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Returns the path to the latest start menu backup file for the given scope.
|
||||
|
||||
.DESCRIPTION
|
||||
Resolves the LocalState folder for the specified scope and returns the
|
||||
full path to the most recent Win11Debloat-StartBackup-*.bak file, or
|
||||
$null if no backup exists.
|
||||
|
||||
For CurrentUser, uses $env:LOCALAPPDATA directly. For AllUsers, scans
|
||||
every user profile.
|
||||
|
||||
.PARAMETER Scope
|
||||
The scope to check: CurrentUser or AllUsers.
|
||||
|
||||
.EXAMPLE
|
||||
$backupPath = Get-StartMenuBackupPath -Scope 'CurrentUser'
|
||||
|
||||
.EXAMPLE
|
||||
$backupPath = Get-StartMenuBackupPath -Scope 'AllUsers'
|
||||
#>
|
||||
function Get-StartMenuBackupPath {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateSet('CurrentUser', 'AllUsers')]
|
||||
[string]$Scope
|
||||
)
|
||||
|
||||
if ($Scope -eq 'CurrentUser') {
|
||||
$localStateDir = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
|
||||
$latestBackup = Get-ChildItem -Path (Join-Path $localStateDir 'Win11Debloat-StartBackup-*.bak') -ErrorAction SilentlyContinue |
|
||||
Sort-Object Name -Descending |
|
||||
Select-Object -First 1
|
||||
if ($latestBackup) { return $latestBackup.FullName }
|
||||
return $null
|
||||
}
|
||||
else {
|
||||
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
|
||||
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
|
||||
foreach ($startMenuPath in $usersStartMenuPaths) {
|
||||
$latestBackup = Get-ChildItem -Path (Join-Path $startMenuPath.FullName 'Win11Debloat-StartBackup-*.bak') -ErrorAction SilentlyContinue |
|
||||
Sort-Object Name -Descending |
|
||||
Select-Object -First 1
|
||||
if ($latestBackup) { return $latestBackup.FullName }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Restores a user's start menu from a backup file.
|
||||
|
||||
.DESCRIPTION
|
||||
Moves the current start2.bin to a .restore.bak safety copy, then copies
|
||||
the specified backup file into place. Returns a PSCustomObject with
|
||||
UserName, Result ($true/$false), and Message properties describing
|
||||
the outcome.
|
||||
|
||||
.PARAMETER StartMenuBinFile
|
||||
The full path to the user's start2.bin file to restore.
|
||||
|
||||
.PARAMETER BackupFilePath
|
||||
Path to the backup file to restore from. If omitted, automatically
|
||||
finds the latest Win11Debloat-StartBackup-*.bak file.
|
||||
|
||||
.EXAMPLE
|
||||
RestoreStartMenuFromBackup -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
|
||||
|
||||
.EXAMPLE
|
||||
RestoreStartMenuFromBackup -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
|
||||
#>
|
||||
function RestoreStartMenuFromBackup {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$StartMenuBinFile,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$BackupFilePath
|
||||
)
|
||||
|
||||
$userName = GetStartMenuUserNameFromPath -StartMenuBinFile $StartMenuBinFile
|
||||
$backupBinFile = if ([string]::IsNullOrWhiteSpace($BackupFilePath)) {
|
||||
$StartMenuBinFile + '.bak'
|
||||
# Auto-detect latest backup in the same folder as the start2.bin
|
||||
$startMenuDir = Split-Path $StartMenuBinFile -Parent
|
||||
$latestBackup = Get-ChildItem -Path (Join-Path $startMenuDir 'Win11Debloat-StartBackup-*.bak') -ErrorAction SilentlyContinue |
|
||||
Sort-Object Name -Descending |
|
||||
Select-Object -First 1
|
||||
|
||||
if ($latestBackup) { $latestBackup.FullName } else { $null }
|
||||
}
|
||||
else {
|
||||
$BackupFilePath
|
||||
}
|
||||
$currentBinBackup = $StartMenuBinFile + '.restore.bak'
|
||||
$restoreTimestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
|
||||
$restoreBackupFileName = "Win11Debloat-StartRestore-$restoreTimestamp.bak"
|
||||
$currentBinBackup = Join-Path (Split-Path $StartMenuBinFile -Parent) $restoreBackupFileName
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($backupBinFile)) {
|
||||
return [PSCustomObject]@{
|
||||
UserName = $userName
|
||||
Result = $false
|
||||
Message = "No start menu backup file found for user $userName."
|
||||
}
|
||||
}
|
||||
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Write-Host "[WhatIf] Restore start menu for user $userName from backup $backupBinFile" -ForegroundColor Cyan
|
||||
return [PSCustomObject]@{
|
||||
UserName = $userName
|
||||
Result = $true
|
||||
Message = "[WhatIf] Restored start menu for user $userName."
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $backupBinFile)) {
|
||||
return [PSCustomObject]@{
|
||||
@@ -155,23 +336,61 @@ function RestoreStartMenuFromBackup {
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Restores the start menu for the current target user from a backup.
|
||||
|
||||
.DESCRIPTION
|
||||
Resolves the start2.bin path for the currently logged-in user, then
|
||||
delegates to RestoreStartMenuFromBackup.
|
||||
|
||||
.PARAMETER BackupFilePath
|
||||
Path to the backup file to restore from. If omitted, automatically
|
||||
finds the latest Win11Debloat-StartBackup-*.bak file.
|
||||
|
||||
.EXAMPLE
|
||||
RestoreStartMenu
|
||||
|
||||
.EXAMPLE
|
||||
RestoreStartMenu -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
|
||||
#>
|
||||
function RestoreStartMenu {
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$BackupFilePath
|
||||
)
|
||||
|
||||
$targetUserName = GetUserName
|
||||
$startMenuBinFile = GetStartMenuBinPathForUser -UserName $targetUserName
|
||||
$targetUserName = $env:USERNAME
|
||||
$startMenuBinFile = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
|
||||
|
||||
Write-Host "Restoring start menu for user $targetUserName from backup..."
|
||||
|
||||
return RestoreStartMenuFromBackup -StartMenuBinFile $startMenuBinFile -BackupFilePath $BackupFilePath
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Restores the start menu for all user profiles from a backup.
|
||||
|
||||
.DESCRIPTION
|
||||
Iterates over every existing user profile and restores each user's
|
||||
start2.bin from the latest backup in their LocalState folder. For the
|
||||
Default user profile, removes the start2.bin file (which was previously
|
||||
copied from a template) so that new profiles revert to the system
|
||||
default start menu.
|
||||
|
||||
.PARAMETER BackupFilePath
|
||||
Path to the backup file to restore from. If omitted, automatically
|
||||
finds the latest Win11Debloat-StartBackup-*.bak in each user's
|
||||
LocalState folder.
|
||||
|
||||
.EXAMPLE
|
||||
RestoreStartMenuForAllUsers
|
||||
|
||||
.EXAMPLE
|
||||
RestoreStartMenuForAllUsers -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
|
||||
#>
|
||||
function RestoreStartMenuForAllUsers {
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$BackupFilePath
|
||||
)
|
||||
|
||||
@@ -191,19 +410,29 @@ function RestoreStartMenuForAllUsers {
|
||||
if (Test-Path $defaultStartMenuPath) {
|
||||
$defaultStartMenuBinFile = Join-Path $defaultStartMenuPath 'start2.bin'
|
||||
if (Test-Path -LiteralPath $defaultStartMenuBinFile) {
|
||||
try {
|
||||
Remove-Item -LiteralPath $defaultStartMenuBinFile -Force
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Write-Host "[WhatIf] Remove start2.bin for the default user profile" -ForegroundColor Cyan
|
||||
$results += [PSCustomObject]@{
|
||||
UserName = 'Default'
|
||||
Result = $true
|
||||
Message = 'Removed start2.bin for the default user profile.'
|
||||
Message = '[WhatIf] Removed start2.bin for the default user profile.'
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$results += [PSCustomObject]@{
|
||||
UserName = 'Default'
|
||||
Result = $false
|
||||
Message = "Failed to remove start2.bin for the default user profile. $($_.Exception.Message)"
|
||||
else {
|
||||
try {
|
||||
Remove-Item -LiteralPath $defaultStartMenuBinFile -Force
|
||||
$results += [PSCustomObject]@{
|
||||
UserName = 'Default'
|
||||
Result = $true
|
||||
Message = 'Removed start2.bin for the default user profile.'
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$results += [PSCustomObject]@{
|
||||
UserName = 'Default'
|
||||
Result = $false
|
||||
Message = "Failed to remove start2.bin for the default user profile. $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
# Restart the Windows Explorer process
|
||||
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
|
||||
}
|
||||
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Write-Host "[WhatIf] Restart the Windows Explorer process" -ForegroundColor Cyan
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,38 +7,21 @@ function Invoke-WithLoadedRestoreHive {
|
||||
$ArgumentObject = $null
|
||||
)
|
||||
|
||||
$hiveDatPath = if ($Target -eq 'DefaultUserProfile') {
|
||||
GetUserDirectory -userName 'Default' -fileName 'NTUSER.DAT'
|
||||
$targetUserName = if ($Target -eq 'DefaultUserProfile') {
|
||||
'Default'
|
||||
}
|
||||
elseif ($Target -like 'User:*') {
|
||||
$userName = $Target.Substring(5)
|
||||
if ([string]::IsNullOrWhiteSpace($userName)) {
|
||||
throw 'Invalid backup target format for user restore.'
|
||||
}
|
||||
GetUserDirectory -userName $userName -fileName 'NTUSER.DAT'
|
||||
$userName
|
||||
}
|
||||
else {
|
||||
throw "Unsupported backup target '$Target'."
|
||||
}
|
||||
|
||||
$global:LASTEXITCODE = 0
|
||||
reg load 'HKU\Default' "$hiveDatPath" | Out-Null
|
||||
$loadExitCode = $LASTEXITCODE
|
||||
if ($loadExitCode -ne 0) {
|
||||
throw "Failed to load target user hive '$hiveDatPath' (exit code: $loadExitCode)."
|
||||
}
|
||||
|
||||
try {
|
||||
& $ScriptBlock $ArgumentObject
|
||||
}
|
||||
finally {
|
||||
$global:LASTEXITCODE = 0
|
||||
reg unload 'HKU\Default' | Out-Null
|
||||
$unloadExitCode = $LASTEXITCODE
|
||||
if ($unloadExitCode -ne 0) {
|
||||
throw "Failed to unload registry hive 'HKU\Default' (exit code: $unloadExitCode)"
|
||||
}
|
||||
}
|
||||
Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $ScriptBlock -ArgumentObject $ArgumentObject
|
||||
}
|
||||
|
||||
function Restore-RegistryKeySnapshot {
|
||||
|
||||
@@ -87,7 +87,17 @@ function Normalize-RegistryBackup {
|
||||
$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) {
|
||||
$errors.Add([string]$allowListValidationError)
|
||||
}
|
||||
@@ -110,6 +120,7 @@ function Normalize-RegistryBackup {
|
||||
ComputerName = [string]$Backup.ComputerName
|
||||
Target = $normalizedTarget
|
||||
SelectedFeatures = @($selectedFeatures)
|
||||
SelectedUndoFeatures = @($selectedUndoFeatures)
|
||||
RegistryKeys = @($normalizedKeys)
|
||||
}
|
||||
}
|
||||
@@ -122,6 +133,11 @@ function Restore-RegistryBackupState {
|
||||
|
||||
$friendlyTarget = GetFriendlyRegistryBackupTarget -Target ([string]$Backup.Target)
|
||||
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Write-Host "[WhatIf] Restore registry backup for $friendlyTarget" -ForegroundColor Cyan
|
||||
return [PSCustomObject]@{ Result = $true }
|
||||
}
|
||||
|
||||
$restoreAction = {
|
||||
param($normalizedBackup)
|
||||
|
||||
@@ -137,9 +153,10 @@ function Restore-RegistryBackupState {
|
||||
Write-Host "Restore requires loading target user hive."
|
||||
Invoke-WithLoadedRestoreHive -Target $Backup.Target -ScriptBlock $restoreAction -ArgumentObject $Backup
|
||||
Write-Host "Restore completed for $friendlyTarget."
|
||||
return
|
||||
return [PSCustomObject]@{ Result = $true }
|
||||
}
|
||||
|
||||
& $restoreAction $Backup
|
||||
Write-Host "Restore completed for $friendlyTarget."
|
||||
return [PSCustomObject]@{ Result = $true }
|
||||
}
|
||||
|
||||
307
Scripts/Features/StoreSearchSuggestions.ps1
Normal file
307
Scripts/Features/StoreSearchSuggestions.ps1
Normal file
@@ -0,0 +1,307 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Disables Microsoft Store search suggestions in the start menu for all user profiles.
|
||||
|
||||
.DESCRIPTION
|
||||
Iterates over every existing user profile and the Default user profile,
|
||||
denying the EVERYONE group FullControl access to each user's Store app
|
||||
database file (store.db). This prevents Windows from showing Store search
|
||||
suggestions in the start menu search pane.
|
||||
|
||||
.EXAMPLE
|
||||
DisableStoreSearchSuggestionsForAllUsers
|
||||
#>
|
||||
function DisableStoreSearchSuggestionsForAllUsers {
|
||||
# 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 disable start search suggestions
|
||||
foreach ($storeDbPath in $usersStoreDbPaths) {
|
||||
DisableStoreSearchSuggestions -StoreAppsDatabase ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
|
||||
}
|
||||
|
||||
# Also disable start search suggestions for the default user profile
|
||||
$defaultStoreDbPath = GetStoreAppsDatabasePathForUser -UserName "Default"
|
||||
if ($defaultStoreDbPath) {
|
||||
DisableStoreSearchSuggestions -StoreAppsDatabase $defaultStoreDbPath
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Disables Microsoft Store search suggestions for a single user.
|
||||
|
||||
.DESCRIPTION
|
||||
Denies the EVERYONE group FullControl access to the specified Store app
|
||||
database file (store.db). If the file does not exist (e.g. on EEA systems
|
||||
where Store app suggestions are absent by default), it creates the file
|
||||
and its parent directory first to prevent Windows from recreating it later.
|
||||
|
||||
.PARAMETER StoreAppsDatabase
|
||||
The full path to the user's store.db file.
|
||||
|
||||
.EXAMPLE
|
||||
DisableStoreSearchSuggestions -StoreAppsDatabase "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
|
||||
#>
|
||||
function DisableStoreSearchSuggestions {
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
[string]$StoreAppsDatabase
|
||||
)
|
||||
|
||||
$userName = [regex]::Match($StoreAppsDatabase, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value
|
||||
if (-not $userName) { $userName = '<unknown>' }
|
||||
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Write-Host "[WhatIf] Disable Microsoft Store search suggestions for user $userName by restricting access to ${StoreAppsDatabase}" -ForegroundColor Cyan
|
||||
return
|
||||
}
|
||||
|
||||
# This file doesn't exist in EEA (No Store app suggestions).
|
||||
if (-not (Test-Path -Path $StoreAppsDatabase))
|
||||
{
|
||||
Write-Host "Unable to find Store app database for user $userName, creating it now to prevent Windows from creating it later..." -ForegroundColor Yellow
|
||||
|
||||
$storeDbDir = Split-Path -Path $StoreAppsDatabase -Parent
|
||||
|
||||
if (-not (Test-Path -Path $storeDbDir)) {
|
||||
New-Item -Path $storeDbDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
New-Item -Path $StoreAppsDatabase -ItemType File -Force | Out-Null
|
||||
}
|
||||
|
||||
$AccountSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0') # 'EVERYONE' group
|
||||
$Acl = Get-Acl -Path $StoreAppsDatabase
|
||||
$Ace = [System.Security.AccessControl.FileSystemAccessRule]::new($AccountSid, 'FullControl', 'Deny')
|
||||
$Acl.SetAccessRule($Ace) | Out-Null
|
||||
Set-Acl -Path $StoreAppsDatabase -AclObject $Acl | Out-Null
|
||||
|
||||
Write-Host "Disabled Microsoft Store search suggestions for user $userName"
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Re-enables Microsoft Store search suggestions in the start menu for all user profiles.
|
||||
|
||||
.DESCRIPTION
|
||||
Iterates over every existing user profile and the Default user profile,
|
||||
removing the deny ACL from each user's Store app database file (store.db)
|
||||
and then deleting the file. This restores the default Windows behavior
|
||||
where Store search suggestions appear in the start menu.
|
||||
|
||||
.EXAMPLE
|
||||
EnableStoreSearchSuggestionsForAllUsers
|
||||
#>
|
||||
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 -StoreAppsDatabase ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
|
||||
}
|
||||
|
||||
# Also re-enable for the default user profile
|
||||
$defaultStoreDbPath = GetStoreAppsDatabasePathForUser -UserName "Default"
|
||||
if ($defaultStoreDbPath) {
|
||||
EnableStoreSearchSuggestions -StoreAppsDatabase $defaultStoreDbPath
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Re-enables Microsoft Store search suggestions for a single user.
|
||||
|
||||
.DESCRIPTION
|
||||
Takes ownership of the specified Store app database file, removes any
|
||||
EVERYONE deny FullControl ACL entries, and deletes the file. If the file
|
||||
does not exist, no action is taken. Callers should handle the case where
|
||||
the file is absent gracefully.
|
||||
|
||||
.PARAMETER StoreAppsDatabase
|
||||
The full path to the user's store.db file.
|
||||
|
||||
.EXAMPLE
|
||||
EnableStoreSearchSuggestions -StoreAppsDatabase "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
|
||||
#>
|
||||
function EnableStoreSearchSuggestions {
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
[string]$StoreAppsDatabase
|
||||
)
|
||||
|
||||
$userName = [regex]::Match($StoreAppsDatabase, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value
|
||||
if (-not $userName) { $userName = '<unknown>' }
|
||||
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Write-Host "[WhatIf] Re-enable Microsoft Store search suggestions for user $userName by restoring access to ${StoreAppsDatabase}" -ForegroundColor Cyan
|
||||
return
|
||||
}
|
||||
|
||||
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)"
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Returns the full path to the Store app database file for a given user.
|
||||
|
||||
.DESCRIPTION
|
||||
Resolves the path to the Microsoft Store app database (store.db) for the
|
||||
specified username. When no username is provided or the value is empty,
|
||||
falls back to the current user's local app data path via $env:LOCALAPPDATA.
|
||||
|
||||
.PARAMETER UserName
|
||||
The target username. Pass an empty string or omit to resolve for the current user.
|
||||
|
||||
.EXAMPLE
|
||||
GetStoreAppsDatabasePathForUser -UserName "Jeff"
|
||||
|
||||
.EXAMPLE
|
||||
GetStoreAppsDatabasePathForUser -UserName "Default"
|
||||
#>
|
||||
function GetStoreAppsDatabasePathForUser {
|
||||
param(
|
||||
[string]$UserName
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($UserName)) {
|
||||
return "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
|
||||
}
|
||||
|
||||
return (GetUserDirectory -userName $UserName -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false)
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Tests whether Store search suggestions are disabled for a single user.
|
||||
|
||||
.DESCRIPTION
|
||||
Checks whether the specified store.db file has an EVERYONE deny
|
||||
FullControl ACL entry applied. Returns $true if the deny rule is present,
|
||||
$false otherwise (including when the file or directory does not exist).
|
||||
|
||||
.PARAMETER StoreAppsDatabase
|
||||
The full path to the user's store.db file.
|
||||
|
||||
.EXAMPLE
|
||||
Test-StoreSearchSuggestionsDisabled -StoreAppsDatabase "C:\Users\Jeff\AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
|
||||
#>
|
||||
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
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Tests whether Store search suggestions are disabled for all user profiles.
|
||||
|
||||
.DESCRIPTION
|
||||
Collects the store.db paths for all existing user profiles and the Default
|
||||
user profile, then verifies that every one of them has the EVERYONE deny
|
||||
FullControl ACL applied. Returns $true only if ALL paths pass the check.
|
||||
Returns $false immediately if any user's store.db is not disabled.
|
||||
|
||||
.EXAMPLE
|
||||
Test-StoreSearchSuggestionsDisabledForAllUsers
|
||||
#>
|
||||
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 = GetStoreAppsDatabasePathForUser -UserName "Default"
|
||||
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
|
||||
}
|
||||
128
Scripts/Features/TelemetryScheduledTasks.ps1
Normal file
128
Scripts/Features/TelemetryScheduledTasks.ps1
Normal file
@@ -0,0 +1,128 @@
|
||||
# List of known Windows telemetry-related scheduled tasks
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Returns the list of known Windows telemetry-related scheduled tasks.
|
||||
|
||||
.DESCRIPTION
|
||||
Returns an array of hashtables, each with a Path and Name key, representing
|
||||
scheduled tasks that collect or report telemetry data on Windows.
|
||||
|
||||
.EXAMPLE
|
||||
Get-TelemetryScheduledTasks
|
||||
#>
|
||||
function Get-TelemetryScheduledTasks {
|
||||
return @(
|
||||
@{ Path = "\Microsoft\Windows\Application Experience\"; Name = "Microsoft Compatibility Appraiser" },
|
||||
@{ Path = "\Microsoft\Windows\Application Experience\"; Name = "Microsoft Compatibility Appraiser Exp" },
|
||||
@{ Path = "\Microsoft\Windows\Application Experience\"; Name = "ProgramDataUpdater" },
|
||||
@{ Path = "\Microsoft\Windows\Application Experience\"; Name = "StartupAppTask" },
|
||||
@{ Path = "\Microsoft\Windows\Customer Experience Improvement Program\"; Name = "Consolidator" },
|
||||
@{ Path = "\Microsoft\Windows\Customer Experience Improvement Program\"; Name = "UsbCeip" },
|
||||
@{ Path = "\Microsoft\Windows\DiskDiagnostic\"; Name = "Microsoft-Windows-DiskDiagnosticDataCollector" },
|
||||
@{ Path = "\Microsoft\Windows\Autochk\"; Name = "Proxy" }
|
||||
)
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Disables known Windows telemetry-related scheduled tasks.
|
||||
|
||||
.DESCRIPTION
|
||||
Iterates over a predefined list of Windows scheduled tasks associated with
|
||||
telemetry and disables each one that exists and is not already disabled.
|
||||
Supports -WhatIf to preview changes without applying them.
|
||||
|
||||
.EXAMPLE
|
||||
Disable-TelemetryScheduledTasks
|
||||
#>
|
||||
function Disable-TelemetryScheduledTasks {
|
||||
Write-Host "> Disabling telemetry scheduled tasks..."
|
||||
$tasks = Get-TelemetryScheduledTasks
|
||||
|
||||
foreach ($task in $tasks) {
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Write-Host "[WhatIf] Disable Scheduled Task: $($task.Path)$($task.Name)" -ForegroundColor Cyan
|
||||
continue
|
||||
}
|
||||
|
||||
$result = Invoke-NonBlocking -ScriptBlock {
|
||||
param($path, $name)
|
||||
Import-Module ScheduledTasks -ErrorAction SilentlyContinue
|
||||
$taskObj = Get-ScheduledTask -TaskPath $path -TaskName $name -ErrorAction SilentlyContinue
|
||||
if (-not $taskObj) {
|
||||
return @{ Success = $true; Status = 'NotFound' }
|
||||
}
|
||||
if ($taskObj.State -ne 'Disabled') {
|
||||
try {
|
||||
Disable-ScheduledTask -TaskPath $path -TaskName $name -ErrorAction Stop | Out-Null
|
||||
return @{ Success = $true; Status = 'Disabled' }
|
||||
}
|
||||
catch {
|
||||
return @{ Success = $false; Status = 'Error'; Error = $_.Exception.Message }
|
||||
}
|
||||
}
|
||||
return @{ Success = $true; Status = 'AlreadyDisabled' }
|
||||
} -ArgumentList @($task.Path, $task.Name)
|
||||
|
||||
switch ($result.Status) {
|
||||
'Disabled' { Write-Host "Disabled Scheduled Task: $($task.Path)$($task.Name)" }
|
||||
'AlreadyDisabled' { Write-Host "Scheduled Task $($task.Path)$($task.Name) is already disabled" -ForegroundColor DarkGray }
|
||||
'NotFound' { Write-Host "Scheduled Task $($task.Path)$($task.Name) not found" -ForegroundColor DarkGray }
|
||||
'Error' { Write-Host "Failed to disable Scheduled Task: $($task.Path)$($task.Name) - $($result.Error)" -ForegroundColor Yellow }
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Enables known Windows telemetry-related scheduled tasks.
|
||||
|
||||
.DESCRIPTION
|
||||
Iterates over a predefined list of Windows scheduled tasks associated with
|
||||
telemetry and enables each one that exists and is currently disabled.
|
||||
Supports -WhatIf to preview changes without applying them.
|
||||
|
||||
.EXAMPLE
|
||||
Enable-TelemetryScheduledTasks
|
||||
#>
|
||||
function Enable-TelemetryScheduledTasks {
|
||||
Write-Host "> Enabling telemetry scheduled tasks..."
|
||||
$tasks = Get-TelemetryScheduledTasks
|
||||
|
||||
foreach ($task in $tasks) {
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Write-Host "[WhatIf] Enable Scheduled Task: $($task.Path)$($task.Name)" -ForegroundColor Cyan
|
||||
continue
|
||||
}
|
||||
|
||||
$result = Invoke-NonBlocking -ScriptBlock {
|
||||
param($path, $name)
|
||||
Import-Module ScheduledTasks -ErrorAction SilentlyContinue
|
||||
$taskObj = Get-ScheduledTask -TaskPath $path -TaskName $name -ErrorAction SilentlyContinue
|
||||
if (-not $taskObj) {
|
||||
return @{ Success = $true; Status = 'NotFound' }
|
||||
}
|
||||
if ($taskObj.State -eq 'Disabled') {
|
||||
try {
|
||||
Enable-ScheduledTask -TaskPath $path -TaskName $name -ErrorAction Stop | Out-Null
|
||||
return @{ Success = $true; Status = 'Enabled' }
|
||||
}
|
||||
catch {
|
||||
return @{ Success = $false; Status = 'Error'; Error = $_.Exception.Message }
|
||||
}
|
||||
}
|
||||
return @{ Success = $true; Status = 'AlreadyEnabled' }
|
||||
} -ArgumentList @($task.Path, $task.Name)
|
||||
|
||||
switch ($result.Status) {
|
||||
'Enabled' { Write-Host "Enabled Scheduled Task: $($task.Path)$($task.Name)" }
|
||||
'AlreadyEnabled' { Write-Host "Scheduled Task $($task.Path)$($task.Name) is already enabled." -ForegroundColor DarkGray }
|
||||
'NotFound' { Write-Host "Scheduled Task $($task.Path)$($task.Name) not found." -ForegroundColor DarkGray }
|
||||
'Error' { Write-Host "Failed to enable Scheduled Task: $($task.Path)$($task.Name) - $($result.Error)" -ForegroundColor Yellow }
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
}
|
||||
61
Scripts/Features/WindowsOptionalFeatures.ps1
Normal file
61
Scripts/Features/WindowsOptionalFeatures.ps1
Normal file
@@ -0,0 +1,61 @@
|
||||
# Enables a Windows optional feature and pipes its output to the console
|
||||
function EnableWindowsFeature {
|
||||
param (
|
||||
[string]$FeatureName
|
||||
)
|
||||
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Write-Host "[WhatIf] Enable Windows feature: $FeatureName" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
|
||||
$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
|
||||
)
|
||||
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Write-Host "[WhatIf] Disable Windows feature: $FeatureName" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
|
||||
$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')
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
function LoadAppsDetailsFromJson {
|
||||
param (
|
||||
[switch]$OnlyInstalled,
|
||||
[string]$InstalledList = "",
|
||||
[object[]]$InstalledList = $null,
|
||||
[switch]$InitialCheckedFromJson
|
||||
)
|
||||
|
||||
@@ -24,15 +24,19 @@ function LoadAppsDetailsFromJson {
|
||||
if ($OnlyInstalled) {
|
||||
$isInstalled = $false
|
||||
foreach ($appId in $appIdArray) {
|
||||
if (($InstalledList -like ("*$appId*")) -or (Get-AppxPackage -Name $appId)) {
|
||||
# Check Get-AppxPackage first (fast, no process launch)
|
||||
if (Get-AppxPackage -Name $appId) {
|
||||
$isInstalled = $true
|
||||
break
|
||||
}
|
||||
if (($appId -eq "Microsoft.Edge") -and ($InstalledList -like "* Microsoft.Edge *")) {
|
||||
|
||||
# Then check the pre-fetched winget list
|
||||
if ($InstalledList -and (Test-AppInWingetList -appId $appId -InstalledList $InstalledList)) {
|
||||
$isInstalled = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $isInstalled) { continue }
|
||||
}
|
||||
|
||||
@@ -52,6 +56,7 @@ function LoadAppsDetailsFromJson {
|
||||
Description = $appData.Description
|
||||
SelectedByDefault = $appData.SelectedByDefault
|
||||
Recommendation = $appData.Recommendation
|
||||
RemovalMethod = if ($appData.RemovalMethod -and $appData.RemovalMethod -eq 'WinGet') { 'WinGet' } else { 'Appx' }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
# Returns list of apps from the specified file, it trims the app names and removes any comments
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Returns a list of app IDs from the specified JSON file.
|
||||
|
||||
.DESCRIPTION
|
||||
Reads an Apps.json file and returns the AppIds for every entry where
|
||||
SelectedByDefault is $true. Each app entry may declare a single AppId
|
||||
or an array of AppIds; both forms are handled transparently.
|
||||
|
||||
.PARAMETER appsFilePath
|
||||
Path to a JSON file in the Config/Apps.json format.
|
||||
|
||||
.OUTPUTS
|
||||
System.String[]. An array of app ID strings, or an empty array if the
|
||||
file does not exist or contains no selected-by-default apps.
|
||||
#>
|
||||
function LoadAppsFromFile {
|
||||
param (
|
||||
$appsFilePath
|
||||
@@ -11,30 +26,14 @@ function LoadAppsFromFile {
|
||||
}
|
||||
|
||||
try {
|
||||
# Check if file is JSON or text format
|
||||
if ($appsFilePath -like "*.json") {
|
||||
# JSON file format
|
||||
$jsonContent = Get-Content -Path $appsFilePath -Raw | ConvertFrom-Json
|
||||
Foreach ($appData in $jsonContent.Apps) {
|
||||
# Handle AppId as array (could be single or multiple IDs)
|
||||
$appIdArray = if ($appData.AppId -is [array]) { $appData.AppId } else { @($appData.AppId) }
|
||||
$appIdArray = $appIdArray | ForEach-Object { $_.Trim() } | Where-Object { $_.length -gt 0 }
|
||||
$selectedByDefault = $appData.SelectedByDefault
|
||||
if ($selectedByDefault -and $appIdArray.Count -gt 0) {
|
||||
$appsList += $appIdArray
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
# Legacy text file format
|
||||
Foreach ($app in (Get-Content -Path $appsFilePath | Where-Object { $_ -notmatch '^#.*' -and $_ -notmatch '^\s*$' } )) {
|
||||
if (-not ($app.IndexOf('#') -eq -1)) {
|
||||
$app = $app.Substring(0, $app.IndexOf('#'))
|
||||
}
|
||||
|
||||
$app = $app.Trim()
|
||||
$appString = $app.Trim('*')
|
||||
$appsList += $appString
|
||||
$jsonContent = Get-Content -Path $appsFilePath -Raw | ConvertFrom-Json
|
||||
Foreach ($appData in $jsonContent.Apps) {
|
||||
# Handle AppId as array (could be single or multiple IDs)
|
||||
$appIdArray = if ($appData.AppId -is [array]) { $appData.AppId } else { @($appData.AppId) }
|
||||
$appIdArray = $appIdArray | ForEach-Object { $_.Trim() } | Where-Object { $_.length -gt 0 }
|
||||
$selectedByDefault = $appData.SelectedByDefault
|
||||
if ($selectedByDefault -and $appIdArray.Count -gt 0) {
|
||||
$appsList += $appIdArray
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ function LoadSettings {
|
||||
|
||||
$feature = $script:Features[$setting.Name]
|
||||
|
||||
# Skip unknown settings that aren't defined in Features.json
|
||||
if (-not $feature) { 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))) {
|
||||
continue
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# Saves the provided appsList to the CustomAppsList file
|
||||
function SaveCustomAppsListToFile {
|
||||
param (
|
||||
$appsList
|
||||
)
|
||||
|
||||
$script:SelectedApps = $appsList
|
||||
|
||||
# Create file that stores selected apps if it doesn't exist
|
||||
if (-not (Test-Path $script:CustomAppsListFilePath)) {
|
||||
$null = New-Item $script:CustomAppsListFilePath -ItemType File
|
||||
}
|
||||
|
||||
Set-Content -Path $script:CustomAppsListFilePath -Value $script:SelectedApps
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
# Saves the current settings, excluding control parameters, to 'LastUsedSettings.json' file
|
||||
function SaveSettings {
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Write-Host "[WhatIf] Save settings to LastUsedSettings.json" -ForegroundColor Cyan
|
||||
return
|
||||
}
|
||||
|
||||
$settings = @{
|
||||
"Version" = "1.0"
|
||||
"Settings" = @()
|
||||
}
|
||||
|
||||
foreach ($param in $script:Params.Keys) {
|
||||
if ($script:ControlParams -notcontains $param) {
|
||||
if ($script:ControlParams -notcontains $param -and $script:Features.ContainsKey($param)) {
|
||||
$value = $script:Params[$param]
|
||||
|
||||
$settings.Settings += @{
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
# Checks if the system is set to use dark mode for apps
|
||||
function GetSystemUsesDarkMode {
|
||||
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 {
|
||||
return $false
|
||||
|
||||
545
Scripts/GUI/MainWindow-AppSelection.ps1
Normal file
545
Scripts/GUI/MainWindow-AppSelection.ps1
Normal file
@@ -0,0 +1,545 @@
|
||||
# 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."
|
||||
}
|
||||
"Target user only" {
|
||||
$AppRemovalScopeDescription.Text = "Apps will only be removed for the specified target user."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
[object[]]$ListOfApps
|
||||
)
|
||||
|
||||
$script:MainWindowLastSelectedCheckbox = $null
|
||||
|
||||
$loaderScriptPath = $script:LoadAppsDetailsScriptPath
|
||||
$helperScriptPath = $script:TestAppInWingetListScriptPath
|
||||
$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.
|
||||
# The helper is dot-sourced inside the job because the runspace
|
||||
# does not inherit the parent scope's dot-sourced functions.
|
||||
$rawAppData = Invoke-NonBlocking -ScriptBlock {
|
||||
param($loaderScript, $helperScript, $appsListFilePath, $installedList, $onlyInstalled)
|
||||
$script:AppsListFilePath = $appsListFilePath
|
||||
. $helperScript
|
||||
. $loaderScript
|
||||
LoadAppsDetailsFromJson -OnlyInstalled:$onlyInstalled -InstalledList $installedList -InitialCheckedFromJson:$false
|
||||
} -ArgumentList $loaderScriptPath, $helperScriptPath, $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 = $Window.Resources['AppRecommendationSafeColor']
|
||||
$brushDefault = $Window.Resources['AppRecommendationOptionalColor']
|
||||
$brushUnsafe = $Window.Resources['AppRecommendationUnsafeColor']
|
||||
|
||||
# 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] Can be safely removed 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 = $null
|
||||
|
||||
if ($OnlyInstalledAppsBox.IsChecked -and ($script:WingetInstalled -eq $true)) {
|
||||
Write-Host "Retrieving installed apps via winget..."
|
||||
$listOfApps = GetInstalledAppsViaWinget -TimeOut 20 -NonBlocking
|
||||
|
||||
if ($null -eq $listOfApps) {
|
||||
Write-Warning "WinGet returned no data (command timed out or failed)"
|
||||
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
|
||||
}
|
||||
465
Scripts/GUI/MainWindow-Deployment.ps1
Normal file
465
Scripts/GUI/MainWindow-Deployment.ps1
Normal file
@@ -0,0 +1,465 @@
|
||||
# MainWindow-Deployment.ps1
|
||||
# Overview generation, pending tweak actions, feature labels, tweak preset maps, apply logic, user mode state, user selection, and validation.
|
||||
|
||||
function Get-UndoFeatureLabel {
|
||||
param([string]$FeatureId)
|
||||
|
||||
$undoLabel = $script:UndoFeatureLabelLookup[$FeatureId]
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$undoLabel)) {
|
||||
return [string]$undoLabel
|
||||
}
|
||||
|
||||
# Fall back to the regular label (prefixed for undo context)
|
||||
return [string]$script:FeatureLabelLookup[$FeatureId]
|
||||
}
|
||||
|
||||
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 = [string]$script:FeatureLabelLookup[$mapping.FeatureId]
|
||||
})
|
||||
}
|
||||
elseif ($wasApplied -and -not $isNowChecked) {
|
||||
$actions.Add([PSCustomObject]@{
|
||||
Action = 'Undo'
|
||||
FeatureId = [string]$mapping.FeatureId
|
||||
Label = [string]$script:FeatureLabelLookup[$mapping.FeatureId]
|
||||
})
|
||||
}
|
||||
}
|
||||
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 = [string]$script:FeatureLabelLookup[$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']
|
||||
}
|
||||
}
|
||||
570
Scripts/GUI/MainWindow-TweaksBuilder.ps1
Normal file
570
Scripts/GUI/MainWindow-TweaksBuilder.ps1
Normal file
@@ -0,0 +1,570 @@
|
||||
# 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 the 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 = '' }
|
||||
}
|
||||
}
|
||||
|
||||
# Build a FeatureId -> feature lookup for version filtering in groups
|
||||
$featureMap = @{}
|
||||
foreach ($f in $featuresJson.Features) {
|
||||
$featureMap[$f.FeatureId] = $f
|
||||
}
|
||||
|
||||
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
|
||||
# Filter values by Windows version compatibility of their referenced features
|
||||
$filteredValues = @($group.Values | Where-Object {
|
||||
$allCompatible = $true
|
||||
foreach ($fid in $_.FeatureIds) {
|
||||
if ($featureMap.ContainsKey($fid)) {
|
||||
$f = $featureMap[$fid]
|
||||
if (($f.MinVersion -and $WinVersion -lt $f.MinVersion) -or ($f.MaxVersion -and $WinVersion -gt $f.MaxVersion)) {
|
||||
$allCompatible = $false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
$allCompatible
|
||||
})
|
||||
# Skip the group entirely if all values are incompatible with this Windows version
|
||||
if ($filteredValues.Count -eq 0) { continue }
|
||||
|
||||
# When only 1 value remains, render as the underlying feature directly
|
||||
if ($filteredValues.Count -eq 1) {
|
||||
$featureIds = $filteredValues[0].FeatureIds
|
||||
|
||||
if (-not $featureIds -or $featureIds.Count -eq 0) { continue }
|
||||
|
||||
$soleFid = $featureIds[0]
|
||||
|
||||
if ($featureMap.ContainsKey($soleFid)) {
|
||||
$soleFeature = $featureMap[$soleFid]
|
||||
$opt = 'Apply'
|
||||
if ($soleFeature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($soleFeature.FeatureId -match '^Enable') { $opt = 'Enable' }
|
||||
$items = @('No Change', $opt)
|
||||
$comboName = ("Feature_{0}_Combo" -f $soleFeature.FeatureId) -replace '[^a-zA-Z0-9_]', ''
|
||||
$combo = CreateLabeledCombo -parent $panel -labelText $soleFeature.Label -comboName $comboName -items $items
|
||||
# attach tooltip from Features.json if present
|
||||
if ($soleFeature.ToolTip -or $soleFeature.DisableWhenApplied -eq $true) {
|
||||
$tooltipText = $soleFeature.ToolTip
|
||||
if ($soleFeature.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 = $soleFeature.FeatureId; Label = $soleFeature.Label; Category = $categoryName }
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
$items = @('No Change') + ($filteredValues | 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 = $filteredValues; 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
|
||||
}
|
||||
167
Scripts/GUI/MainWindow-WindowChrome.ps1
Normal file
167
Scripts/GUI/MainWindow-WindowChrome.ps1
Normal file
@@ -0,0 +1,167 @@
|
||||
# MainWindow-WindowChrome.ps1
|
||||
# Window sizing, DPI-aware coordinate conversion, and UI animations.
|
||||
|
||||
# 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
|
||||
|
||||
if ($Window.WindowState -eq $windowStateMaximized) {
|
||||
$chrome = [System.Windows.Shell.WindowChrome]::GetWindowChrome($Window)
|
||||
$resizeBorder = if ($chrome) { $chrome.ResizeBorderThickness } else { [System.Windows.SystemParameters]::WindowResizeBorderThickness }
|
||||
|
||||
# Compute margins using screen bounds vs working area
|
||||
$marginLeft = $resizeBorder.Left
|
||||
$marginTop = $resizeBorder.Top
|
||||
$marginRight = $resizeBorder.Right
|
||||
$marginBottom = $resizeBorder.Bottom
|
||||
|
||||
$screen = Get-WindowScreen -Window $Window
|
||||
if ($screen) {
|
||||
$workTL = ConvertTo-ScreenPointToDip -Window $Window -X $screen.WorkingArea.Left -Y $screen.WorkingArea.Top
|
||||
$workSize = ConvertTo-ScreenPixelsToDip -Window $Window -Width $screen.WorkingArea.Width -Height $screen.WorkingArea.Height
|
||||
$screenTL = ConvertTo-ScreenPointToDip -Window $Window -X $screen.Bounds.Left -Y $screen.Bounds.Top
|
||||
$screenSize = ConvertTo-ScreenPixelsToDip -Window $Window -Width $screen.Bounds.Width -Height $screen.Bounds.Height
|
||||
|
||||
$marginLeft += ($workTL.X - $screenTL.X)
|
||||
$marginTop += ($workTL.Y - $screenTL.Y)
|
||||
$marginRight += ($screenTL.X + $screenSize.Width) - ($workTL.X + $workSize.Width)
|
||||
$marginBottom += ($screenTL.Y + $screenSize.Height) - ($workTL.Y + $workSize.Height)
|
||||
}
|
||||
|
||||
$MainBorder.Margin = [System.Windows.Thickness]::new($marginLeft, $marginTop, $marginRight, $marginBottom)
|
||||
$MainBorder.BorderThickness = [System.Windows.Thickness]::new(0)
|
||||
$MainBorder.CornerRadius = [System.Windows.CornerRadius]::new(0)
|
||||
$MainBorder.Effect = $null
|
||||
$TitleBarBackground.CornerRadius = [System.Windows.CornerRadius]::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)
|
||||
}
|
||||
}
|
||||
|
||||
# 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)
|
||||
}
|
||||
@@ -1,3 +1,11 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Creates a lightweight state object for the restore-backup dialog result.
|
||||
|
||||
.DESCRIPTION
|
||||
Encapsulates the user's dialog choice (Result), the selected backup file
|
||||
path, and the parsed backup payload so callers receive a single object.
|
||||
#>
|
||||
function New-RestoreDialogState {
|
||||
param(
|
||||
[string]$Result = 'Cancel',
|
||||
@@ -8,6 +16,16 @@ function New-RestoreDialogState {
|
||||
return @{ Result = $Result; SelectedFile = $SelectedFile; Backup = $Backup }
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Looks up a feature definition by ID from the provided feature catalog.
|
||||
|
||||
.PARAMETER FeatureId
|
||||
The identifier to search for (e.g. 'DisableTelemetry').
|
||||
|
||||
.PARAMETER Features
|
||||
A hashtable loaded from Features.json (FeatureId -> feature object).
|
||||
#>
|
||||
function Get-RestoreDialogFeatureDefinition {
|
||||
param(
|
||||
[string]$FeatureId,
|
||||
@@ -25,6 +43,21 @@ function Get-RestoreDialogFeatureDefinition {
|
||||
return $null
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Determines whether a feature can be automatically reverted via registry restore.
|
||||
|
||||
.DESCRIPTION
|
||||
Returns $true when the feature has a non-empty RegistryKey, indicating
|
||||
an apply .reg file exists that can be undone automatically. Features
|
||||
with custom logic (no RegistryKey) must be manually reverted.
|
||||
|
||||
.PARAMETER FeatureId
|
||||
The feature identifier to check.
|
||||
|
||||
.PARAMETER Features
|
||||
A hashtable loaded from Features.json.
|
||||
#>
|
||||
function Test-RestoreDialogFeatureCanAutoRevert {
|
||||
param(
|
||||
[string]$FeatureId,
|
||||
@@ -43,6 +76,20 @@ function Test-RestoreDialogFeatureCanAutoRevert {
|
||||
return $false
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resolves a human-readable label for a feature shown in the restore dialog.
|
||||
|
||||
.DESCRIPTION
|
||||
Returns the feature's Label from Features.json when found, falling back
|
||||
to the raw FeatureId string. For null/empty FeatureIds returns 'Unknown feature'.
|
||||
|
||||
.PARAMETER FeatureId
|
||||
The feature identifier to resolve a label for.
|
||||
|
||||
.PARAMETER Features
|
||||
A hashtable loaded from Features.json.
|
||||
#>
|
||||
function Get-RestoreDialogFeatureDisplayLabel {
|
||||
param(
|
||||
[string]$FeatureId,
|
||||
@@ -54,13 +101,28 @@ function Get-RestoreDialogFeatureDisplayLabel {
|
||||
}
|
||||
|
||||
$featureDefinition = Get-RestoreDialogFeatureDefinition -FeatureId $FeatureId -Features $Features
|
||||
if ($featureDefinition -and -not [string]::IsNullOrWhiteSpace([string]$featureDefinition.Label)) {
|
||||
if ($featureDefinition) {
|
||||
return [string]$featureDefinition.Label
|
||||
}
|
||||
|
||||
return $FeatureId
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Checks whether a feature should appear in the restore dialog's overview list.
|
||||
|
||||
.DESCRIPTION
|
||||
A feature is considered visible when it exists in the catalog and has
|
||||
a non-empty Category (meaning it belongs to a UI grouping). Features
|
||||
without a Category are hidden from the overview.
|
||||
|
||||
.PARAMETER FeatureId
|
||||
The feature identifier to check.
|
||||
|
||||
.PARAMETER Features
|
||||
A hashtable loaded from Features.json.
|
||||
#>
|
||||
function Test-RestoreDialogFeatureVisibleInOverview {
|
||||
param(
|
||||
[string]$FeatureId,
|
||||
@@ -79,18 +141,115 @@ function Test-RestoreDialogFeatureVisibleInOverview {
|
||||
return -not [string]::IsNullOrWhiteSpace([string]$featureDefinition.Category)
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Extracts deduplicated forward (apply) feature IDs from a backup payload.
|
||||
|
||||
.PARAMETER SelectedBackup
|
||||
The parsed backup object containing a SelectedFeatures property.
|
||||
#>
|
||||
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())
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Extracts deduplicated undo feature IDs from a backup payload.
|
||||
|
||||
.PARAMETER SelectedBackup
|
||||
The parsed backup object containing a SelectedUndoFeatures property.
|
||||
#>
|
||||
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())
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Merges forward and undo feature IDs from a backup into a single deduplicated list.
|
||||
|
||||
.PARAMETER SelectedBackup
|
||||
The parsed backup object containing SelectedFeatures and SelectedUndoFeatures.
|
||||
#>
|
||||
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())
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Convenience wrapper that returns all combined feature IDs from a backup.
|
||||
|
||||
.PARAMETER SelectedBackup
|
||||
The parsed backup object.
|
||||
#>
|
||||
function Get-SelectedFeatureIdsFromBackup {
|
||||
param($SelectedBackup)
|
||||
|
||||
return @(
|
||||
foreach ($featureId in @($SelectedBackup.SelectedFeatures)) {
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$featureId)) {
|
||||
[string]$featureId
|
||||
}
|
||||
}
|
||||
)
|
||||
return @(Get-CombinedSelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Splits selected feature IDs into revertible and non-revertible lists for display.
|
||||
|
||||
.DESCRIPTION
|
||||
Iterates the provided feature IDs, filters to those visible in the overview,
|
||||
and separates them into auto-revertible (has a RegistryKey) and non-revertible
|
||||
(requires manual undo) buckets. Each entry includes a display label.
|
||||
|
||||
.PARAMETER SelectedFeatureIds
|
||||
The list of feature IDs to categorize.
|
||||
|
||||
.PARAMETER Features
|
||||
A hashtable loaded from Features.json.
|
||||
#>
|
||||
function Get-RestoreBackupFeatureLists {
|
||||
param(
|
||||
[string[]]$SelectedFeatureIds,
|
||||
|
||||
@@ -1,3 +1,26 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Applies light or dark theme colors to a WPF window's resource dictionary.
|
||||
|
||||
.DESCRIPTION
|
||||
Iterates over a predefined set of theme color categories and
|
||||
populates the window's Resources with SolidColorBrush entries keyed by
|
||||
category and resource name (e.g. "AppAccentColor"). Additionally loads and
|
||||
merges shared XAML styles from the script's SharedStylesSchema path if
|
||||
available.
|
||||
|
||||
.PARAMETER window
|
||||
The WPF Window whose resource dictionary will be populated.
|
||||
|
||||
.PARAMETER usesDarkMode
|
||||
When $true, dark theme colors are applied; when $false, light theme colors.
|
||||
|
||||
.EXAMPLE
|
||||
SetWindowThemeResources -window $MainWindow -usesDarkMode $true
|
||||
|
||||
.EXAMPLE
|
||||
SetWindowThemeResources -window $Dialog -usesDarkMode $false
|
||||
#>
|
||||
# Sets resource colors for a WPF window based on dark mode preference
|
||||
function SetWindowThemeResources {
|
||||
param (
|
||||
@@ -5,80 +28,99 @@ function SetWindowThemeResources {
|
||||
[bool]$usesDarkMode
|
||||
)
|
||||
|
||||
if ($usesDarkMode) {
|
||||
$window.Resources.Add("BgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#202020")))
|
||||
$window.Resources.Add("FgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFFFFF")))
|
||||
$window.Resources.Add("CardBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#2b2b2b")))
|
||||
$window.Resources.Add("BorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#404040")))
|
||||
$window.Resources.Add("ButtonBorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#404040")))
|
||||
$window.Resources.Add("CheckBoxBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#272727")))
|
||||
$window.Resources.Add("CheckBoxBorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#808080")))
|
||||
$window.Resources.Add("CheckBoxHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#343434")))
|
||||
$window.Resources.Add("ComboBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#373737")))
|
||||
$window.Resources.Add("ComboHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#434343")))
|
||||
$window.Resources.Add("ComboItemBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#2c2c2c")))
|
||||
$window.Resources.Add("ComboItemHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#383838")))
|
||||
$window.Resources.Add("ComboItemSelectedColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#343434")))
|
||||
$window.Resources.Add("AccentColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFD700")))
|
||||
$window.Resources.Add("ButtonDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#434343")))
|
||||
$window.Resources.Add("ButtonTextDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#989898")))
|
||||
$window.Resources.Add("SecondaryButtonBg", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#393939")))
|
||||
$window.Resources.Add("SecondaryButtonHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#2a2a2a")))
|
||||
$window.Resources.Add("SecondaryButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#1e1e1e")))
|
||||
$window.Resources.Add("SecondaryButtonDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#3b3b3b")))
|
||||
$window.Resources.Add("SecondaryButtonTextDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#787878")))
|
||||
$window.Resources.Add("InputFocusColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#1f1f1f")))
|
||||
$window.Resources.Add("ScrollBarThumbColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#3d3d3d")))
|
||||
$window.Resources.Add("ScrollBarThumbHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#4b4b4b")))
|
||||
$window.Resources.Add("TitlebarButtonHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#2d2d2d")))
|
||||
$window.Resources.Add("TitlebarButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#292929")))
|
||||
$window.Resources.Add("AppIdColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#afafaf")))
|
||||
$window.Resources.Add("SearchHighlightColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#4A4A2A")))
|
||||
$window.Resources.Add("SearchHighlightActiveColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#8A7000")))
|
||||
$window.Resources.Add("TableHeaderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#333333")))
|
||||
}
|
||||
else {
|
||||
$window.Resources.Add("BgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f3f3f3")))
|
||||
$window.Resources.Add("FgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#000000")))
|
||||
$window.Resources.Add("CardBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#fbfbfb")))
|
||||
$window.Resources.Add("BorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#ededed")))
|
||||
$window.Resources.Add("ButtonBorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#d3d3d3")))
|
||||
$window.Resources.Add("CheckBoxBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f5f5f5")))
|
||||
$window.Resources.Add("CheckBoxBorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#898989")))
|
||||
$window.Resources.Add("CheckBoxHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#ececec")))
|
||||
$window.Resources.Add("ComboBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFFFFF")))
|
||||
$window.Resources.Add("ComboHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f8f8f8")))
|
||||
$window.Resources.Add("ComboItemBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f9f9f9")))
|
||||
$window.Resources.Add("ComboItemHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f0f0f0")))
|
||||
$window.Resources.Add("ComboItemSelectedColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f3f3f3")))
|
||||
$window.Resources.Add("AccentColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#ffae00")))
|
||||
$window.Resources.Add("ButtonDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#bfbfbf")))
|
||||
$window.Resources.Add("ButtonTextDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#ffffff")))
|
||||
$window.Resources.Add("SecondaryButtonBg", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#fbfbfb")))
|
||||
$window.Resources.Add("SecondaryButtonHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f6f6f6")))
|
||||
$window.Resources.Add("SecondaryButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f0f0f0")))
|
||||
$window.Resources.Add("SecondaryButtonDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f7f7f7")))
|
||||
$window.Resources.Add("SecondaryButtonTextDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#b7b7b7")))
|
||||
$window.Resources.Add("InputFocusColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#fbfbfb")))
|
||||
$window.Resources.Add("ScrollBarThumbColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#b9b9b9")))
|
||||
$window.Resources.Add("ScrollBarThumbHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#8b8b8b")))
|
||||
$window.Resources.Add("TitlebarButtonHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e1e1e1")))
|
||||
$window.Resources.Add("TitlebarButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e6e6e6")))
|
||||
$window.Resources.Add("AppIdColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#666666")))
|
||||
$window.Resources.Add("SearchHighlightColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFF4CE")))
|
||||
$window.Resources.Add("SearchHighlightActiveColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFD966")))
|
||||
$window.Resources.Add("TableHeaderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#ffffff")))
|
||||
$ThemeColor = @{
|
||||
App = @{
|
||||
AccentColor = @{ Light = '#ffae00'; Dark = '#ffd700' }
|
||||
BorderColor = @{ Light = '#ededed'; Dark = '#404040' }
|
||||
BgColor = @{ Light = '#f3f3f3'; Dark = '#202020' }
|
||||
FgColor = @{ Light = '#000000'; Dark = '#ffffff' }
|
||||
IdColor = @{ Light = '#666666'; Dark = '#afafaf' }
|
||||
}
|
||||
|
||||
Card = @{
|
||||
BgColor = @{ Light = '#fbfbfb'; Dark = '#2b2b2b' }
|
||||
}
|
||||
|
||||
Button = @{
|
||||
BorderColor = @{ Light = '#d3d3d3'; Dark = '#404040' }
|
||||
BgColor = @{ Light = '#0067c0'; Dark = '#0067c0' }
|
||||
DisabledColor = @{ Light = '#bfbfbf'; Dark = '#434343' }
|
||||
HoverColor = @{ Light = '#1975c5'; Dark = '#1975c5' }
|
||||
PressedColor = @{ Light = '#3183ca'; Dark = '#3183ca' }
|
||||
TextDisabledColor = @{ Light = '#ffffff'; Dark = '#989898' }
|
||||
}
|
||||
|
||||
SecondaryButton = @{
|
||||
BgColor = @{ Light = '#fbfbfb'; Dark = '#393939' }
|
||||
DisabledColor = @{ Light = '#f7f7f7'; Dark = '#3b3b3b' }
|
||||
HoverColor = @{ Light = '#f6f6f6'; Dark = '#2a2a2a' }
|
||||
PressedColor = @{ Light = '#f0f0f0'; Dark = '#1e1e1e' }
|
||||
TextDisabledColor = @{ Light = '#b7b7b7'; Dark = '#787878' }
|
||||
}
|
||||
|
||||
CheckBox = @{
|
||||
BgColor = @{ Light = '#f5f5f5'; Dark = '#272727' }
|
||||
BorderColor = @{ Light = '#898989'; Dark = '#808080' }
|
||||
HoverColor = @{ Light = '#ececec'; Dark = '#343434' }
|
||||
}
|
||||
|
||||
ComboBox = @{
|
||||
BgColor = @{ Light = '#ffffff'; Dark = '#373737' }
|
||||
HoverColor = @{ Light = '#f8f8f8'; Dark = '#434343' }
|
||||
ItemBgColor = @{ Light = '#f9f9f9'; Dark = '#2c2c2c' }
|
||||
ItemHoverColor = @{ Light = '#f0f0f0'; Dark = '#383838' }
|
||||
ItemSelectedColor = @{ Light = '#f3f3f3'; Dark = '#343434' }
|
||||
}
|
||||
|
||||
TextBox = @{
|
||||
BorderColor = @{ Light = '#bdbdbd'; Dark = '#989a9d' }
|
||||
BgColor = @{ Light = '#fbfbfb'; Dark = '#2d2d2d' }
|
||||
FocusColor = @{ Light = '#ffffff'; Dark = '#1f1f1f' }
|
||||
HoverColor = @{ Light = '#f6f6f6'; Dark = '#323232' }
|
||||
SideBorderColor = @{ Light = '#ececec'; Dark = '#343434' }
|
||||
}
|
||||
|
||||
ScrollBar = @{
|
||||
ThumbColor = @{ Light = '#b9b9b9'; Dark = '#3d3d3d' }
|
||||
ThumbHoverColor = @{ Light = '#8b8b8b'; Dark = '#4b4b4b' }
|
||||
}
|
||||
|
||||
TitleBar = @{
|
||||
ButtonHoverColor = @{ Light = '#dcdcdc'; Dark = '#353535' }
|
||||
ButtonPressedColor = @{ Light = '#cccccc'; Dark = '#333333' }
|
||||
CloseHoverColor = @{ Light = '#e81123'; Dark = '#e81123' }
|
||||
ClosePressedColor = @{ Light = '#f1707a'; Dark = '#f1707a' }
|
||||
UnfocusedFgColor = @{ Light = '#868686'; Dark = '#969696' }
|
||||
}
|
||||
|
||||
Search = @{
|
||||
HighlightActiveColor = @{ Light = '#ffd966'; Dark = '#8a7000' }
|
||||
HighlightColor = @{ Light = '#fff4ce'; Dark = '#4a4a2a' }
|
||||
}
|
||||
|
||||
Table = @{
|
||||
HeaderColor = @{ Light = '#ffffff'; Dark = '#303030' }
|
||||
}
|
||||
|
||||
Icon = @{
|
||||
ErrorColor = @{ Light = '#e81123'; Dark = '#e81123' }
|
||||
InformationColor = @{ Light = '#0078d4'; Dark = '#0078d4' }
|
||||
QuestionColor = @{ Light = '#0078d4'; Dark = '#0078d4' }
|
||||
SuccessColor = @{ Light = '#107c10'; Dark = '#107c10' }
|
||||
WarningColor = @{ Light = '#ffb900'; Dark = '#ffb900' }
|
||||
}
|
||||
}
|
||||
|
||||
$window.Resources.Add("ButtonBg", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0067c0")))
|
||||
$window.Resources.Add("ButtonHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#1E88E5")))
|
||||
$window.Resources.Add("ButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#3284cc")))
|
||||
$window.Resources.Add("CloseHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#c42b1c")))
|
||||
$window.Resources.Add("InformationIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0078D4")))
|
||||
$window.Resources.Add("SuccessIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#107C10")))
|
||||
$window.Resources.Add("WarningIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFB900")))
|
||||
$window.Resources.Add("ErrorIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#E81123")))
|
||||
$window.Resources.Add("QuestionIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0078D4")))
|
||||
$Theme = if ($usesDarkMode) { 'Dark' } else { 'Light' }
|
||||
|
||||
foreach ($Group in $ThemeColor.GetEnumerator()) {
|
||||
foreach ($Resource in $Group.Value.GetEnumerator()) {
|
||||
$ResourceName = $Group.Key + $Resource.Key
|
||||
$window.Resources[$ResourceName] = [System.Windows.Media.SolidColorBrush]::new(
|
||||
[System.Windows.Media.ColorConverter]::ConvertFromString($Resource.Value[$Theme])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
# Load and merge shared styles
|
||||
if ($script:SharedStylesSchema -and (Test-Path $script:SharedStylesSchema)) {
|
||||
|
||||
@@ -53,12 +53,12 @@ function Show-AppSelectionWindow {
|
||||
$window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{})
|
||||
|
||||
$appsPanel.Children.Clear()
|
||||
$listOfApps = ""
|
||||
$listOfApps = $null
|
||||
|
||||
if ($onlyInstalledBox.IsChecked -and ($script:WingetInstalled -eq $true)) {
|
||||
# Attempt to get a list of installed apps via WinGet, times out after 10 seconds
|
||||
$listOfApps = GetInstalledAppsViaWinget -TimeOut 10
|
||||
if (-not $listOfApps) {
|
||||
$listOfApps = GetInstalledAppsViaWinget -TimeOut 10 -NonBlocking
|
||||
if ($null -eq $listOfApps) {
|
||||
# Show error that the script was unable to get list of apps from WinGet
|
||||
Show-MessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' -Owner $window | Out-Null
|
||||
$onlyInstalledBox.IsChecked = $false
|
||||
@@ -130,15 +130,11 @@ function Show-AppSelectionWindow {
|
||||
return
|
||||
}
|
||||
|
||||
if ($selectedApps -contains "Microsoft.WindowsStore" -and -not $Silent) {
|
||||
$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
|
||||
}
|
||||
if (-not (ConfirmUnsafeAppRemoval -SelectedApps $selectedApps -Owner $window)) {
|
||||
return
|
||||
}
|
||||
|
||||
SaveCustomAppsListToFile -appsList $selectedApps
|
||||
$script:SelectedApps = $selectedApps
|
||||
|
||||
$window.DialogResult = $true
|
||||
})
|
||||
|
||||
@@ -7,26 +7,6 @@ function Show-ApplyModal {
|
||||
)
|
||||
|
||||
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
|
||||
|
||||
# P/Invoke helpers for forcing focus back after Explorer restart
|
||||
if (-not ([System.Management.Automation.PSTypeName]'Win11Debloat.FocusHelper').Type) {
|
||||
Add-Type -Namespace Win11Debloat -Name FocusHelper -MemberDefinition @'
|
||||
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
||||
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr lpdwProcessId);
|
||||
[DllImport("user32.dll")] public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
|
||||
[DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
|
||||
|
||||
public static void ForceActivate(IntPtr hwnd) {
|
||||
IntPtr fg = GetForegroundWindow();
|
||||
uint fgThread = GetWindowThreadProcessId(fg, IntPtr.Zero);
|
||||
uint myThread = GetCurrentThreadId();
|
||||
if (fgThread != myThread) AttachThreadInput(myThread, fgThread, true);
|
||||
SetForegroundWindow(hwnd);
|
||||
if (fgThread != myThread) AttachThreadInput(myThread, fgThread, false);
|
||||
}
|
||||
'@
|
||||
}
|
||||
|
||||
$usesDarkMode = GetSystemUsesDarkMode
|
||||
|
||||
@@ -89,7 +69,7 @@ function Show-ApplyModal {
|
||||
$script:ApplyProgressBarEl.Value = 0
|
||||
$script:ApplyModalInErrorState = $false
|
||||
|
||||
# Set up progress callback for ExecuteAllChanges
|
||||
# Set up progress callback for Invoke-AllChanges
|
||||
$script:ApplyProgressCallback = {
|
||||
param($currentStep, $totalSteps, $stepName)
|
||||
$script:ApplyStepNameEl.Text = $stepName
|
||||
@@ -122,7 +102,7 @@ function Show-ApplyModal {
|
||||
# Run changes in background to keep UI responsive
|
||||
$applyWindow.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{
|
||||
try {
|
||||
ExecuteAllChanges
|
||||
Invoke-AllChanges
|
||||
|
||||
$registryImportFailureCount = [int]$script:RegistryImportFailures
|
||||
|
||||
@@ -133,8 +113,7 @@ function Show-ApplyModal {
|
||||
# Wait for Explorer to finish relaunching, then reclaim focus.
|
||||
Start-Sleep -Milliseconds 800
|
||||
$applyWindow.Dispatcher.Invoke([action]{
|
||||
$hwnd = (New-Object System.Windows.Interop.WindowInteropHelper($applyWindow)).Handle
|
||||
[Win11Debloat.FocusHelper]::ForceActivate($hwnd)
|
||||
$applyWindow.Activate()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -178,7 +157,7 @@ function Show-ApplyModal {
|
||||
$tb = [System.Windows.Controls.TextBlock]::new()
|
||||
$tb.Text = "$([char]0x2022) $featureName"
|
||||
$tb.FontSize = 12
|
||||
$tb.SetResourceReference([System.Windows.Controls.TextBlock]::ForegroundProperty, 'FgColor')
|
||||
$tb.SetResourceReference([System.Windows.Controls.TextBlock]::ForegroundProperty, "AppFgColor")
|
||||
$tb.Opacity = 0.85
|
||||
$tb.Margin = [System.Windows.Thickness]::new(0, 2, 0, 0)
|
||||
$applyRebootList.Children.Add($tb) | Out-Null
|
||||
@@ -186,7 +165,7 @@ function Show-ApplyModal {
|
||||
$applyRebootPanel.Visibility = 'Visible'
|
||||
}
|
||||
else {
|
||||
$script:ApplyCompletionMessageEl.Text = "Your clean system is ready. Thanks for using Win11Debloat!"
|
||||
$script:ApplyCompletionMessageEl.Text = "Your system is ready. Thanks for using Win11Debloat!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Hides the currently displayed bubble popup.
|
||||
|
||||
.DESCRIPTION
|
||||
Closes the bubble popup with a smooth fade-out animation (220ms). If the
|
||||
-Immediate switch is used, the popup is closed instantly without animation.
|
||||
This function is called automatically by Show-Bubble's timer and can also
|
||||
be invoked manually to dismiss the bubble early.
|
||||
|
||||
.PARAMETER Immediate
|
||||
If specified, the bubble popup is closed instantly without a fade-out
|
||||
animation. Any pending close timer is also stopped.
|
||||
|
||||
.EXAMPLE
|
||||
Hide-Bubble
|
||||
|
||||
.EXAMPLE
|
||||
Hide-Bubble -Immediate
|
||||
#>
|
||||
function Hide-Bubble {
|
||||
param (
|
||||
[Parameter(Mandatory=$false)]
|
||||
@@ -37,6 +57,34 @@ function Hide-Bubble {
|
||||
$bubblePanel.BeginAnimation([System.Windows.UIElement]::OpacityProperty, $fadeOut)
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Displays a transient bubble popup hint anchored above a target control.
|
||||
|
||||
.DESCRIPTION
|
||||
Shows a WPF popup styled as a speech bubble above the specified target
|
||||
control. The bubble fades in with a animation, displays for a configurable
|
||||
duration, then fades out. Any previously shown bubble is dismissed
|
||||
immediately before showing the new one.
|
||||
|
||||
.PARAMETER TargetControl
|
||||
The WPF Control above which the bubble popup will be placed. This
|
||||
parameter is mandatory.
|
||||
|
||||
.PARAMETER Message
|
||||
The text message to display inside the bubble. Defaults to
|
||||
'View the selected changes here'.
|
||||
|
||||
.PARAMETER DurationSeconds
|
||||
The number of seconds the bubble remains visible before auto-hiding.
|
||||
The minimum value is 1 second. Defaults to 5 seconds.
|
||||
|
||||
.EXAMPLE
|
||||
Show-Bubble -TargetControl $myButton
|
||||
|
||||
.EXAMPLE
|
||||
Show-Bubble -TargetControl $myButton -Message 'Changes saved!' -DurationSeconds 3
|
||||
#>
|
||||
function Show-Bubble {
|
||||
param (
|
||||
[Parameter(Mandatory=$true)]
|
||||
|
||||
@@ -21,7 +21,8 @@ function Show-ImportExportConfigWindow {
|
||||
$Owner.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Visible' })
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
catch { }
|
||||
|
||||
# Load XAML from schema file
|
||||
$schemaPath = $script:ImportExportConfigSchema
|
||||
@@ -52,7 +53,8 @@ function Show-ImportExportConfigWindow {
|
||||
if ($mainCheckBoxStyle) {
|
||||
$dlg.Resources.Add([type][System.Windows.Controls.CheckBox], $mainCheckBoxStyle)
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
catch { }
|
||||
|
||||
# Populate named elements
|
||||
$dlg.Title = $Title
|
||||
@@ -78,7 +80,7 @@ function Show-ImportExportConfigWindow {
|
||||
$cb.Margin = [System.Windows.Thickness]::new(0,0,0,4)
|
||||
$cb.FontSize = 14
|
||||
$cb.FontWeight = [System.Windows.FontWeights]::Medium
|
||||
$cb.Foreground = $dlg.FindResource('FgColor')
|
||||
$cb.Foreground = $dlg.FindResource("AppFgColor")
|
||||
if ($DisabledCategories -contains $cat) {
|
||||
$cb.IsChecked = $false
|
||||
$cb.IsEnabled = $false
|
||||
@@ -93,9 +95,9 @@ function Show-ImportExportConfigWindow {
|
||||
$detailsText = New-Object System.Windows.Controls.TextBlock
|
||||
$detailsText.Text = $CategoryDetails[$cat]
|
||||
$detailsText.FontSize = 12
|
||||
$detailsText.Foreground = $dlg.FindResource('FgColor')
|
||||
$detailsText.Margin = [System.Windows.Thickness]::new(30,0,0,0)
|
||||
$detailsText.Opacity = 0.75
|
||||
$detailsText.Foreground = $dlg.FindResource("AppFgColor")
|
||||
$detailsText.Margin = [System.Windows.Thickness]::new(32,0,0,0)
|
||||
$detailsText.Opacity = if ($DisabledCategories -contains $cat) { 0.45 } else { 0.75 }
|
||||
$detailsText.TextWrapping = [System.Windows.TextWrapping]::Wrap
|
||||
$container.Children.Add($detailsText) | Out-Null
|
||||
}
|
||||
@@ -286,11 +288,17 @@ function Build-CategoryDetails {
|
||||
$details = @{}
|
||||
|
||||
if ($AppCount -gt 0) {
|
||||
$details['Applications'] = "$AppCount app$(if ($AppCount -ne 1) { 's' })"
|
||||
$details['Applications'] = "$AppCount app$(if ($AppCount -ne 1) { 's' }) selected"
|
||||
}
|
||||
else {
|
||||
$details['Applications'] = 'No apps selected'
|
||||
}
|
||||
|
||||
if ($TweakCount -gt 0) {
|
||||
$details['System Tweaks'] = "$TweakCount tweak$(if ($TweakCount -ne 1) { 's' })"
|
||||
$details['System Tweaks'] = "$TweakCount tweak$(if ($TweakCount -ne 1) { 's' }) selected"
|
||||
}
|
||||
else {
|
||||
$details['System Tweaks'] = 'No tweaks selected'
|
||||
}
|
||||
|
||||
if ($DeploymentSettings) {
|
||||
@@ -381,7 +389,7 @@ function Export-Configuration {
|
||||
$deploymentSettings = Get-DeploymentSettings -Owner $Owner -UserSelectionCombo $UserSelectionCombo -OtherUsernameTextBox $OtherUsernameTextBox
|
||||
$categoryDetails = Build-CategoryDetails -AppCount $selectedApps.Count -TweakCount $tweakSettings.Count -DeploymentSettings $deploymentSettings
|
||||
|
||||
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Export Configuration' -Prompt 'Select the settings you wish to include in your export.' -DisabledCategories $disabledCategories -CategoryDetails $categoryDetails -ActionLabel 'Export Settings'
|
||||
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Export Configuration' -Prompt 'Create a configuration file based on the currently selected settings. You can choose which settings categories you wish to include in the export.' -DisabledCategories $disabledCategories -CategoryDetails $categoryDetails -ActionLabel 'Export Settings'
|
||||
if (-not $categories) {
|
||||
Write-Host 'Export canceled.'
|
||||
return
|
||||
@@ -413,6 +421,12 @@ function Export-Configuration {
|
||||
|
||||
Write-Host "Exporting configuration to '$($saveDialog.FileName)'... (Categories: $($categories -join ', '))"
|
||||
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Write-Host "[WhatIf] Export configuration to '$($saveDialog.FileName)'" -ForegroundColor Cyan
|
||||
Show-MessageBox -Message "[WhatIf] Configuration would be exported to this file (no file written)." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null
|
||||
return
|
||||
}
|
||||
|
||||
if (SaveToFile -Config $config -FilePath $saveDialog.FileName) {
|
||||
Write-Host "Configuration exported successfully: $($saveDialog.FileName)"
|
||||
Show-MessageBox -Message "Configuration exported successfully." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null
|
||||
@@ -475,7 +489,7 @@ function Import-Configuration {
|
||||
$tweakCount = @($config.Tweaks | Where-Object { $_ -and $_.Name -and $_.Value -eq $true }).Count
|
||||
$categoryDetails = Build-CategoryDetails -AppCount $appCount -TweakCount $tweakCount -DeploymentSettings @($config.Deployment)
|
||||
|
||||
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Import Configuration' -Prompt 'Select the settings you wish to import. You can review and modify them before they are applied.' -Categories $availableCategories -CategoryDetails $categoryDetails -ActionLabel 'Import Settings'
|
||||
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Import Configuration' -Prompt 'Choose the settings categories that you wish to import. You can review and modify the imported settings before they are applied.' -Categories $availableCategories -CategoryDetails $categoryDetails -ActionLabel 'Import Settings'
|
||||
if (-not $categories) {
|
||||
Write-Host 'Import canceled.'
|
||||
return
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -87,27 +87,27 @@ function Show-MessageBox {
|
||||
switch ($Icon) {
|
||||
'Information' {
|
||||
$iconText.Text = [char]0xE946
|
||||
$iconText.Foreground = $msgWindow.FindResource('InformationIconColor')
|
||||
$iconText.Foreground = $msgWindow.FindResource('IconInformationColor')
|
||||
$iconText.Visibility = 'Visible'
|
||||
}
|
||||
'Success' {
|
||||
$iconText.Text = [char]0xE73E
|
||||
$iconText.Foreground = $msgWindow.FindResource('SuccessIconColor')
|
||||
$iconText.Foreground = $msgWindow.FindResource('IconSuccessColor')
|
||||
$iconText.Visibility = 'Visible'
|
||||
}
|
||||
'Warning' {
|
||||
$iconText.Text = [char]0xE7BA
|
||||
$iconText.Foreground = $msgWindow.FindResource('WarningIconColor')
|
||||
$iconText.Foreground = $msgWindow.FindResource('IconWarningColor')
|
||||
$iconText.Visibility = 'Visible'
|
||||
}
|
||||
'Error' {
|
||||
$iconText.Text = [char]0xEA39
|
||||
$iconText.Foreground = $msgWindow.FindResource('ErrorIconColor')
|
||||
$iconText.Foreground = $msgWindow.FindResource('IconErrorColor')
|
||||
$iconText.Visibility = 'Visible'
|
||||
}
|
||||
'Question' {
|
||||
$iconText.Text = [char]0xE897
|
||||
$iconText.Foreground = $msgWindow.FindResource('QuestionIconColor')
|
||||
$iconText.Foreground = $msgWindow.FindResource('IconQuestionColor')
|
||||
$iconText.Visibility = 'Visible'
|
||||
}
|
||||
default {
|
||||
@@ -121,6 +121,8 @@ function Show-MessageBox {
|
||||
$button1.Content = 'OK'
|
||||
$button1.Add_Click({ $msgWindow.Tag = 'OK'; $msgWindow.Close() })
|
||||
$button2.Visibility = 'Collapsed'
|
||||
# Right-align sole button by moving it to column 1
|
||||
[System.Windows.Controls.Grid]::SetColumn($button1, 1)
|
||||
}
|
||||
'OKCancel' {
|
||||
$button1.Content = 'OK'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
function Show-RestoreBackupDialog {
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[System.Windows.Window]$Owner = $null
|
||||
)
|
||||
|
||||
@@ -70,6 +69,9 @@ function Show-RestoreBackupDialog {
|
||||
$backupCreatedText = $window.FindName('BackupCreatedText')
|
||||
$backupTargetText = $window.FindName('BackupTargetText')
|
||||
$featuresItemsControl = $window.FindName('FeaturesItemsControl')
|
||||
$reappliedSeparator = $window.FindName('ReappliedSeparator')
|
||||
$reappliedPanel = $window.FindName('ReappliedPanel')
|
||||
$reappliedFeaturesItemsControl = $window.FindName('ReappliedFeaturesItemsControl')
|
||||
$nonRevertibleSeparator = $window.FindName('NonRevertibleSeparator')
|
||||
$nonRevertiblePanel = $window.FindName('NonRevertiblePanel')
|
||||
$nonRevertibleFeaturesItemsControl = $window.FindName('NonRevertibleFeaturesItemsControl')
|
||||
@@ -119,6 +121,8 @@ function Show-RestoreBackupDialog {
|
||||
|
||||
$overviewFeaturesSection.Visibility = 'Collapsed'
|
||||
$overviewSummaryText.Visibility = 'Visible'
|
||||
$reappliedSeparator.Visibility = 'Collapsed'
|
||||
$reappliedPanel.Visibility = 'Collapsed'
|
||||
$nonRevertibleSeparator.Visibility = 'Collapsed'
|
||||
$nonRevertiblePanel.Visibility = 'Collapsed'
|
||||
$introInfoPanel.Visibility = 'Collapsed'
|
||||
@@ -192,6 +196,10 @@ function Show-RestoreBackupDialog {
|
||||
$primaryActionBtn.Visibility = 'Visible'
|
||||
$primaryActionBtn.IsDefault = $true
|
||||
$chooseRegistryBtn.IsDefault = $false
|
||||
|
||||
# Show intro panel so user can configure scope & auto-detect
|
||||
$startMenuAutoBackupCheck.IsChecked = $true
|
||||
$state.SelectedStartMenuBackupFilePath = $null
|
||||
& $refreshStartMenuUi
|
||||
}
|
||||
|
||||
@@ -215,13 +223,33 @@ function Show-RestoreBackupDialog {
|
||||
}
|
||||
}
|
||||
|
||||
$selectedFeatureIds = Get-SelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup
|
||||
$featureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds $selectedFeatureIds -Features $script:Features
|
||||
$revertibleFeaturesList = @($featureLists.Revertible)
|
||||
$nonRevertibleFeaturesList = @($featureLists.NonRevertible)
|
||||
Write-Host "Backup overview prepared. Revertible=$($revertibleFeaturesList.Count), NonRevertible=$($nonRevertibleFeaturesList.Count)"
|
||||
$selectedForwardFeatureIds = @(Get-SelectedForwardFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
|
||||
$selectedUndoFeatureIds = @(Get-SelectedUndoFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
|
||||
|
||||
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.'
|
||||
}
|
||||
|
||||
@@ -229,13 +257,16 @@ function Show-RestoreBackupDialog {
|
||||
$backupCreatedText.Text = $createdText
|
||||
$backupTargetText.Text = GetFriendlyRegistryBackupTarget -Target ([string]$SelectedBackup.Target)
|
||||
$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'
|
||||
$nonRevertibleFeaturesItemsControl.ItemsSource = $nonRevertibleFeaturesList
|
||||
|
||||
$hasNonRevertibleItems = ($nonRevertibleFeaturesList.Count -gt 0)
|
||||
if ($hasNonRevertibleItems) { $nonRevertiblePanel.Visibility = 'Visible' } else { $nonRevertiblePanel.Visibility = 'Collapsed' }
|
||||
if ($hasNonRevertibleItems) { $nonRevertibleSeparator.Visibility = 'Visible' } else { $nonRevertibleSeparator.Visibility = 'Collapsed' }
|
||||
if ($hasNonRevertibleItems -and ($revertibleFeaturesList.Count -gt 0 -or $reappliedFeaturesList.Count -gt 0)) { $nonRevertibleSeparator.Visibility = 'Visible' } else { $nonRevertibleSeparator.Visibility = 'Collapsed' }
|
||||
$introInfoPanel.Visibility = 'Collapsed'
|
||||
$overviewPanel.Visibility = 'Visible'
|
||||
|
||||
@@ -295,6 +326,17 @@ function Show-RestoreBackupDialog {
|
||||
return
|
||||
}
|
||||
|
||||
if (-not $useManualBackupFile) {
|
||||
$scopeInfo = & $getStartMenuScopeInfo
|
||||
$autoBackupPath = Get-StartMenuBackupPath -Scope $scopeInfo.Scope
|
||||
if ($null -eq $autoBackupPath) {
|
||||
$scopeText = $scopeInfo.SummaryText
|
||||
Show-MessageBox -Owner $window -Title 'No Backup Found' -Message "No Start Menu backup file was found for $scopeText. Uncheck 'Automatically find Start Menu backup' to select a backup file manually." -Button 'OK' -Icon 'Warning' | Out-Null
|
||||
return
|
||||
}
|
||||
$state.SelectedStartMenuBackupFilePath = if ($scopeInfo.Scope -eq 'CurrentUser') { $autoBackupPath } else { $null }
|
||||
}
|
||||
|
||||
$window.Tag = @{
|
||||
Result = 'RestoreStartMenu'
|
||||
StartMenuScope = $scope
|
||||
@@ -326,6 +368,7 @@ function Show-RestoreBackupDialog {
|
||||
})
|
||||
|
||||
$startMenuScopeCombo.Add_SelectionChanged({
|
||||
$state.SelectedStartMenuBackupFilePath = $null
|
||||
& $refreshStartMenuUi
|
||||
})
|
||||
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
function Show-RestoreBackupWindow {
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[System.Windows.Window]$Owner = $null
|
||||
)
|
||||
|
||||
try {
|
||||
Write-Host 'Opening restore backup dialog.'
|
||||
|
||||
$restoreResult = [PSCustomObject]@{
|
||||
RestoredRegistry = $false
|
||||
RestoredStartMenu = $false
|
||||
}
|
||||
|
||||
$dialogResult = Show-RestoreBackupDialog -Owner $Owner
|
||||
if (-not $dialogResult -or $dialogResult.Result -eq 'Cancel') {
|
||||
Write-Host 'Restore canceled by user.'
|
||||
return
|
||||
return $restoreResult
|
||||
}
|
||||
|
||||
$successMessage = $null
|
||||
@@ -23,8 +27,16 @@ function Show-RestoreBackupWindow {
|
||||
}
|
||||
|
||||
Write-Host "User confirmed registry restore for $($backup.Target)."
|
||||
Restore-RegistryBackupState -Backup $backup
|
||||
$successMessage = 'Registry backup restored successfully. Please restart your computer for all changes to take effect.'
|
||||
$restoreOpResult = Restore-RegistryBackupState -Backup $backup
|
||||
if ($restoreOpResult -and $restoreOpResult.Result) {
|
||||
$restoreResult.RestoredRegistry = $true
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
$successMessage = '[WhatIf] Registry backup would be restored (no changes made).'
|
||||
}
|
||||
else {
|
||||
$successMessage = 'Registry backup restored successfully. Some changes may require a restart to take effect.'
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif ($dialogResult.Result -eq 'RestoreStartMenu') {
|
||||
$scope = $dialogResult.StartMenuScope
|
||||
@@ -62,13 +74,18 @@ function Show-RestoreBackupWindow {
|
||||
$warningMessage = "The Start Menu backup was successfully restored for $successCount user(s).`nSome users could not be restored:`n$failureSummary"
|
||||
}
|
||||
else {
|
||||
if ($scope -eq 'AllUsers') {
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
$successMessage = '[WhatIf] Start Menu backup would be restored (no changes made).'
|
||||
}
|
||||
elseif ($scope -eq 'AllUsers') {
|
||||
$successMessage = "The Start Menu backup was successfully restored for all users. The changes will apply the next time users sign in."
|
||||
}
|
||||
else {
|
||||
$successMessage = "The Start Menu backup was successfully restored for the current user. The changes will apply the next time you sign in."
|
||||
}
|
||||
}
|
||||
|
||||
$restoreResult.RestoredStartMenu = $true
|
||||
}
|
||||
|
||||
if ($warningMessage) {
|
||||
@@ -79,10 +96,16 @@ function Show-RestoreBackupWindow {
|
||||
Write-Host "$successMessage"
|
||||
Show-MessageBox -Title 'Backup Restored' -Message $successMessage -Icon Success
|
||||
}
|
||||
|
||||
return $restoreResult
|
||||
}
|
||||
catch {
|
||||
$errorMessage = if ($_.Exception.Message) { $_.Exception.Message } else { 'An unexpected error occurred.' }
|
||||
Write-Error "Restore operation failed: $errorMessage"
|
||||
Show-MessageBox -Title 'Error' -Message "Restore failed: $errorMessage" -Icon Error
|
||||
return [PSCustomObject]@{
|
||||
RestoredRegistry = $false
|
||||
RestoredStartMenu = $false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ param (
|
||||
[string]$User,
|
||||
[switch]$NoRestartExplorer,
|
||||
[switch]$CreateRestorePoint,
|
||||
[switch]$RunAppsListGenerator,
|
||||
[switch]$RunDefaults,
|
||||
[switch]$RunDefaultsLite,
|
||||
[switch]$RunSavedSettings,
|
||||
@@ -15,11 +14,8 @@ param (
|
||||
[string]$Apps,
|
||||
[string]$AppRemovalTarget,
|
||||
[switch]$RemoveApps,
|
||||
[switch]$RemoveAppsCustom,
|
||||
[switch]$RemoveGamingApps,
|
||||
[switch]$RemoveCommApps,
|
||||
[switch]$RemoveHPApps,
|
||||
[switch]$RemoveW11Outlook,
|
||||
[switch]$ForceRemoveEdge,
|
||||
[switch]$DisableDVR,
|
||||
[switch]$DisableGameBarIntegration,
|
||||
@@ -58,7 +54,7 @@ param (
|
||||
[switch]$HideSearchTb, [switch]$ShowSearchIconTb, [switch]$ShowSearchLabelTb, [switch]$ShowSearchBoxTb,
|
||||
[switch]$HideTaskview,
|
||||
[switch]$DisableStartRecommended,
|
||||
[switch]$DisableStartAllApps,
|
||||
[switch]$DisableStartAllApps, [switch]$StartAllAppsCategory, [switch]$StartAllAppsGrid, [switch]$StartAllAppsList,
|
||||
[switch]$DisableStartPhoneLink,
|
||||
[switch]$DisableCopilot,
|
||||
[switch]$DisableRecall,
|
||||
@@ -135,12 +131,12 @@ catch {
|
||||
Exit
|
||||
}
|
||||
|
||||
Write-Output ""
|
||||
Write-Output "> Cleaning up old Win11Debloat folder..."
|
||||
|
||||
# Remove old script folder if it exists, but keep config and log files
|
||||
# Remove old script folder if it exists, but keep configs, logs and backups
|
||||
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'
|
||||
@@ -148,10 +144,12 @@ $backupDir = Join-Path $tempWorkPath 'ConfigOld'
|
||||
|
||||
# Temporarily move existing config files if they exist to prevent them from being overwritten by the new script files, will be moved back after the new script is unpacked
|
||||
if (Test-Path "$configDir") {
|
||||
Write-Output ""
|
||||
Write-Output "> Backing up existing config files..."
|
||||
|
||||
New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null
|
||||
|
||||
$filesToKeep = @(
|
||||
'CustomAppsList',
|
||||
'LastUsedSettings.json'
|
||||
)
|
||||
|
||||
@@ -178,6 +176,9 @@ if (Test-Path "$backupDir") {
|
||||
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"
|
||||
Remove-Item "$backupDir" -Recurse -Force
|
||||
}
|
||||
@@ -218,13 +219,13 @@ if ($null -ne $debloatProcess) {
|
||||
$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) {
|
||||
Write-Output ""
|
||||
Write-Output "> Cleaning up..."
|
||||
|
||||
# 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 ""
|
||||
|
||||
@@ -7,7 +7,6 @@ param (
|
||||
[string]$User,
|
||||
[switch]$NoRestartExplorer,
|
||||
[switch]$CreateRestorePoint,
|
||||
[switch]$RunAppsListGenerator,
|
||||
[switch]$RunDefaults,
|
||||
[switch]$RunDefaultsLite,
|
||||
[switch]$RunSavedSettings,
|
||||
@@ -15,11 +14,8 @@ param (
|
||||
[string]$Apps,
|
||||
[string]$AppRemovalTarget,
|
||||
[switch]$RemoveApps,
|
||||
[switch]$RemoveAppsCustom,
|
||||
[switch]$RemoveGamingApps,
|
||||
[switch]$RemoveCommApps,
|
||||
[switch]$RemoveHPApps,
|
||||
[switch]$RemoveW11Outlook,
|
||||
[switch]$ForceRemoveEdge,
|
||||
[switch]$DisableDVR,
|
||||
[switch]$DisableGameBarIntegration,
|
||||
@@ -58,7 +54,7 @@ param (
|
||||
[switch]$HideSearchTb, [switch]$ShowSearchIconTb, [switch]$ShowSearchLabelTb, [switch]$ShowSearchBoxTb,
|
||||
[switch]$HideTaskview,
|
||||
[switch]$DisableStartRecommended,
|
||||
[switch]$DisableStartAllApps,
|
||||
[switch]$DisableStartAllApps, [switch]$StartAllAppsCategory, [switch]$StartAllAppsGrid, [switch]$StartAllAppsList,
|
||||
[switch]$DisableStartPhoneLink,
|
||||
[switch]$DisableCopilot,
|
||||
[switch]$DisableRecall,
|
||||
@@ -136,12 +132,12 @@ catch {
|
||||
Exit
|
||||
}
|
||||
|
||||
Write-Output ""
|
||||
Write-Output "> Cleaning up old Win11Debloat folder..."
|
||||
|
||||
# Remove old script folder if it exists, but keep config and log files
|
||||
# Remove old script folder if it exists, but keep configs, logs and backups
|
||||
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'
|
||||
@@ -149,10 +145,12 @@ $backupDir = Join-Path $tempWorkPath 'ConfigOld'
|
||||
|
||||
# Temporarily move existing config files if they exist to prevent them from being overwritten by the new script files, will be moved back after the new script is unpacked
|
||||
if (Test-Path "$configDir") {
|
||||
Write-Output ""
|
||||
Write-Output "> Backing up existing config files..."
|
||||
|
||||
New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null
|
||||
|
||||
$filesToKeep = @(
|
||||
'CustomAppsList',
|
||||
'LastUsedSettings.json'
|
||||
)
|
||||
|
||||
@@ -171,7 +169,7 @@ Expand-Archive $tempArchivePath $tempWorkPath
|
||||
Remove-Item $tempArchivePath
|
||||
|
||||
# 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
|
||||
if (Test-Path "$backupDir") {
|
||||
@@ -179,6 +177,9 @@ if (Test-Path "$backupDir") {
|
||||
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"
|
||||
Remove-Item "$backupDir" -Recurse -Force
|
||||
}
|
||||
@@ -219,13 +220,13 @@ if ($null -ne $debloatProcess) {
|
||||
$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) {
|
||||
Write-Output ""
|
||||
Write-Output "> Cleaning up..."
|
||||
|
||||
# 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 ""
|
||||
|
||||
@@ -203,6 +203,11 @@ function Invoke-RegistryOperationsFromRegFile {
|
||||
$operations = @(Get-RegFileOperations -regFilePath $RegFilePath)
|
||||
$totalOperations = $operations.Count
|
||||
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Write-Host "[WhatIf] Apply $totalOperations registry changes from '$RegFilePath'" -ForegroundColor Cyan
|
||||
return
|
||||
}
|
||||
|
||||
foreach ($operation in $operations) {
|
||||
try {
|
||||
Invoke-RegistryOperation -Operation $operation -RegFilePath $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 -ne 'Yes') {
|
||||
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 -ne 'Yes') {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
return $true
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
# Operation type constants, used to indicate the type of operation for each registry entry
|
||||
$script:OpType_RemoveKey = 'DeleteKey'
|
||||
$script:OpType_RemoveValue = 'DeleteValue'
|
||||
$script:OpType_Store = 'SetValue'
|
||||
|
||||
function Get-RegFileOperations {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -26,6 +31,7 @@ function Get-RegFileOperations {
|
||||
$operations = @()
|
||||
$currentKeyPath = $null
|
||||
$isDeletedKey = $false
|
||||
$opRef = $script:OpType_RemoveKey
|
||||
|
||||
foreach ($rawLine in $lines) {
|
||||
$line = $rawLine.Trim()
|
||||
@@ -43,7 +49,7 @@ function Get-RegFileOperations {
|
||||
|
||||
if ($isDeletedKey) {
|
||||
$operations += [PSCustomObject]@{
|
||||
OperationType = 'DeleteKey'
|
||||
OperationType = $opRef
|
||||
KeyPath = $currentKeyPath
|
||||
}
|
||||
}
|
||||
@@ -87,10 +93,12 @@ function Convert-RegValueData {
|
||||
[Parameter(Mandatory)]
|
||||
[string]$valueData
|
||||
)
|
||||
$opStore = $script:OpType_Store
|
||||
$opRemove = $script:OpType_RemoveValue
|
||||
|
||||
if ($valueData -eq '-') {
|
||||
return [PSCustomObject]@{
|
||||
OperationType = 'DeleteValue'
|
||||
OperationType = $opRemove
|
||||
ValueType = $null
|
||||
ValueData = $null
|
||||
}
|
||||
@@ -98,7 +106,7 @@ function Convert-RegValueData {
|
||||
|
||||
if ($valueData -match '^dword:(?<value>[0-9a-fA-F]{1,8})$') {
|
||||
return [PSCustomObject]@{
|
||||
OperationType = 'SetValue'
|
||||
OperationType = $opStore
|
||||
ValueType = 'DWord'
|
||||
ValueData = [uint32]::Parse($matches.value, [System.Globalization.NumberStyles]::HexNumber)
|
||||
}
|
||||
@@ -106,7 +114,7 @@ function Convert-RegValueData {
|
||||
|
||||
if ($valueData -match '^qword:(?<value>[0-9a-fA-F]{1,16})$') {
|
||||
return [PSCustomObject]@{
|
||||
OperationType = 'SetValue'
|
||||
OperationType = $opStore
|
||||
ValueType = 'QWord'
|
||||
ValueData = [uint64]::Parse($matches.value, [System.Globalization.NumberStyles]::HexNumber)
|
||||
}
|
||||
@@ -122,17 +130,20 @@ function Convert-RegValueData {
|
||||
}
|
||||
|
||||
return [PSCustomObject]@{
|
||||
OperationType = 'SetValue'
|
||||
OperationType = $opStore
|
||||
ValueType = $valueType
|
||||
ValueData = $value
|
||||
}
|
||||
}
|
||||
|
||||
if ($valueData -match '^"(?<value>.*)"$') {
|
||||
$stringValue = $matches.value
|
||||
# Unescape registry string escape sequences
|
||||
$stringValue = $stringValue -replace '\\"', '"' -replace '\\\\', '\'
|
||||
return [PSCustomObject]@{
|
||||
OperationType = 'SetValue'
|
||||
OperationType = $opStore
|
||||
ValueType = 'String'
|
||||
ValueData = $matches.value
|
||||
ValueData = $stringValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,13 +157,9 @@ function Convert-HexStringToByteArray {
|
||||
)
|
||||
|
||||
$parts = $hexValue.Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
||||
$bytes = New-Object byte[] $parts.Count
|
||||
|
||||
for ($i = 0; $i -lt $parts.Count; $i++) {
|
||||
$bytes[$i] = [byte]::Parse($parts[$i], [System.Globalization.NumberStyles]::HexNumber)
|
||||
}
|
||||
|
||||
return $bytes
|
||||
return [System.Linq.Enumerable]::Select($parts, [Func[object, byte]] {
|
||||
param($h) [System.Convert]::ToByte($h, 16)
|
||||
}) -as [byte[]]
|
||||
}
|
||||
|
||||
function Convert-RegistryByteArrayToString {
|
||||
|
||||
@@ -27,16 +27,19 @@ function Split-RegistryPath {
|
||||
$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>.*))?$') {
|
||||
$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 { '' }
|
||||
$targetMountName = [string]$script:RegistryTargetHiveMountName
|
||||
if ([string]::IsNullOrWhiteSpace($remainingSubKey)) {
|
||||
$normalizedSubKey = 'Default'
|
||||
$normalizedSubKey = $targetMountName
|
||||
}
|
||||
else {
|
||||
$normalizedSubKey = "Default\$remainingSubKey"
|
||||
$normalizedSubKey = "$targetMountName\$remainingSubKey"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,14 +70,15 @@ function Get-RegistryRootKey {
|
||||
function Get-RegistryFilePathForFeature {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
$Feature,
|
||||
[string]$RegistryKey,
|
||||
[switch]$UseSysprepRegFiles
|
||||
)
|
||||
|
||||
$useSysprepLayout = $UseSysprepRegFiles -or $script:Params.ContainsKey('Sysprep') -or $script:Params.ContainsKey('User')
|
||||
if ($useSysprepLayout) {
|
||||
return Join-Path (Join-Path $script:RegfilesPath 'Sysprep') $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]@{
|
||||
IsValid = $true
|
||||
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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,15 @@ function Invoke-NonBlocking {
|
||||
|
||||
$result = $ps.EndInvoke($handle)
|
||||
|
||||
# Surface non-terminating errors raised inside the runspace so GUI-mode operations
|
||||
# (e.g. failed app removals) don't fail silently - the runspace keeps its own error
|
||||
# stream that is otherwise discarded on Dispose.
|
||||
if ($ps.HadErrors) {
|
||||
foreach ($runspaceError in $ps.Streams.Error) {
|
||||
Write-Error -ErrorRecord $runspaceError
|
||||
}
|
||||
}
|
||||
|
||||
if ($result.Count -eq 0) { return $null }
|
||||
if ($result.Count -eq 1) { return $result[0] }
|
||||
return @($result)
|
||||
|
||||
@@ -7,7 +7,6 @@ param (
|
||||
[string]$User,
|
||||
[switch]$NoRestartExplorer,
|
||||
[switch]$CreateRestorePoint,
|
||||
[switch]$RunAppsListGenerator,
|
||||
[switch]$RunDefaults,
|
||||
[switch]$RunDefaultsLite,
|
||||
[switch]$RunSavedSettings,
|
||||
@@ -15,11 +14,8 @@ param (
|
||||
[string]$Apps,
|
||||
[string]$AppRemovalTarget,
|
||||
[switch]$RemoveApps,
|
||||
[switch]$RemoveAppsCustom,
|
||||
[switch]$RemoveGamingApps,
|
||||
[switch]$RemoveCommApps,
|
||||
[switch]$RemoveHPApps,
|
||||
[switch]$RemoveW11Outlook,
|
||||
[switch]$ForceRemoveEdge,
|
||||
[switch]$DisableDVR,
|
||||
[switch]$DisableGameBarIntegration,
|
||||
@@ -59,7 +55,7 @@ param (
|
||||
[switch]$HideSearchTb, [switch]$ShowSearchIconTb, [switch]$ShowSearchLabelTb, [switch]$ShowSearchBoxTb,
|
||||
[switch]$HideTaskview,
|
||||
[switch]$DisableStartRecommended,
|
||||
[switch]$DisableStartAllApps,
|
||||
[switch]$DisableStartAllApps, [switch]$StartAllAppsCategory, [switch]$StartAllAppsGrid, [switch]$StartAllAppsList,
|
||||
[switch]$DisableStartPhoneLink,
|
||||
[switch]$DisableCopilot,
|
||||
[switch]$DisableRecall,
|
||||
@@ -141,7 +137,7 @@ if (-not $isAdmin) {
|
||||
}
|
||||
|
||||
# Define script-level variables & paths
|
||||
$script:Version = "2026.05.20"
|
||||
$script:Version = "2026.06.24"
|
||||
$configPath = Join-Path $PSScriptRoot 'Config'
|
||||
$logsPath = Join-Path $PSScriptRoot 'Logs'
|
||||
$schemasPath = Join-Path $PSScriptRoot 'Schemas'
|
||||
@@ -151,14 +147,13 @@ $script:AppsListFilePath = Join-Path $configPath 'Apps.json'
|
||||
$script:DefaultSettingsFilePath = Join-Path $configPath 'DefaultSettings.json'
|
||||
$script:FeaturesFilePath = Join-Path $configPath 'Features.json'
|
||||
$script:SavedSettingsFilePath = Join-Path $configPath 'LastUsedSettings.json'
|
||||
$script:CustomAppsListFilePath = Join-Path $configPath 'CustomAppsList'
|
||||
$script:DefaultLogPath = Join-Path $logsPath 'Win11Debloat.log'
|
||||
$script:RegfilesPath = Join-Path $PSScriptRoot 'Regfiles'
|
||||
$script:RegistryBackupsPath = Join-Path $PSScriptRoot 'Backups'
|
||||
$script:AssetsPath = Join-Path $PSScriptRoot 'Assets'
|
||||
$script:AppSelectionSchema = Join-Path $schemasPath 'AppSelectionWindow.xaml'
|
||||
$script:MainWindowSchema = Join-Path $schemasPath 'MainWindow.xaml'
|
||||
$script:MessageBoxSchema = Join-Path $schemasPath 'MessageBoxWindow.xaml'
|
||||
$script:MessageBoxSchema = Join-Path $schemasPath 'MessageBox.xaml'
|
||||
$script:AboutWindowSchema = Join-Path $schemasPath 'AboutWindow.xaml'
|
||||
$script:ApplyChangesWindowSchema = Join-Path $schemasPath 'ApplyChangesWindow.xaml'
|
||||
$script:SharedStylesSchema = Join-Path $schemasPath 'SharedStyles.xaml'
|
||||
@@ -166,8 +161,9 @@ $script:BubbleHintSchema = Join-Path $schemasPath 'BubbleHint.xaml'
|
||||
$script:ImportExportConfigSchema = Join-Path $schemasPath 'ImportExportConfigWindow.xaml'
|
||||
$script:RestoreBackupWindowSchema = Join-Path $schemasPath 'RestoreBackupWindow.xaml'
|
||||
$script:LoadAppsDetailsScriptPath = Join-Path (Join-Path $scriptsPath 'FileIO') 'LoadAppsDetailsFromJson.ps1'
|
||||
$script:TestAppInWingetListScriptPath = Join-Path (Join-Path $scriptsPath 'AppRemoval') 'Test-AppInWingetList.ps1'
|
||||
|
||||
$script:ControlParams = 'WhatIf', 'Confirm', 'Verbose', 'Debug', 'LogPath', 'Silent', 'Sysprep', 'User', 'NoRestartExplorer', 'RunDefaults', 'RunDefaultsLite', 'RunSavedSettings', 'Config', 'RunAppsListGenerator', 'CLI', 'AppRemovalTarget'
|
||||
$script:ControlParams = 'WhatIf', 'Confirm', 'Verbose', 'Debug', 'LogPath', 'Silent', 'Sysprep', 'User', 'NoRestartExplorer', 'RunDefaults', 'RunDefaultsLite', 'RunSavedSettings', 'Config', 'CLI', 'AppRemovalTarget'
|
||||
|
||||
# Script-level variables for GUI elements
|
||||
$script:GuiWindow = $null
|
||||
@@ -211,7 +207,8 @@ Write-Host " " -NoNewline; Write-Host " (" -ForegroundColor
|
||||
Write-Host " " -NoNewline; Write-Host " ( " -ForegroundColor DarkYellow -NoNewline; Write-Host "'" -ForegroundColor Red -NoNewline; Write-Host " ) " -ForegroundColor DarkYellow -NoNewline; Write-Host "*" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host " Win11Debloat is launching..." -ForegroundColor White
|
||||
Write-Host " Leave this window open" -ForegroundColor DarkGray
|
||||
Write-Host " Keep this window open" -ForegroundColor DarkGray
|
||||
Write-Host ""
|
||||
Write-Host ""
|
||||
|
||||
# Log script output to 'Win11Debloat.log' at the specified path
|
||||
@@ -222,6 +219,15 @@ else {
|
||||
Start-Transcript -Path $script:DefaultLogPath -Append -IncludeInvocationHeader -Force | Out-Null
|
||||
}
|
||||
|
||||
# Check if the device is domain-joined and warn the user (Group Policy may override changes)
|
||||
try {
|
||||
$computerSystem = Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue
|
||||
if ($null -ne $computerSystem -and $computerSystem.PartOfDomain) {
|
||||
Write-Warning "This machine is domain-joined. Group Policy may override changes made by Win11Debloat."
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
# Check if script has all required files
|
||||
if (-not ((Test-Path $script:DefaultSettingsFilePath) -and (Test-Path $script:AppsListFilePath) -and (Test-Path $script:RegfilesPath) -and (Test-Path $script:AssetsPath) -and (Test-Path $script:AppSelectionSchema) -and (Test-Path $script:ApplyChangesWindowSchema) -and (Test-Path $script:SharedStylesSchema) -and (Test-Path $script:BubbleHintSchema) -and (Test-Path $script:RestoreBackupWindowSchema) -and (Test-Path $script:FeaturesFilePath))) {
|
||||
Write-Error "Win11Debloat is unable to find required files, please ensure all script files are present"
|
||||
@@ -236,6 +242,10 @@ $script:Features = @{}
|
||||
try {
|
||||
$featuresData = Get-Content -Path $script:FeaturesFilePath -Raw | ConvertFrom-Json
|
||||
foreach ($feature in $featuresData.Features) {
|
||||
if ([string]::IsNullOrWhiteSpace([string]$feature.FeatureId) -or [string]::IsNullOrWhiteSpace([string]$feature.Label) -or [string]::IsNullOrWhiteSpace([string]$feature.ApplyText)) {
|
||||
Write-Warning "Feature '$($feature.FeatureId)' is missing a FeatureId, Label, or ApplyText in Features.json and will be skipped."
|
||||
continue
|
||||
}
|
||||
$script:Features[$feature.FeatureId] = $feature
|
||||
}
|
||||
}
|
||||
@@ -281,6 +291,7 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
||||
. "$PSScriptRoot/Scripts/AppRemoval/ForceRemoveEdge.ps1"
|
||||
. "$PSScriptRoot/Scripts/AppRemoval/RemoveApps.ps1"
|
||||
. "$PSScriptRoot/Scripts/AppRemoval/GetInstalledAppsViaWinget.ps1"
|
||||
. "$PSScriptRoot/Scripts/AppRemoval/Test-AppInWingetList.ps1"
|
||||
|
||||
# CLI functions
|
||||
. "$PSScriptRoot/Scripts/CLI/AwaitKeyToExit.ps1"
|
||||
@@ -293,7 +304,8 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
||||
. "$PSScriptRoot/Scripts/CLI/PrintHeader.ps1"
|
||||
|
||||
# Features functions
|
||||
. "$PSScriptRoot/Scripts/Features/ExecuteChanges.ps1"
|
||||
. "$PSScriptRoot/Scripts/Features/GetCurrentTweakState.ps1"
|
||||
. "$PSScriptRoot/Scripts/Features/InvokeChanges.ps1"
|
||||
. "$PSScriptRoot/Scripts/Features/CreateSystemRestorePoint.ps1"
|
||||
. "$PSScriptRoot/Scripts/Features/BackupRegistryFeatureSelection.ps1"
|
||||
. "$PSScriptRoot/Scripts/Features/BackupRegistrySnapshotCapture.ps1"
|
||||
@@ -301,8 +313,9 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
||||
. "$PSScriptRoot/Scripts/Features/RegistryBackupValidation.ps1"
|
||||
. "$PSScriptRoot/Scripts/Features/RestoreRegistryApplyState.ps1"
|
||||
. "$PSScriptRoot/Scripts/Features/RestoreRegistryBackup.ps1"
|
||||
. "$PSScriptRoot/Scripts/Features/DisableStoreSearchSuggestions.ps1"
|
||||
. "$PSScriptRoot/Scripts/Features/EnableWindowsFeature.ps1"
|
||||
. "$PSScriptRoot/Scripts/Features/StoreSearchSuggestions.ps1"
|
||||
. "$PSScriptRoot/Scripts/Features/TelemetryScheduledTasks.ps1"
|
||||
. "$PSScriptRoot/Scripts/Features/WindowsOptionalFeatures.ps1"
|
||||
. "$PSScriptRoot/Scripts/Features/ImportRegistryFile.ps1"
|
||||
. "$PSScriptRoot/Scripts/Features/ReplaceStartMenu.ps1"
|
||||
. "$PSScriptRoot/Scripts/Features/RestartExplorer.ps1"
|
||||
@@ -312,7 +325,6 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
||||
. "$PSScriptRoot/Scripts/FileIO/SaveToFile.ps1"
|
||||
. "$PSScriptRoot/Scripts/FileIO/SaveSettings.ps1"
|
||||
. "$PSScriptRoot/Scripts/FileIO/LoadSettings.ps1"
|
||||
. "$PSScriptRoot/Scripts/FileIO/SaveCustomAppsListToFile.ps1"
|
||||
. "$PSScriptRoot/Scripts/FileIO/ValidateAppslist.ps1"
|
||||
. "$PSScriptRoot/Scripts/FileIO/LoadAppsFromFile.ps1"
|
||||
. "$PSScriptRoot/Scripts/FileIO/LoadAppsDetailsFromJson.ps1"
|
||||
@@ -330,6 +342,11 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
||||
. "$PSScriptRoot/Scripts/GUI/Show-RestoreBackupWindow.ps1"
|
||||
. "$PSScriptRoot/Scripts/GUI/RestoreBackupDialogFeatureLists.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-AboutDialog.ps1"
|
||||
. "$PSScriptRoot/Scripts/GUI/Show-Bubble.ps1"
|
||||
@@ -337,6 +354,7 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
||||
# Helper functions
|
||||
. "$PSScriptRoot/Scripts/Helpers/AddParameter.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/ResolveUserProfilePath.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/UserHiveHelpers.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/CheckIfUserExists.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/CheckModernStandbySupport.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/GenerateAppsList.ps1"
|
||||
@@ -350,7 +368,7 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
||||
. "$PSScriptRoot/Scripts/Helpers/GetUserName.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/RegistryPathHelpers.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/ApplyRegistryRegFile.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/TestIfUserIsLoggedIn.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/ConfirmUnsafeAppRemoval.ps1"
|
||||
|
||||
# Threading functions
|
||||
. "$PSScriptRoot/Scripts/Threading/DoEvents.ps1"
|
||||
@@ -373,6 +391,7 @@ $WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\Current
|
||||
$script:ModernStandbySupported = CheckModernStandbySupport
|
||||
|
||||
$script:Params = $PSBoundParameters
|
||||
$script:UndoParams = @{}
|
||||
|
||||
# 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")) {
|
||||
@@ -416,7 +435,11 @@ if ($script:Params.ContainsKey("User")) {
|
||||
GetUserDirectory -userName $script:Params.Item("User") | Out-Null
|
||||
}
|
||||
if ($script:Params.ContainsKey("AppRemovalTarget")) {
|
||||
GetUserDirectory -userName $script:Params.Item("AppRemovalTarget") | Out-Null
|
||||
$appRemovalTargetValue = $script:Params.Item("AppRemovalTarget")
|
||||
# 'AllUsers' / 'CurrentUser' are sentinel scope values, not real usernames - don't resolve them as a profile
|
||||
if ($appRemovalTargetValue -notin @('AllUsers', 'CurrentUser')) {
|
||||
GetUserDirectory -userName $appRemovalTargetValue | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# Remove LastUsedSettings.json file if it exists and is empty
|
||||
@@ -427,24 +450,6 @@ if ((Test-Path $script:SavedSettingsFilePath) -and ([String]::IsNullOrWhiteSpace
|
||||
# Default to CLI mode for deployment-targeted parameters.
|
||||
$launchInCLI = $CLI -or $script:Params.ContainsKey("User") -or $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("AppRemovalTarget")
|
||||
|
||||
# Only run the app selection form if the 'RunAppsListGenerator' parameter was passed to the script
|
||||
if ($RunAppsListGenerator) {
|
||||
PrintHeader "Custom Apps List Generator"
|
||||
|
||||
$result = Show-AppSelectionWindow
|
||||
|
||||
# Show different message based on whether the app selection was saved or cancelled
|
||||
if ($result -ne $true) {
|
||||
Write-Host "Application selection window was closed without saving." -ForegroundColor Red
|
||||
}
|
||||
else {
|
||||
Write-Output "Your app selection was saved to the 'CustomAppsList' file, found at:"
|
||||
Write-Host "$PSScriptRoot" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
AwaitKeyToExit
|
||||
}
|
||||
|
||||
# Change script execution based on provided parameters or user input
|
||||
if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSavedSettings -or $Config -or ($controlParamsCount -eq $script:Params.Count)) {
|
||||
if ($RunDefaults -or $RunDefaultsLite) {
|
||||
@@ -482,7 +487,11 @@ if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSa
|
||||
try {
|
||||
$result = Show-MainWindow
|
||||
|
||||
Stop-Transcript
|
||||
try {
|
||||
Stop-Transcript
|
||||
}
|
||||
catch { }
|
||||
|
||||
Exit
|
||||
}
|
||||
catch {
|
||||
@@ -529,7 +538,7 @@ if (($controlParamsCount -eq $script:Params.Keys.Count) -or ($script:Params.Keys
|
||||
|
||||
# Execute all selected/provided parameters using the consolidated function
|
||||
# (This also handles restore point creation if requested)
|
||||
ExecuteAllChanges
|
||||
Invoke-AllChanges
|
||||
|
||||
RestartExplorer
|
||||
|
||||
|
||||
Reference in New Issue
Block a user