15 Commits

Author SHA1 Message Date
Raphire
cbd61902ed Set default launch mode to CLI for deployment-targeted parameters 2026-04-02 00:03:26 +02:00
Raphire
54edf224a3 Improve Window controls 2026-04-02 00:00:32 +02:00
Natan Heringer
3eade4ddba Add drive letter position and visibility options (#527) (#533) 2026-03-29 00:25:12 +01:00
Raphire
487b1fbee9 Prevent Window Snapping 2026-03-27 21:14:22 +01:00
Jeffrey
774c8ecd92 Add ability to export/import settings configuration (#522) 2026-03-27 20:33:24 +01:00
Jeffrey
e05af92acc Add support for multiple AppIds for app removal (#526) 2026-03-23 22:59:04 +01:00
Raphire
edd815fdbb Add shadow to bubble hint 2026-03-18 23:32:10 +01:00
Raphire
17ee530962 Refactor ImportRegistryFile function to improve error handling and streamline registry file validation 2026-03-18 22:49:44 +01:00
Raphire
999e442658 Simplify user validation messages and add user logged-in check function 2026-03-18 22:39:49 +01:00
Raphire
1d4cf4a801 Fix: Skip confirmation when running last used settings with -Silent parameter #521 2026-03-18 19:28:47 +01:00
Raphire
7a3431e56b Close app preset menu when main window loses focus 2026-03-17 21:28:35 +01:00
Raphire
1b41f05743 Fix inconsistent search highlighting after sorting 2026-03-16 19:25:53 +01:00
Raphire
c6535803ec Bump version 2026-03-15 22:59:37 +01:00
Jeffrey
c37bdcf5f2 Improve app page with sorting, recommendations and more (#520) 2026-03-15 22:58:06 +01:00
Jeffrey
d187679cd0 Add bubble hint to guide users to review the selected changes after clicking Default Mode button (#519) 2026-03-15 20:16:53 +01:00
55 changed files with 2993 additions and 1231 deletions

View File

@@ -77,7 +77,7 @@ Win11Debloat/
2. **Document Changes**: Update the `README.md` and other relevant documentation. Wiki documentation will be generated/updated based on the `Features.json` and `Apps.json` files. 2. **Document Changes**: Update the `README.md` and other relevant documentation. Wiki documentation will be generated/updated based on the `Features.json` and `Apps.json` files.
3. **Follow Existing Patterns**: Look at existing implementations for guidance. 3. **Follow Existing Patterns**: Look at existing implementations for guidance.
4. **Use Clear Naming**: Choose descriptive names for features, IDs, and registry files. 4. **Use Clear Naming**: Choose descriptive names for features, IDs, and registry files.
5. **Minimal Changes**: Registry files should only modify what's necessary. 5. **Minimal Changes**: Registry files should only modify what's necessary. Avoid using policies where possible.
6. **Comment Your Code**: Add comments explaining your reasoning for complex logic in PowerShell scripts. 6. **Comment Your Code**: Add comments explaining your reasoning for complex logic in PowerShell scripts.
7. **Version Constraints**: Use `MinVersion` and `MaxVersion` if a feature only applies to specific Windows versions. 7. **Version Constraints**: Use `MinVersion` and `MaxVersion` if a feature only applies to specific Windows versions.
8. **Limit pull requests to 1 feature**: Keep pull requests limited to just one feature, this makes it easier to review your changes. 8. **Limit pull requests to 1 feature**: Keep pull requests limited to just one feature, this makes it easier to review your changes.

File diff suppressed because it is too large Load Diff

View File

@@ -187,6 +187,7 @@
"Label": "Open File Explorer to", "Label": "Open File Explorer to",
"ToolTip": "This setting allows you to choose the default location that File Explorer opens to.", "ToolTip": "This setting allows you to choose the default location that File Explorer opens to.",
"Category": "File Explorer", "Category": "File Explorer",
"Priority": 1,
"Values": [ "Values": [
{ {
"Label": "Home", "Label": "Home",
@@ -214,6 +215,39 @@
} }
] ]
}, },
{
"GroupId": "DriveLetterPosition",
"Label": "Drive letter position",
"ToolTip": "This setting allows you to choose where drive letters are shown in File Explorer.",
"Category": "File Explorer",
"Priority": 20,
"Values": [
{
"Label": "Show drive letters after drive label (Default)",
"FeatureIds": [
"ShowDriveLettersLast"
]
},
{
"Label": "Show drive letters before drive label",
"FeatureIds": [
"ShowDriveLettersFirst"
]
},
{
"Label": "Show network drive letters before drive label",
"FeatureIds": [
"ShowNetworkDriveLettersFirst"
]
},
{
"Label": "Hide all drive letters",
"FeatureIds": [
"HideDriveLetters"
]
}
]
},
{ {
"GroupId": "ShowTabsInAltTab", "GroupId": "ShowTabsInAltTab",
"Label": "Show tabs from apps when snapping or pressing Alt+Tab", "Label": "Show tabs from apps when snapping or pressing Alt+Tab",
@@ -1220,6 +1254,7 @@
"Label": "file extensions for known file types", "Label": "file extensions for known file types",
"ToolTip": "This will show file extensions for known file types. By default, Windows hides file extensions for known file types which can lead to confusion and security risks.", "ToolTip": "This will show file extensions for known file types. By default, Windows hides file extensions for known file types which can lead to confusion and security risks.",
"Category": "File Explorer", "Category": "File Explorer",
"Priority": 2,
"Action": "Show", "Action": "Show",
"RegistryKey": "Show_Extensions_For_Known_File_Types.reg", "RegistryKey": "Show_Extensions_For_Known_File_Types.reg",
"ApplyText": "Enabling file extensions for known file types...", "ApplyText": "Enabling file extensions for known file types...",
@@ -1233,6 +1268,7 @@
"Label": "hidden files, folders and drives", "Label": "hidden files, folders and drives",
"ToolTip": "By default, Windows hides certain files, folders and drives to prevent accidental modification or deletion. Turn this on to show all files in File Explorer.", "ToolTip": "By default, Windows hides certain files, folders and drives to prevent accidental modification or deletion. Turn this on to show all files in File Explorer.",
"Category": "File Explorer", "Category": "File Explorer",
"Priority": 3,
"Action": "Show", "Action": "Show",
"RegistryKey": "Show_Hidden_Folders.reg", "RegistryKey": "Show_Hidden_Folders.reg",
"ApplyText": "Unhiding hidden files, folders and drives...", "ApplyText": "Unhiding hidden files, folders and drives...",
@@ -1246,6 +1282,7 @@
"Label": "'Home' from navigation pane", "Label": "'Home' from navigation pane",
"ToolTip": "Hides the 'Home' section from the File Explorer navigation pane.", "ToolTip": "Hides the 'Home' section from the File Explorer navigation pane.",
"Category": "File Explorer", "Category": "File Explorer",
"Priority": 4,
"Action": "Hide", "Action": "Hide",
"RegistryKey": "Hide_Home_from_Explorer.reg", "RegistryKey": "Hide_Home_from_Explorer.reg",
"ApplyText": "Hiding the home section from the File Explorer navigation pane...", "ApplyText": "Hiding the home section from the File Explorer navigation pane...",
@@ -1259,6 +1296,7 @@
"Label": "'Gallery' from navigation pane", "Label": "'Gallery' from navigation pane",
"ToolTip": "Hides the 'Gallery' section from the File Explorer navigation pane.", "ToolTip": "Hides the 'Gallery' section from the File Explorer navigation pane.",
"Category": "File Explorer", "Category": "File Explorer",
"Priority": 5,
"Action": "Hide", "Action": "Hide",
"RegistryKey": "Hide_Gallery_from_Explorer.reg", "RegistryKey": "Hide_Gallery_from_Explorer.reg",
"ApplyText": "Hiding the gallery section from the File Explorer navigation pane...", "ApplyText": "Hiding the gallery section from the File Explorer navigation pane...",
@@ -1272,6 +1310,7 @@
"Label": "duplicate removable drive entries", "Label": "duplicate removable drive entries",
"ToolTip": "By default, Windows shows removable drives both under 'This PC' and in the navigation pane with its own entry. Enable this setting to only show removable drives under 'This PC'.", "ToolTip": "By default, Windows shows removable drives both under 'This PC' and in the navigation pane with its own entry. Enable this setting to only show removable drives under 'This PC'.",
"Category": "File Explorer", "Category": "File Explorer",
"Priority": 6,
"Action": "Hide", "Action": "Hide",
"RegistryKey": "Hide_duplicate_removable_drives_from_navigation_pane_of_File_Explorer.reg", "RegistryKey": "Hide_duplicate_removable_drives_from_navigation_pane_of_File_Explorer.reg",
"ApplyText": "Hiding duplicate removable drive entries from the File Explorer navigation pane...", "ApplyText": "Hiding duplicate removable drive entries from the File Explorer navigation pane...",
@@ -1285,6 +1324,7 @@
"Label": "common folders back to 'This PC' page", "Label": "common folders back to 'This PC' page",
"ToolTip": "This setting will add common folders like Desktop, Documents, Downloads, Music, Pictures and Videos back to the 'This PC' page in File Explorer.", "ToolTip": "This setting will add common folders like Desktop, Documents, Downloads, Music, Pictures and Videos back to the 'This PC' page in File Explorer.",
"Category": "File Explorer", "Category": "File Explorer",
"Priority": 7,
"Action": "Add", "Action": "Add",
"RegistryKey": "Add_All_Folders_Under_This_PC.reg", "RegistryKey": "Add_All_Folders_Under_This_PC.reg",
"ApplyText": "Adding all common folders (Desktop, Downloads, etc.) back to 'This PC' in File Explorer...", "ApplyText": "Adding all common folders (Desktop, Downloads, etc.) back to 'This PC' in File Explorer...",
@@ -1376,6 +1416,7 @@
"Label": "'Include in library' option in the context menu", "Label": "'Include in library' option in the context menu",
"ToolTip": "Hides the 'Include in library' option from the File Explorer context menu.", "ToolTip": "Hides the 'Include in library' option from the File Explorer context menu.",
"Category": "File Explorer", "Category": "File Explorer",
"Priority": 8,
"Action": "Hide", "Action": "Hide",
"RegistryKey": "Disable_Include_in_library_from_context_menu.reg", "RegistryKey": "Disable_Include_in_library_from_context_menu.reg",
"ApplyText": "Hiding 'Include in library' in the context menu...", "ApplyText": "Hiding 'Include in library' in the context menu...",
@@ -1389,6 +1430,7 @@
"Label": "'Give access to' option in the context menu", "Label": "'Give access to' option in the context menu",
"ToolTip": "Hides the 'Give access to' option from the File Explorer context menu.", "ToolTip": "Hides the 'Give access to' option from the File Explorer context menu.",
"Category": "File Explorer", "Category": "File Explorer",
"Priority": 9,
"Action": "Hide", "Action": "Hide",
"RegistryKey": "Disable_Give_access_to_context_menu.reg", "RegistryKey": "Disable_Give_access_to_context_menu.reg",
"ApplyText": "Hiding 'Give access to' in the context menu...", "ApplyText": "Hiding 'Give access to' in the context menu...",
@@ -1402,6 +1444,7 @@
"Label": "'Share' option in the context menu", "Label": "'Share' option in the context menu",
"ToolTip": "Hides the 'Share' option from the File Explorer context menu.", "ToolTip": "Hides the 'Share' option from the File Explorer context menu.",
"Category": "File Explorer", "Category": "File Explorer",
"Priority": 10,
"Action": "Hide", "Action": "Hide",
"RegistryKey": "Disable_Share_from_context_menu.reg", "RegistryKey": "Disable_Share_from_context_menu.reg",
"ApplyText": "Hiding 'Share' in the context menu...", "ApplyText": "Hiding 'Share' in the context menu...",
@@ -1415,6 +1458,7 @@
"Label": "'OneDrive' folder from navigation pane", "Label": "'OneDrive' folder from navigation pane",
"ToolTip": "Hides the 'OneDrive' folder from the File Explorer navigation pane.", "ToolTip": "Hides the 'OneDrive' folder from the File Explorer navigation pane.",
"Category": "File Explorer", "Category": "File Explorer",
"Priority": 11,
"Action": "Hide", "Action": "Hide",
"RegistryKey": "Hide_Onedrive_Folder.reg", "RegistryKey": "Hide_Onedrive_Folder.reg",
"ApplyText": "Hiding the OneDrive folder from the File Explorer navigation pane...", "ApplyText": "Hiding the OneDrive folder from the File Explorer navigation pane...",
@@ -1428,6 +1472,7 @@
"Label": "'3D objects' folder under 'This PC'", "Label": "'3D objects' folder under 'This PC'",
"ToolTip": "Hides the '3D objects' folder from the File Explorer navigation pane.", "ToolTip": "Hides the '3D objects' folder from the File Explorer navigation pane.",
"Category": "File Explorer", "Category": "File Explorer",
"Priority": 12,
"Action": "Hide", "Action": "Hide",
"RegistryKey": "Hide_3D_Objects_Folder.reg", "RegistryKey": "Hide_3D_Objects_Folder.reg",
"ApplyText": "Hiding the 3D objects folder from the File Explorer navigation pane...", "ApplyText": "Hiding the 3D objects folder from the File Explorer navigation pane...",
@@ -1441,6 +1486,7 @@
"Label": "'Music' folder under 'This PC'", "Label": "'Music' folder under 'This PC'",
"ToolTip": "Hides the 'Music' folder from the File Explorer navigation pane.", "ToolTip": "Hides the 'Music' folder from the File Explorer navigation pane.",
"Category": "File Explorer", "Category": "File Explorer",
"Priority": 13,
"Action": "Hide", "Action": "Hide",
"RegistryKey": "Hide_Music_Folder.reg", "RegistryKey": "Hide_Music_Folder.reg",
"ApplyText": "Hiding the music folder from the File Explorer navigation pane...", "ApplyText": "Hiding the music folder from the File Explorer navigation pane...",
@@ -1489,6 +1535,58 @@
"RequiresReboot": true, "RequiresReboot": true,
"MinVersion": 22000, "MinVersion": 22000,
"MaxVersion": null "MaxVersion": null
},
{
"FeatureId": "ShowDriveLettersFirst",
"Label": "show drive letters before drive label",
"ToolTip": "This setting will show drive letters before the drive label in File Explorer.",
"Category": "File Explorer",
"Action": "Show first",
"RegistryKey": "Show_Drive_Letters_First.reg",
"ApplyText": "Showing drive letters before drive label...",
"UndoAction": "Show last",
"RegistryUndoKey": "Undo/Show_Drive_Letters_Last.reg",
"MinVersion": null,
"MaxVersion": null
},
{
"FeatureId": "ShowDriveLettersLast",
"Label": "show drive letters after drive label",
"ToolTip": "This setting will show drive letters after the drive label in File Explorer (Default Windows behavior).",
"Category": "File Explorer",
"Action": "Show last",
"RegistryKey": "Show_Drive_Letters_Last.reg",
"ApplyText": "Showing drive letters after drive label...",
"UndoAction": null,
"RegistryUndoKey": null,
"MinVersion": null,
"MaxVersion": null
},
{
"FeatureId": "ShowNetworkDriveLettersFirst",
"Label": "show network drive letters before drive label",
"ToolTip": "This setting will show only network drive letters before the drive label in File Explorer.",
"Category": "File Explorer",
"Action": "Show network first",
"RegistryKey": "Show_Network_Drive_Letters_First.reg",
"ApplyText": "Showing network drive letters before drive label...",
"UndoAction": "Show last",
"RegistryUndoKey": "Undo/Show_Drive_Letters_Last.reg",
"MinVersion": null,
"MaxVersion": null
},
{
"FeatureId": "HideDriveLetters",
"Label": "hide all drive letters",
"ToolTip": "This setting will hide all drive letters from the File Explorer navigation pane and 'This PC'.",
"Category": "File Explorer",
"Action": "Hide",
"RegistryKey": "Hide_Drive_Letters.reg",
"ApplyText": "Hiding all drive letters...",
"UndoAction": "Show last",
"RegistryUndoKey": "Undo/Show_Drive_Letters_Last.reg",
"MinVersion": null,
"MaxVersion": null
} }
] ]
} }

View File

@@ -97,23 +97,23 @@ Below is an overview of the key features and functionality offered by Win11Deblo
#### AI Features #### AI Features
- Disable & remove Microsoft Copilot. - Disable & remove Microsoft Copilot.
- Disable Windows Recall. (W11 only) - Disable Windows Recall.
- Disable Click to Do, AI text & image analysis tool. (W11 only) - Disable Click to Do, AI text & image analysis tool.
- Prevent AI service (WSAIFabricSvc) from starting automatically. (W11 only) - Prevent AI service (WSAIFabricSvc) from starting automatically.
- Disable AI Features in Edge. (W11 only) - Disable AI Features in Edge.
- Disable AI Features in Paint. (W11 only) - Disable AI Features in Paint.
- Disable AI Features in Notepad. (W11 only) - Disable AI Features in Notepad.
#### System #### System
- Disable the Drag Tray for sharing & moving files. (W11 only) - Disable the Drag Tray for sharing & moving files.
- Restore the old Windows 10 style context menu. (W11 only) - Restore the old Windows 10 style context menu.
- Turn off Enhance Pointer Precision, also known as mouse acceleration. - Turn off Enhance Pointer Precision, also known as mouse acceleration.
- Disable the Sticky Keys keyboard shortcut. (W11 only) - Disable the Sticky Keys keyboard shortcut.
- Disable Storage Sense automatic disk cleanup. - Disable Storage Sense automatic disk cleanup.
- Disable fast start-up to ensure a full shutdown. - Disable fast start-up to ensure a full shutdown.
- Disable BitLocker automatic device encryption. - Disable BitLocker automatic device encryption.
- Disable network connectivity during Modern Standby to reduce battery drain. (W11 only) - Disable network connectivity during Modern Standby to reduce battery drain.
#### Windows Update #### Windows Update
@@ -129,49 +129,50 @@ Below is an overview of the key features and functionality offered by Win11Deblo
#### Start Menu & Search #### Start Menu & Search
- Remove or replace all pinned apps from the start menu. (W11 only) - Remove or replace all pinned apps from the start menu.
- Hide the recommended section in the start menu. (W11 only) - Hide the recommended section in the start menu.
- Hide the 'All Apps' section in the start menu. (W11 only) - Hide the 'All Apps' section in the start menu.
- Disable the Phone Link mobile devices integration in the start menu. (W11 only) - Disable the Phone Link mobile devices integration in the start menu.
- Disable Bing web search & Copilot integration in Windows search. - Disable Bing web search & Copilot integration in Windows search.
- Disable Microsoft Store app suggestions in Windows search. (W11 only) - Disable Microsoft Store app suggestions in Windows search.
- Disable Search Highlights (dynamic/branded content) in the taskbar search box. (W11 only) - Disable Search Highlights (dynamic/branded content) in the taskbar search box.
- Disable local Windows search history. - Disable local Windows search history.
#### Taskbar #### Taskbar
- Align taskbar icons to the left. (W11 only) - Align taskbar icons to the left.
- Hide or change the search icon/box on the taskbar. (W11 only) - Hide or change the search icon/box on the taskbar.
- Hide the taskview button from the taskbar. (W11 only) - Hide the taskview button from the taskbar.
- Disable widgets on the taskbar & lock screen. - Disable widgets on the taskbar & lock screen.
- Hide the chat (meet now) icon from the taskbar. (W10 only) - Hide the chat (meet now) icon from the taskbar.
- Enable the 'End Task' option in the taskbar right click menu. (W11 only) - Enable the 'End Task' option in the taskbar right click menu.
- Enable the 'Last Active Click' behavior in the taskbar app area. This allows you to repeatedly click on an application's icon in the taskbar to switch focus between the open windows of that application. - Enable the 'Last Active Click' behavior in the taskbar app area. This allows you to repeatedly click on an application's icon in the taskbar to switch focus between the open windows of that application.
- Choose how app icons are shown on the taskbar when using multiple monitors. (W11 only) - Choose how app icons are shown on the taskbar when using multiple monitors.
- Choose combine mode for taskbar buttons and labels. (W11 only) - Choose combine mode for taskbar buttons and labels.
#### File Explorer #### File Explorer
- Change the default location that File Explorer opens to. - Change the default location that File Explorer opens to.
- Show file extensions for known file types. - Show file extensions for known file types.
- Show hidden files, folders and drives. - Show hidden files, folders and drives.
- Hide the Home or Gallery section from the File Explorer navigation pane. (W11 only) - Hide the Home or Gallery section from the File Explorer navigation pane.
- Hide duplicate removable drive entries from the File Explorer navigation pane, so only the entry under 'This PC' remains. - Hide duplicate removable drive entries from the File Explorer navigation pane, so only the entry under 'This PC' remains.
- Add all common folders (Desktop, Downloads, etc.) back to 'This PC' in File Explorer. (W11 only) - Add all common folders (Desktop, Downloads, etc.) back to 'This PC' in File Explorer.
- Hide the 3D objects, music or OneDrive folder from the File Explorer navigation pane. (W10 only) - Hide the 3D objects, music or OneDrive folder from the File Explorer navigation pane.
- Hide the 'Include in library', 'Give access to' and 'Share' options from the context menu. (W10 only) - Hide the 'Include in library', 'Give access to' and 'Share' options from the context menu.
- Change drive letter position or visibility in File Explorer.
#### Multi-tasking #### Multi-tasking
- Disable window snapping. (W11 only) - Disable window snapping.
- Disable Snap Assist suggestions when snapping a window. (W11 only) - Disable Snap Assist suggestions when snapping a window.
- Disable Snap Layout suggestions when dragging windows to the top of screen and when hovering on the maximize button. (W11 only) - Disable Snap Layout suggestions when dragging windows to the top of screen and when hovering on the maximize button.
- Change if tabs are shown when snapping or pressing Alt+Tab. (W11 only) - Change if tabs are shown when snapping or pressing Alt+Tab.
#### Optional Windows Features #### Optional Windows Features
- Enable Windows Sandbox, a lightweight desktop environment for safely running applications in isolation. (W11 only) - Enable Windows Sandbox, a lightweight desktop environment for safely running applications in isolation.
- Enable Windows Subsystem for Linux which allows you to run a Linux environment directly on Windows. (W11 only) - Enable Windows Subsystem for Linux which allows you to run a Linux environment directly on Windows.
#### Other #### Other

View File

@@ -0,0 +1,4 @@
Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer]
"ShowDriveLettersFirst"=dword:00000002

View File

@@ -0,0 +1,4 @@
Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer]
"ShowDriveLettersFirst"=dword:00000004

View File

@@ -0,0 +1,4 @@
Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer]
"ShowDriveLettersFirst"=dword:00000000

View File

@@ -0,0 +1,4 @@
Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer]
"ShowDriveLettersFirst"=dword:00000001

View File

@@ -0,0 +1,4 @@
Windows Registry Editor Version 5.00
[hkey_users\default\Software\Microsoft\Windows\CurrentVersion\Explorer]
"ShowDriveLettersFirst"=dword:00000002

View File

@@ -0,0 +1,4 @@
Windows Registry Editor Version 5.00
[hkey_users\default\Software\Microsoft\Windows\CurrentVersion\Explorer]
"ShowDriveLettersFirst"=dword:00000004

View File

@@ -0,0 +1,4 @@
Windows Registry Editor Version 5.00
[hkey_users\default\Software\Microsoft\Windows\CurrentVersion\Explorer]
"ShowDriveLettersFirst"=dword:00000000

View File

@@ -0,0 +1,4 @@
Windows Registry Editor Version 5.00
[hkey_users\default\Software\Microsoft\Windows\CurrentVersion\Explorer]
"ShowDriveLettersFirst"=dword:00000001

View File

@@ -0,0 +1,4 @@
Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer]
"ShowDriveLettersFirst"=dword:00000000

View File

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

View File

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

51
Schemas/BubbleHint.xaml Normal file
View File

@@ -0,0 +1,51 @@
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="BubblePanel"
SnapsToDevicePixels="True">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0"
Margin="10,10,10,8">
<Border Name="BubbleBorder"
Background="{DynamicResource CardBgColor}"
BorderBrush="{DynamicResource ButtonBorderColor}"
BorderThickness="1"
CornerRadius="8"
Padding="10,7,10,7">
<Border.Effect>
<DropShadowEffect BlurRadius="9"
Opacity="0.16"
ShadowDepth="2"
Direction="270"
Color="Black"/>
</Border.Effect>
<TextBlock Name="BubbleText"
Text="View the selected changes here"
TextWrapping="Wrap"
MaxWidth="260"
Foreground="{DynamicResource FgColor}"/>
</Border>
</Grid>
<Grid Grid.Row="1"
HorizontalAlignment="Center"
Margin="0,-9,0,0"
Panel.ZIndex="1"
Width="12"
Height="8">
<Polygon Name="BubblePointer"
Points="0,0 12,0 6,7"
Fill="{DynamicResource CardBgColor}"
Stroke="{DynamicResource ButtonBorderColor}"
StrokeThickness="1"
Stretch="Fill"/>
<Rectangle VerticalAlignment="Top"
Height="2"
Margin="1,-1,1,0"
Fill="{DynamicResource CardBgColor}"/>
</Grid>
</Grid>

View File

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

View File

@@ -1,16 +1,33 @@
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
Title="Win11Debloat" Title="Win11Debloat"
MinWidth="1130" MinHeight="600" MinWidth="1130" MinHeight="600"
MaxWidth="1400"
ResizeMode="CanResize" ResizeMode="CanResize"
SnapsToDevicePixels="True" SnapsToDevicePixels="True"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
WindowStyle="None" WindowStyle="None"
AllowsTransparency="True" AllowsTransparency="False"
Background="Transparent" Background="{DynamicResource BgColor}"
Foreground="{DynamicResource FgColor}"> Foreground="{DynamicResource FgColor}">
<shell:WindowChrome.WindowChrome>
<shell:WindowChrome ResizeBorderThickness="5"
CaptionHeight="32"
CornerRadius="8"
GlassFrameThickness="0"
UseAeroCaptionButtons="False"/>
</shell:WindowChrome.WindowChrome>
<Window.Resources> <Window.Resources>
<!-- Sort column header hover style -->
<Style x:Key="SortHeaderBtnStyle" TargetType="StackPanel">
<Setter Property="Opacity" Value="1.0"/>
<Setter Property="Background" Value="Transparent"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Opacity" Value="0.75"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- ComboBox Style --> <!-- ComboBox Style -->
<Style TargetType="ComboBox"> <Style TargetType="ComboBox">
<Setter Property="Background" Value="{DynamicResource ComboBgColor}"/> <Setter Property="Background" Value="{DynamicResource ComboBgColor}"/>
@@ -99,13 +116,16 @@
PlacementTarget="{Binding ElementName=ToggleButton}" PlacementTarget="{Binding ElementName=ToggleButton}"
VerticalOffset="1" VerticalOffset="1"
HorizontalOffset="0"> HorizontalOffset="0">
<Grid x:Name="DropDown" MinWidth="{TemplateBinding ActualWidth}" MaxHeight="{TemplateBinding MaxDropDownHeight}"> <Grid x:Name="DropDown" MinWidth="{TemplateBinding ActualWidth}" MaxHeight="{TemplateBinding MaxDropDownHeight}" Margin="12">
<Border x:Name="DropDownBorder" <Border x:Name="DropDownBorder"
Background="{DynamicResource ComboItemBgColor}" Background="{DynamicResource ComboItemBgColor}"
BorderBrush="{DynamicResource BorderColor}" BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1" BorderThickness="1"
CornerRadius="4" CornerRadius="4"
Padding="5,4,5,1"> Padding="5,4,5,1">
<Border.Effect>
<DropShadowEffect BlurRadius="12" Opacity="0.25" ShadowDepth="4"/>
</Border.Effect>
<ScrollViewer Margin="0,2,0,0" <ScrollViewer Margin="0,2,0,0"
VerticalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"> HorizontalScrollBarVisibility="Disabled">
@@ -338,7 +358,10 @@
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </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="18" Height="18" Background="{DynamicResource CheckBoxBgColor}" BorderBrush="{DynamicResource CheckBoxBorderColor}" BorderThickness="1" CornerRadius="4" Margin="0,0,8,0">
<Grid>
<TextBlock x:Name="CheckMark" Text="&#xE73E;" FontFamily="Segoe Fluent Icons" FontSize="12" Foreground="{DynamicResource ButtonBg}" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Collapsed"/> <TextBlock x:Name="CheckMark" Text="&#xE73E;" FontFamily="Segoe Fluent Icons" FontSize="12" Foreground="{DynamicResource ButtonBg}" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Collapsed"/>
<TextBlock x:Name="IndeterminateMark" Text="&#xE738;" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource ButtonBg}" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Collapsed" Margin="1,1,0,0" />
</Grid>
</Border> </Border>
<ContentPresenter Grid.Column="1" VerticalAlignment="Center" Margin="0,0,0,2"/> <ContentPresenter Grid.Column="1" VerticalAlignment="Center" Margin="0,0,0,2"/>
</Grid> </Grid>
@@ -353,6 +376,13 @@
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonBg}"/> <Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonBg}"/>
<Setter TargetName="CheckMark" Property="Foreground" Value="White"/> <Setter TargetName="CheckMark" Property="Foreground" Value="White"/>
</Trigger> </Trigger>
<Trigger Property="IsChecked" Value="{x:Null}">
<Setter TargetName="IndeterminateMark" Property="Visibility" Value="Visible"/>
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonBg}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonBg}"/>
<Setter TargetName="CheckBoxBorder" Property="Opacity" Value="0.8"/>
<Setter TargetName="IndeterminateMark" Property="Foreground" Value="White"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False"> <Trigger Property="IsEnabled" Value="False">
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonDisabled}"/> <Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonDisabled}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource BorderColor}"/> <Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
@@ -368,6 +398,15 @@
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonHover}"/> <Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonHover}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonHover}"/> <Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonHover}"/>
</MultiTrigger> </MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsChecked" Value="{x:Null}"/>
</MultiTrigger.Conditions>
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonHover}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonHover}"/>
<Setter TargetName="CheckBoxBorder" Property="Opacity" Value="0.8"/>
</MultiTrigger>
</ControlTemplate.Triggers> </ControlTemplate.Triggers>
</ControlTemplate> </ControlTemplate>
</Setter.Value> </Setter.Value>
@@ -407,10 +446,37 @@
<Setter Property="Margin" Value="8,0,8,0"/> <Setter Property="Margin" Value="8,0,8,0"/>
</Style> </Style>
<!-- Column widths for the apps table row grid --> <!-- Column widths for the app table rows and header (dot | name | description | id) -->
<GridLength x:Key="AppTableCol0Width">160</GridLength> <GridLength x:Key="AppTableDotColWidth">16</GridLength>
<GridLength x:Key="AppTableCol1Width">1*</GridLength> <GridLength x:Key="AppTableNameColWidth">151</GridLength>
<GridLength x:Key="AppTableCol2Width">286</GridLength> <GridLength x:Key="AppTableDescColWidth">1*</GridLength>
<GridLength x:Key="AppTableIdColWidth">261</GridLength>
<!-- Recommendation dot shape style for app table rows -->
<Style x:Key="AppRecommendationDotStyle" TargetType="Ellipse">
<Setter Property="Width" Value="9"/>
<Setter Property="Height" Value="9"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<!-- Container style for each dynamically-created app table row -->
<Style x:Key="AppTableRowStyle" TargetType="Grid">
<Setter Property="Margin" Value="0,1,0,0"/>
</Style>
<!-- Style for dynamically-created preset checkboxes in the Quick Select popup -->
<Style x:Key="PresetCheckBoxStyle" TargetType="CheckBox" BasedOn="{StaticResource {x:Type CheckBox}}">
<Setter Property="Margin" Value="8,4"/>
</Style>
<!-- Progress step indicator fill colors -->
<SolidColorBrush x:Key="ProgressActiveColor" Color="#0067c0"/>
<SolidColorBrush x:Key="ProgressInactiveColor" Color="#808080"/>
<!-- Validation feedback colors for username input -->
<SolidColorBrush x:Key="ValidationErrorColor" Color="#c42b1c"/>
<SolidColorBrush x:Key="ValidationSuccessColor" Color="#28a745"/>
<!-- Title Bar Button Style --> <!-- Title Bar Button Style -->
<Style x:Key="TitleBarButton" TargetType="Button"> <Style x:Key="TitleBarButton" TargetType="Button">
@@ -450,10 +516,10 @@
</Setter> </Setter>
<Style.Triggers> <Style.Triggers>
<Trigger Property="IsMouseOver" Value="True"> <Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHover}"/> <Setter Property="Background" Value="{DynamicResource TitlebarButtonHover}"/>
</Trigger> </Trigger>
<Trigger Property="IsPressed" Value="True"> <Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressed}"/> <Setter Property="Background" Value="{DynamicResource TitlebarButtonPressed}"/>
</Trigger> </Trigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>
@@ -480,16 +546,23 @@
<Setter Property="BorderThickness" Value="1"/> <Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="4"/> <Setter Property="Padding" Value="4"/>
<Setter Property="HasDropShadow" Value="True"/> <Setter Property="HasDropShadow" Value="True"/>
<Setter Property="HorizontalOffset" Value="-12"/>
<Setter Property="VerticalOffset" Value="-12"/>
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="ContextMenu"> <ControlTemplate TargetType="ContextMenu">
<Border Margin="12">
<Border Background="{TemplateBinding Background}" <Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4" CornerRadius="4"
Padding="{TemplateBinding Padding}"> Padding="{TemplateBinding Padding}">
<Border.Effect>
<DropShadowEffect BlurRadius="12" Opacity="0.25" ShadowDepth="4"/>
</Border.Effect>
<StackPanel IsItemsHost="True"/> <StackPanel IsItemsHost="True"/>
</Border> </Border>
</Border>
</ControlTemplate> </ControlTemplate>
</Setter.Value> </Setter.Value>
</Setter> </Setter>
@@ -535,18 +608,11 @@
</Style> </Style>
</Window.Resources> </Window.Resources>
<Border BorderBrush="{DynamicResource BorderColor}" <Border x:Name="MainBorder" BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1" BorderThickness="1"
CornerRadius="8" CornerRadius="8"
Background="{DynamicResource BgColor}" Background="{DynamicResource BgColor}"
Margin="25"> Margin="0">
<Border.Effect>
<DropShadowEffect Color="Black"
Opacity="0.15"
BlurRadius="20"
ShadowDepth="0"
Direction="0"/>
</Border.Effect>
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="32"/> <RowDefinition Height="32"/>
@@ -555,18 +621,18 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Resize Borders --> <!-- Resize Borders -->
<Rectangle x:Name="ResizeLeft" Width="5" HorizontalAlignment="Left" Fill="Transparent" Cursor="SizeWE" Grid.Row="0" Grid.RowSpan="3" Panel.ZIndex="100"/> <Rectangle x:Name="ResizeLeft" Width="5" HorizontalAlignment="Left" Fill="Transparent" Cursor="SizeWE" Grid.Row="0" Grid.RowSpan="3" Panel.ZIndex="100" Visibility="Collapsed" IsHitTestVisible="False"/>
<Rectangle x:Name="ResizeRight" Width="5" HorizontalAlignment="Right" Fill="Transparent" Cursor="SizeWE" Grid.Row="0" Grid.RowSpan="3" Panel.ZIndex="100"/> <Rectangle x:Name="ResizeRight" Width="5" HorizontalAlignment="Right" Fill="Transparent" Cursor="SizeWE" Grid.Row="0" Grid.RowSpan="3" Panel.ZIndex="100" Visibility="Collapsed" IsHitTestVisible="False"/>
<Rectangle x:Name="ResizeTop" Height="5" VerticalAlignment="Top" Fill="Transparent" Cursor="SizeNS" Grid.Row="0" Panel.ZIndex="100"/> <Rectangle x:Name="ResizeTop" Height="5" VerticalAlignment="Top" Fill="Transparent" Cursor="SizeNS" Grid.Row="0" Panel.ZIndex="100" Visibility="Collapsed" IsHitTestVisible="False"/>
<Rectangle x:Name="ResizeBottom" Height="5" VerticalAlignment="Bottom" Fill="Transparent" Cursor="SizeNS" Grid.Row="2" Panel.ZIndex="100"/> <Rectangle x:Name="ResizeBottom" Height="5" VerticalAlignment="Bottom" Fill="Transparent" Cursor="SizeNS" Grid.Row="2" Panel.ZIndex="100" Visibility="Collapsed" IsHitTestVisible="False"/>
<Rectangle x:Name="ResizeTopLeft" Width="8" Height="8" HorizontalAlignment="Left" VerticalAlignment="Top" Fill="Transparent" Cursor="SizeNWSE" Grid.Row="0" Panel.ZIndex="101"/> <Rectangle x:Name="ResizeTopLeft" Width="8" Height="8" HorizontalAlignment="Left" VerticalAlignment="Top" Fill="Transparent" Cursor="SizeNWSE" Grid.Row="0" Panel.ZIndex="101" Visibility="Collapsed" IsHitTestVisible="False"/>
<Rectangle x:Name="ResizeTopRight" Width="8" Height="8" HorizontalAlignment="Right" VerticalAlignment="Top" Fill="Transparent" Cursor="SizeNESW" Grid.Row="0" Panel.ZIndex="101"/> <Rectangle x:Name="ResizeTopRight" Width="8" Height="8" HorizontalAlignment="Right" VerticalAlignment="Top" Fill="Transparent" Cursor="SizeNESW" Grid.Row="0" Panel.ZIndex="101" Visibility="Collapsed" IsHitTestVisible="False"/>
<Rectangle x:Name="ResizeBottomLeft" Width="8" Height="8" HorizontalAlignment="Left" VerticalAlignment="Bottom" Fill="Transparent" Cursor="SizeNESW" Grid.Row="2" Panel.ZIndex="101"/> <Rectangle x:Name="ResizeBottomLeft" Width="8" Height="8" HorizontalAlignment="Left" VerticalAlignment="Bottom" Fill="Transparent" Cursor="SizeNESW" Grid.Row="2" Panel.ZIndex="101" Visibility="Collapsed" IsHitTestVisible="False"/>
<Rectangle x:Name="ResizeBottomRight" Width="8" Height="8" HorizontalAlignment="Right" VerticalAlignment="Bottom" Fill="Transparent" Cursor="SizeNWSE" Grid.Row="2" Panel.ZIndex="101"/> <Rectangle x:Name="ResizeBottomRight" Width="8" Height="8" HorizontalAlignment="Right" VerticalAlignment="Bottom" Fill="Transparent" Cursor="SizeNWSE" Grid.Row="2" Panel.ZIndex="101" Visibility="Collapsed" IsHitTestVisible="False"/>
<!-- Custom Title Bar --> <!-- Custom Title Bar -->
<Grid Grid.Row="0" x:Name="TitleBar"> <Grid Grid.Row="0" x:Name="TitleBar">
<Border Background="{DynamicResource BgColor}" CornerRadius="8,8,0,0"> <Border x:Name="TitleBarBackground" Background="{DynamicResource BgColor}" CornerRadius="8,8,0,0">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
@@ -579,10 +645,33 @@
Margin="12,0,0,0" Margin="12,0,0,0"
FontSize="12"/> FontSize="12"/>
<StackPanel Grid.Column="1" Orientation="Horizontal"> <StackPanel Grid.Column="1" Orientation="Horizontal">
<Button x:Name="KofiBtn" Content="&#xEB52;" FontFamily="Segoe Fluent Icons" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="Support the creator" AutomationProperties.Name="Support the creator"/> <Button x:Name="KofiBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="&#xEB52;" FontFamily="Segoe Fluent Icons" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="Support the creator" AutomationProperties.Name="Support the creator"/>
<Button x:Name="MenuBtn" Content="&#xE700;" FontFamily="Segoe Fluent Icons" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="Options" AutomationProperties.Name="Options"> <Button x:Name="MenuBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="&#xE700;" FontFamily="Segoe Fluent Icons" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="Options" AutomationProperties.Name="Options">
<Button.ContextMenu> <Button.ContextMenu>
<ContextMenu x:Name="MainMenu"> <ContextMenu x:Name="MainMenu">
<ContextMenu.Resources>
<Style x:Key="{x:Static MenuItem.SeparatorStyleKey}" TargetType="Separator">
<Setter Property="Margin" Value="4,6"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Separator">
<Border Height="1" Background="{DynamicResource BorderColor}" SnapsToDevicePixels="True" HorizontalAlignment="Stretch"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ContextMenu.Resources>
<MenuItem x:Name="ImportConfigBtn" Header="Import config" AutomationProperties.Name="Import configuration">
<MenuItem.Icon>
<TextBlock Text="&#xe838;" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="ExportConfigBtn" Header="Export config" AutomationProperties.Name="Export configuration">
<MenuItem.Icon>
<TextBlock Text="&#xe74e;" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem x:Name="MenuDocumentation" Header="Documentation" AutomationProperties.Name="Documentation"> <MenuItem x:Name="MenuDocumentation" Header="Documentation" AutomationProperties.Name="Documentation">
<MenuItem.Icon> <MenuItem.Icon>
<TextBlock Text="&#xe736;" FontFamily="Segoe MDL2 Assets" FontSize="16" Foreground="{DynamicResource FgColor}"/> <TextBlock Text="&#xe736;" FontFamily="Segoe MDL2 Assets" FontSize="16" Foreground="{DynamicResource FgColor}"/>
@@ -606,7 +695,7 @@
</ContextMenu> </ContextMenu>
</Button.ContextMenu> </Button.ContextMenu>
</Button> </Button>
<Button x:Name="CloseBtn" Content="&#xE8BB;" Style="{StaticResource CloseButton}" ToolTip="Close" AutomationProperties.Name="Close"/> <Button x:Name="CloseBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="&#xE8BB;" Style="{StaticResource CloseButton}" ToolTip="Close" AutomationProperties.Name="Close"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
</Border> </Border>
@@ -710,9 +799,60 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Grid.Column="0"> <StackPanel Orientation="Horizontal" Grid.Column="0">
<Button x:Name="DefaultAppsBtn" Content="Select Default Apps" ToolTip="Select the default selection of apps" Style="{DynamicResource SecondaryButtonStyle}" Height="32" Padding="10,0" Margin="0,0,10,0" AutomationProperties.Name="Select Default Apps"/> <ToggleButton x:Name="PresetsBtn" ToolTip="Select or clear app presets" Height="32" Padding="10,0" Margin="0,0,10,0" AutomationProperties.Name="App Presets">
<Button x:Name="LoadLastUsedAppsBtn" Content="Select Last Used Selection" ToolTip="Select the apps that were selected the last time Win11Debloat was run" Style="{DynamicResource SecondaryButtonStyle}" Height="32" Padding="10,0" Margin="0,0,10,0" AutomationProperties.Name="Select Last Used Selection"/> <ToggleButton.Style>
<Style TargetType="ToggleButton">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBg}"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border x:Name="Border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="4" Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,0,0,1"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHover}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressed}"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHover}"/>
</Trigger>
</Style.Triggers>
</Style>
</ToggleButton.Style>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Quick Select" FontSize="13" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock x:Name="PresetsArrow" Text="&#xE70D;" FontFamily="Segoe Fluent Icons" FontSize="10" VerticalAlignment="Center" RenderTransformOrigin="0.5,0.5">
<TextBlock.RenderTransform>
<RotateTransform x:Name="PresetsArrowRotation" Angle="0"/>
</TextBlock.RenderTransform>
</TextBlock>
</StackPanel>
</ToggleButton>
<Button x:Name="ClearAppSelectionBtn" Content="Clear Selection" ToolTip="Clear all selected apps" Style="{DynamicResource SecondaryButtonStyle}" Height="32" Padding="10,0" Margin="0,0,10,0" AutomationProperties.Name="Clear Selection"/> <Button x:Name="ClearAppSelectionBtn" Content="Clear Selection" ToolTip="Clear all selected apps" Style="{DynamicResource SecondaryButtonStyle}" Height="32" Padding="10,0" Margin="0,0,10,0" AutomationProperties.Name="Clear Selection"/>
<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.Effect>
<DropShadowEffect BlurRadius="12" Opacity="0.25" ShadowDepth="4"/>
</Border.Effect>
<StackPanel x:Name="PresetsPanel" MinWidth="220">
<CheckBox x:Name="PresetDefaultApps" Content="Default selection" IsThreeState="True" Foreground="{DynamicResource FgColor}" Margin="8,4" AutomationProperties.Name="Default selection"/>
<CheckBox x:Name="PresetLastUsed" Content="Last used selection" IsThreeState="True" Foreground="{DynamicResource FgColor}" Margin="8,4" AutomationProperties.Name="Last used selection"/>
<Separator Margin="4,6" Background="{DynamicResource BorderColor}"/>
<StackPanel x:Name="JsonPresetsPanel"/>
</StackPanel>
</Border>
</Popup>
</StackPanel> </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" IsChecked="False" Foreground="{DynamicResource FgColor}" VerticalAlignment="Center" AutomationProperties.Name="Only show installed apps"/>
@@ -754,15 +894,31 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Column Headers --> <!-- 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 BorderColor}" BorderThickness="1,1,1,0" CornerRadius="4,4,0,0">
<Grid Margin="26,6,8,8"> <Grid Margin="42,6,23,8">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="160"/> <ColumnDefinition Width="{StaticResource AppTableDotColWidth}"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="{StaticResource AppTableNameColWidth}"/>
<ColumnDefinition Width="300"/> <ColumnDefinition Width="{StaticResource AppTableDescColWidth}"/>
<ColumnDefinition Width="{StaticResource AppTableIdColWidth}"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Name" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource FgColor}" Margin="16,0,0,0"/> <StackPanel x:Name="HeaderNameBtn" Grid.Column="1" Orientation="Horizontal" Cursor="Hand" VerticalAlignment="Center" Style="{StaticResource SortHeaderBtnStyle}">
<TextBlock Grid.Column="1" Text="Description" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource FgColor}" Margin="24,0,0,0"/> <TextBlock Text="Name" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource FgColor}"/>
<TextBlock Grid.Column="2" Text="App ID" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource FgColor}"/> <TextBlock x:Name="SortArrowName" Text="&#xE70E;" 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.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="&#xE70E;" 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.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="&#xE70E;" 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.RenderTransform><RotateTransform Angle="0"/></TextBlock.RenderTransform>
</TextBlock>
</StackPanel>
</Grid> </Grid>
</Border> </Border>
<!-- Apps content --> <!-- Apps content -->
@@ -772,7 +928,26 @@
<StackPanel x:Name="AppSelectionPanel" Margin="10,4,0,4"/> <StackPanel x:Name="AppSelectionPanel" Margin="10,4,0,4"/>
</ScrollViewer> </ScrollViewer>
<Border x:Name="LoadingAppsIndicator" CornerRadius="0,0,4,4" Background="{DynamicResource CardBgColor}" Opacity="0.8" Visibility="Collapsed"> <Border x:Name="LoadingAppsIndicator" CornerRadius="0,0,4,4" Background="{DynamicResource CardBgColor}" Opacity="0.8" Visibility="Collapsed">
<TextBlock Text="Loading apps..." FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource FgColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/> <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="&#xF16A;" FontFamily="Segoe Fluent Icons" FontSize="28" Foreground="{DynamicResource FgColor}" HorizontalAlignment="Center" Margin="0,0,0,8" RenderTransformOrigin="0.5,0.5">
<TextBlock.RenderTransform>
<RotateTransform Angle="0"/>
</TextBlock.RenderTransform>
<TextBlock.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
From="0" To="360"
Duration="0:0:1.5"
RepeatBehavior="Forever"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</TextBlock.Triggers>
</TextBlock>
<TextBlock Text="Loading apps..." FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource FgColor}" HorizontalAlignment="Center"/>
</StackPanel>
</Border> </Border>
</Grid> </Grid>
</Border> </Border>
@@ -988,7 +1163,7 @@
<!-- Review & Apply Section --> <!-- Review & Apply Section -->
<StackPanel Grid.Row="1" HorizontalAlignment="Stretch" Background="{DynamicResource BgColor}"> <StackPanel Grid.Row="1" HorizontalAlignment="Stretch" Background="{DynamicResource BgColor}">
<Button x:Name="ReviewChangesBtn" Background="Transparent" BorderThickness="0" Cursor="Hand" HorizontalAlignment="Center" Margin="0,4,0,8" AutomationProperties.Name="Review selected changes"> <Button x:Name="ReviewChangesBtn" Background="Transparent" BorderThickness="0" Cursor="Hand" HorizontalAlignment="Center" Margin="0,4,0,10" AutomationProperties.Name="Review selected changes">
<Button.Template> <Button.Template>
<ControlTemplate TargetType="Button"> <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="14" Foreground="{DynamicResource ButtonBg}" FontWeight="SemiBold" HorizontalAlignment="Center"/>

View File

@@ -3,13 +3,13 @@
Title="MessageBox" Title="MessageBox"
Width="440" Width="440"
SizeToContent="Height" SizeToContent="Height"
MaxHeight="500" MaxHeight="501"
ResizeMode="NoResize" ResizeMode="NoResize"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
WindowStyle="None" WindowStyle="None"
AllowsTransparency="True" AllowsTransparency="True"
Background="Transparent" Background="Transparent"
Topmost="True" Topmost="False"
ShowInTaskbar="False"> ShowInTaskbar="False">
<Border BorderBrush="{DynamicResource BorderColor}" <Border BorderBrush="{DynamicResource BorderColor}"

View File

@@ -0,0 +1,31 @@
# Run winget list and return installed apps.
# Use -NonBlocking to keep the UI responsive (GUI mode) via Invoke-NonBlocking.
function GetInstalledAppsViaWinget {
param (
[int]$TimeOut = 10,
[switch]$NonBlocking
)
if (-not $script:WingetInstalled) { return $null }
$fetchBlock = {
param($timeOut)
$job = Start-Job { return winget list --accept-source-agreements --disable-interactivity }
$done = $job | Wait-Job -Timeout $timeOut
if ($done) {
$result = Receive-Job -Job $job
Remove-Job -Job $job -ErrorAction SilentlyContinue
return $result
}
Remove-Job -Job $job -Force -ErrorAction SilentlyContinue
return $null
}
if ($NonBlocking) {
return Invoke-NonBlocking -ScriptBlock $fetchBlock -ArgumentList $TimeOut
}
else {
return & $fetchBlock $TimeOut
}
}

View File

@@ -9,6 +9,9 @@ function RemoveApps {
$appIndex = 0 $appIndex = 0
$appCount = @($appsList).Count $appCount = @($appsList).Count
$edgeIds = @('Microsoft.Edge', 'XPFFTQ037JWMHS')
$edgeUninstallSucceeded = $false
$edgeScheduledTaskAdded = $false
Foreach ($app in $appsList) { Foreach ($app in $appsList) {
if ($script:CancelRequested) { if ($script:CancelRequested) {
@@ -25,20 +28,27 @@ function RemoveApps {
Write-Host "Attempting to remove $app..." Write-Host "Attempting to remove $app..."
# Use WinGet only to remove OneDrive and Edge # Use WinGet only to remove OneDrive and Edge
if (($app -eq "Microsoft.OneDrive") -or ($app -eq "Microsoft.Edge")) { if (($app -eq "Microsoft.OneDrive") -or ($edgeIds -contains $app)) {
if ($script:WingetInstalled -eq $false) { if ($script:WingetInstalled -eq $false) {
Write-Host "WinGet is either not installed or is outdated, $app could not be removed" -ForegroundColor Red Write-Host "WinGet is either not installed or is outdated, $app could not be removed" -ForegroundColor Red
continue continue
} }
$appName = $app -replace '\.', '_' $isEdgeId = $edgeIds -contains $app
$appName = if ($isEdgeId) { 'Microsoft_Edge' } else { $app -replace '\.', '_' }
# Uninstall app via WinGet, or create a scheduled task to uninstall it later # Uninstall app via WinGet, or create a scheduled task to uninstall it later
if ($script:Params.ContainsKey("User")) { if ($script:Params.ContainsKey("User")) {
if (-not ($isEdgeId -and $edgeScheduledTaskAdded)) {
ImportRegistryFile "Adding scheduled task to uninstall $app for user $(GetUserName)..." "Uninstall_$($appName).reg" ImportRegistryFile "Adding scheduled task to uninstall $app for user $(GetUserName)..." "Uninstall_$($appName).reg"
if ($isEdgeId) { $edgeScheduledTaskAdded = $true }
}
} }
elseif ($script:Params.ContainsKey("Sysprep")) { elseif ($script:Params.ContainsKey("Sysprep")) {
if (-not ($isEdgeId -and $edgeScheduledTaskAdded)) {
ImportRegistryFile "Adding scheduled task to uninstall $app after for new users..." "Uninstall_$($appName).reg" ImportRegistryFile "Adding scheduled task to uninstall $app after for new users..." "Uninstall_$($appName).reg"
if ($isEdgeId) { $edgeScheduledTaskAdded = $true }
}
} }
else { else {
# Uninstall app via WinGet # Uninstall app via WinGet
@@ -47,7 +57,20 @@ function RemoveApps {
winget uninstall --accept-source-agreements --disable-interactivity --id $appId winget uninstall --accept-source-agreements --disable-interactivity --id $appId
} -ArgumentList $app } -ArgumentList $app
If (($app -eq "Microsoft.Edge") -and (Select-String -InputObject $wingetOutput -Pattern "Uninstall failed with exit code")) { $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 Write-Host "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red
if ($script:GuiWindow) { if ($script:GuiWindow) {
@@ -64,6 +87,7 @@ function RemoveApps {
} }
} }
} }
}
continue continue
} }

View File

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

View File

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

View File

@@ -5,31 +5,25 @@ function CreateSystemRestorePoint {
if ($SysRestore.RPSessionInterval -eq 0) { if ($SysRestore.RPSessionInterval -eq 0) {
# In GUI mode, skip the prompt and just try to enable it # In GUI mode, skip the prompt and just try to enable it
if ($script:GuiWindow -or $Silent -or $( Read-Host -Prompt "System restore is disabled, would you like to enable it and create a restore point? (y/n)") -eq 'y') { if ($script:GuiWindow -or $Silent -or $( Read-Host -Prompt "System restore is disabled, would you like to enable it and create a restore point? (y/n)") -eq 'y') {
$enableSystemRestoreJob = Start-Job { try {
$enableResult = Invoke-NonBlocking -TimeoutSeconds 20 -ScriptBlock {
try { try {
Enable-ComputerRestore -Drive "$env:SystemDrive" Enable-ComputerRestore -Drive "$env:SystemDrive"
return $null
} }
catch { catch {
return "Error: Failed to enable System Restore: $_" return "Error: Failed to enable System Restore: $_"
} }
return $null }
}
catch {
$enableResult = "Error: Failed to enable System Restore: $_"
} }
$enableSystemRestoreJobDone = $enableSystemRestoreJob | Wait-Job -TimeOut 20 if ($enableResult) {
Write-Host $enableResult -ForegroundColor Red
if (-not $enableSystemRestoreJobDone) {
Remove-Job -Job $enableSystemRestoreJob -Force -ErrorAction SilentlyContinue
Write-Host "Error: Failed to enable system restore and create restore point, operation timed out" -ForegroundColor Red
$failed = $true $failed = $true
} }
else {
$result = Receive-Job $enableSystemRestoreJob
Remove-Job -Job $enableSystemRestoreJob -ErrorAction SilentlyContinue
if ($result) {
Write-Host $result -ForegroundColor Red
$failed = $true
}
}
} }
else { else {
Write-Host "" Write-Host ""
@@ -38,46 +32,43 @@ function CreateSystemRestorePoint {
} }
if (-not $failed) { if (-not $failed) {
$createRestorePointJob = Start-Job { try {
# Find existing restore points that are less than 24 hours old $result = Invoke-NonBlocking -TimeoutSeconds 20 -ScriptBlock {
try { try {
$recentRestorePoints = Get-ComputerRestorePoint | Where-Object { (Get-Date) - [System.Management.ManagementDateTimeConverter]::ToDateTime($_.CreationTime) -le (New-TimeSpan -Hours 24) } $recentRestorePoints = Get-ComputerRestorePoint | Where-Object { (Get-Date) - [System.Management.ManagementDateTimeConverter]::ToDateTime($_.CreationTime) -le (New-TimeSpan -Hours 24) }
} }
catch { catch {
return @{ Success = $false; Message = "Error: Unable to retrieve existing restore points: $_" } return [PSCustomObject]@{ Success = $false; Message = "Error: Unable to retrieve existing restore points: $_" }
} }
if ($recentRestorePoints.Count -eq 0) { if ($recentRestorePoints.Count -eq 0) {
try { try {
Checkpoint-Computer -Description "Restore point created by Win11Debloat" -RestorePointType "MODIFY_SETTINGS" Checkpoint-Computer -Description "Restore point created by Win11Debloat" -RestorePointType "MODIFY_SETTINGS"
return @{ Success = $true; Message = "System restore point created successfully" } return [PSCustomObject]@{ Success = $true; Message = "System restore point created successfully" }
} }
catch { catch {
return @{ Success = $false; Message = "Error: Unable to create restore point: $_" } return [PSCustomObject]@{ Success = $false; Message = "Error: Unable to create restore point: $_" }
} }
} }
else { else {
return @{ Success = $true; Message = "A recent restore point already exists, no new restore point was created" } return [PSCustomObject]@{ Success = $true; Message = "A recent restore point already exists, no new restore point was created" }
} }
} }
}
catch {
$result = [PSCustomObject]@{ Success = $false; Message = "Error: Failed to create system restore point: $_" }
}
$createRestorePointJobDone = $createRestorePointJob | Wait-Job -TimeOut 20 if ($result -and $result.Success) {
if (-not $createRestorePointJobDone) {
Remove-Job -Job $createRestorePointJob -Force -ErrorAction SilentlyContinue
Write-Host "Error: Failed to create system restore point, operation timed out" -ForegroundColor Red
$failed = $true
}
else {
$result = Receive-Job $createRestorePointJob
Remove-Job -Job $createRestorePointJob -ErrorAction SilentlyContinue
if ($result.Success) {
Write-Host $result.Message Write-Host $result.Message
} }
else { elseif ($result) {
Write-Host $result.Message -ForegroundColor Red Write-Host $result.Message -ForegroundColor Red
$failed = $true $failed = $true
} }
else {
Write-Host "Error: Failed to create system restore point" -ForegroundColor Red
$failed = $true
} }
} }

View File

@@ -0,0 +1,196 @@
# 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'
}
'DisableWidgets' {
# Also remove the app package for Widgets
RemoveApps 'Microsoft.StartExperiencesApp'
}
}
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
}
"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 {
# 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
}
$totalSteps = $actionableKeys.Count
if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ }
$currentStep = 0
# 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"
}
Write-Host "> Attempting to create 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: construct a name from Action and Label, or just Label
if ($feature.Action) {
$stepName = "$($feature.Action) $($feature.Label)"
} else {
$stepName = $feature.Label
}
}
}
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $currentStep $totalSteps $stepName
}
ExecuteParameter -paramKey $paramKey
}
}

View File

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

View File

@@ -0,0 +1,22 @@
# Read Apps.json and return the list of preset objects (Name + AppIds).
# Returns an empty array if the file cannot be read or contains no presets.
function LoadAppPresetsFromJson {
try {
$jsonContent = Get-Content -Path $script:AppsListFilePath -Raw | ConvertFrom-Json
}
catch {
Write-Warning "Failed to read Apps.json: $_"
return @()
}
if (-not $jsonContent.Presets) {
return @()
}
return @($jsonContent.Presets | ForEach-Object {
[PSCustomObject]@{
Name = $_.Name
AppIds = @($_.AppIds)
}
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,10 +11,34 @@ function ApplySettingsToUiControls {
return $false return $false
} }
# First, reset all tweaks to "No Change" (index 0) or unchecked if (-not $uiControlMappings) {
if ($uiControlMappings) { return $true
}
# Build control cache and reverse index (featureId -> control info) in a single pass
$controlCache = @{}
$featureIdIndex = @{}
foreach ($comboName in $uiControlMappings.Keys) { foreach ($comboName in $uiControlMappings.Keys) {
$control = $window.FindName($comboName) $control = $window.FindName($comboName)
if (-not $control) { continue }
$controlCache[$comboName] = $control
$mapping = $uiControlMappings[$comboName]
if ($mapping.Type -eq 'group') {
$i = 1
foreach ($val in $mapping.Values) {
foreach ($fid in $val.FeatureIds) {
$featureIdIndex[$fid] = @{ ComboName = $comboName; Control = $control; Index = $i; MappingType = 'group' }
}
$i++
}
}
elseif ($mapping.Type -eq 'feature') {
$featureIdIndex[$mapping.FeatureId] = @{ ComboName = $comboName; Control = $control; MappingType = 'feature' }
}
# Reset control to default state
if ($control -is [System.Windows.Controls.CheckBox]) { if ($control -is [System.Windows.Controls.CheckBox]) {
$control.IsChecked = $false $control.IsChecked = $false
} }
@@ -22,40 +46,24 @@ function ApplySettingsToUiControls {
$control.SelectedIndex = 0 $control.SelectedIndex = 0
} }
} }
}
# Apply settings from JSON # Apply settings using O(1) lookups
foreach ($setting in $settingsJson.Settings) { foreach ($setting in $settingsJson.Settings) {
if ($setting.Value -ne $true) { continue } if ($setting.Value -ne $true) { continue }
$paramName = $setting.Name if ($setting.Name -eq 'CreateRestorePoint') { continue }
# Skip RestorePointCheckBox, this is always checked by default $entry = $featureIdIndex[$setting.Name]
if ($paramName -eq 'CreateRestorePoint') { if (-not $entry) { continue }
continue
}
if ($uiControlMappings) { $control = $entry.Control
foreach ($comboName in $uiControlMappings.Keys) { if (-not $control -or $control.Visibility -ne 'Visible') { continue }
$mapping = $uiControlMappings[$comboName]
if ($mapping.Type -eq 'group') { if ($entry.MappingType -eq 'group') {
$i = 1
foreach ($val in $mapping.Values) {
if ($val.FeatureIds -contains $paramName) {
$control = $window.FindName($comboName)
if ($control -and $control.Visibility -eq 'Visible') {
if ($control -is [System.Windows.Controls.ComboBox]) { if ($control -is [System.Windows.Controls.ComboBox]) {
$control.SelectedIndex = $i $control.SelectedIndex = $entry.Index
} }
} }
break else {
}
$i++
}
}
elseif ($mapping.Type -eq 'feature') {
if ($mapping.FeatureId -eq $paramName) {
$control = $window.FindName($comboName)
if ($control -and $control.Visibility -eq 'Visible') {
if ($control -is [System.Windows.Controls.CheckBox]) { if ($control -is [System.Windows.Controls.CheckBox]) {
$control.IsChecked = $true $control.IsChecked = $true
} }
@@ -64,10 +72,6 @@ function ApplySettingsToUiControls {
} }
} }
} }
}
}
}
}
return $true return $true
} }

View File

@@ -30,6 +30,8 @@ function SetWindowThemeResources {
$window.Resources.Add("InputFocusColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#1f1f1f"))) $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("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("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("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("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("SearchHighlightActiveColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#8A7000")))
@@ -60,6 +62,8 @@ function SetWindowThemeResources {
$window.Resources.Add("InputFocusColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#fbfbfb"))) $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("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("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("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("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("SearchHighlightActiveColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFD966")))

View File

@@ -83,8 +83,10 @@ function Show-AboutDialog {
}) })
# Show dialog # Show dialog
try {
$aboutWindow.ShowDialog() | Out-Null $aboutWindow.ShowDialog() | Out-Null
}
finally {
# Hide overlay after dialog closes # Hide overlay after dialog closes
if ($overlay) { if ($overlay) {
try { try {
@@ -93,3 +95,4 @@ function Show-AboutDialog {
catch { } catch { }
} }
} }
}

View File

@@ -75,7 +75,8 @@ function Show-AppSelectionWindow {
$checkbox = New-Object System.Windows.Controls.CheckBox $checkbox = New-Object System.Windows.Controls.CheckBox
$checkbox.Content = $_.DisplayName $checkbox.Content = $_.DisplayName
$checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $_.DisplayName) $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $_.DisplayName)
$checkbox.Tag = $_.AppId $checkbox.Tag = $_.AppIdDisplay
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppIds' -Value @($_.AppId)
$checkbox.IsChecked = $_.IsChecked $checkbox.IsChecked = $_.IsChecked
$checkbox.ToolTip = $_.Description $checkbox.ToolTip = $_.Description
$checkbox.Style = $window.Resources["AppsPanelCheckBoxStyle"] $checkbox.Style = $window.Resources["AppsPanelCheckBoxStyle"]
@@ -118,9 +119,10 @@ function Show-AppSelectionWindow {
$selectedApps = @() $selectedApps = @()
foreach ($child in $appsPanel.Children) { foreach ($child in $appsPanel.Children) {
if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) { if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) {
$selectedApps += $child.Tag $selectedApps += @($child.AppIds)
} }
} }
$selectedApps = @($selectedApps | Where-Object { $_ } | Select-Object -Unique)
# Close form without saving if no apps were selected # Close form without saving if no apps were selected
if ($selectedApps.Count -eq 0) { if ($selectedApps.Count -eq 0) {
@@ -143,7 +145,7 @@ function Show-AppSelectionWindow {
# Load apps after window is shown (allows UI to render first) # Load apps after window is shown (allows UI to render first)
$window.Add_ContentRendered({ $window.Add_ContentRendered({
$window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ LoadApps }) $window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ LoadApps }) | Out-Null
}) })
# Show the window and return dialog result # Show the window and return dialog result

View File

@@ -216,7 +216,7 @@ function Show-ApplyModal {
$script:ApplyProgressCallback = $null $script:ApplyProgressCallback = $null
$script:ApplySubStepCallback = $null $script:ApplySubStepCallback = $null
} }
}) }) | Out-Null
# Button handlers # Button handlers
$applyCloseBtn.Add_Click({ $applyCloseBtn.Add_Click({
@@ -242,8 +242,10 @@ function Show-ApplyModal {
}) })
# Show dialog # Show dialog
try {
$applyWindow.ShowDialog() | Out-Null $applyWindow.ShowDialog() | Out-Null
}
finally {
# Hide overlay after dialog closes # Hide overlay after dialog closes
if ($overlay) { if ($overlay) {
try { try {
@@ -252,3 +254,4 @@ function Show-ApplyModal {
catch { } catch { }
} }
} }
}

115
Scripts/GUI/Show-Bubble.ps1 Normal file
View File

@@ -0,0 +1,115 @@
function Hide-Bubble {
param (
[Parameter(Mandatory=$false)]
[switch]$Immediate
)
if ($script:BubbleTimer) {
$script:BubbleTimer.Stop()
$script:BubbleTimer = $null
}
if (-not $script:BubblePopup) { return }
if ($Immediate -or -not $script:BubblePopup.Child) {
$script:BubblePopup.IsOpen = $false
$script:BubblePopup = $null
$script:BubbleIsClosing = $false
return
}
if ($script:BubbleIsClosing) { return }
$script:BubbleIsClosing = $true
$bubblePanel = $script:BubblePopup.Child
$fadeOut = New-Object System.Windows.Media.Animation.DoubleAnimation
$fadeOut.From = [double]$bubblePanel.Opacity
$fadeOut.To = 0
$fadeOut.Duration = [System.Windows.Duration]::new([TimeSpan]::FromMilliseconds(220))
$fadeOut.Add_Completed({
if ($script:BubblePopup) {
$script:BubblePopup.IsOpen = $false
$script:BubblePopup = $null
}
$script:BubbleIsClosing = $false
})
$bubblePanel.BeginAnimation([System.Windows.UIElement]::OpacityProperty, $fadeOut)
}
function Show-Bubble {
param (
[Parameter(Mandatory=$true)]
[System.Windows.Controls.Control]$TargetControl,
[Parameter(Mandatory=$false)]
[string]$Message = 'View the selected changes here',
[Parameter(Mandatory=$false)]
[int]$DurationSeconds = 5
)
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
if (-not $TargetControl) { return }
Hide-Bubble -Immediate
$xaml = Get-Content -Path $script:BubbleHintSchema -Raw
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
try {
$bubblePanel = [System.Windows.Markup.XamlReader]::Load($reader)
}
finally {
$reader.Close()
}
$bubbleText = $bubblePanel.FindName('BubbleText')
if ($bubbleText) {
$bubbleText.Text = $Message
}
$bubblePanel.BeginAnimation([System.Windows.UIElement]::OpacityProperty, $null)
$bubblePanel.Opacity = 0
$popup = New-Object System.Windows.Controls.Primitives.Popup
$popup.AllowsTransparency = $true
$popup.PopupAnimation = 'None'
$popup.StaysOpen = $true
$popup.PlacementTarget = $TargetControl
$popup.Placement = [System.Windows.Controls.Primitives.PlacementMode]::Top
$popup.VerticalOffset = -1
$popup.Child = $bubblePanel
$popup.Add_Opened({
param($sender, $e)
if (-not $sender) { return }
$panel = $sender.Child
$target = $sender.PlacementTarget
if (-not $panel -or -not $target) { return }
$panel.Measure([System.Windows.Size]::new([double]::PositiveInfinity, [double]::PositiveInfinity))
$bubbleWidth = $panel.DesiredSize.Width
$targetWidth = $target.ActualWidth
$sender.HorizontalOffset = ($targetWidth - $bubbleWidth) / 2
$fadeIn = New-Object System.Windows.Media.Animation.DoubleAnimation
$fadeIn.From = 0
$fadeIn.To = 1
$fadeIn.BeginTime = [TimeSpan]::FromMilliseconds(30)
$fadeIn.Duration = [System.Windows.Duration]::new([TimeSpan]::FromMilliseconds(320))
$panel.BeginAnimation([System.Windows.UIElement]::OpacityProperty, $fadeIn)
})
$script:BubbleIsClosing = $false
$script:BubblePopup = $popup
$script:BubblePopup.IsOpen = $true
$script:BubbleTimer = New-Object System.Windows.Threading.DispatcherTimer
$script:BubbleTimer.Interval = [TimeSpan]::FromSeconds([Math]::Max(1, $DurationSeconds))
$script:BubbleTimer.Add_Tick({
Hide-Bubble
})
$script:BubbleTimer.Start()
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -152,8 +152,10 @@ function Show-MessageBox {
}) })
# Show dialog and return result from Tag # Show dialog and return result from Tag
try {
$msgWindow.ShowDialog() | Out-Null $msgWindow.ShowDialog() | Out-Null
}
finally {
# Hide overlay after dialog closes (only if this dialog was the one that showed it) # Hide overlay after dialog closes (only if this dialog was the one that showed it)
if ($overlay -and -not $overlayWasAlreadyVisible) { if ($overlay -and -not $overlayWasAlreadyVisible) {
try { try {
@@ -161,6 +163,7 @@ function Show-MessageBox {
} }
catch { } catch { }
} }
}
return $msgWindow.Tag return $msgWindow.Tag
} }

View File

@@ -11,6 +11,7 @@ param (
[switch]$RunDefaults, [switch]$RunDefaults,
[switch]$RunDefaultsLite, [switch]$RunDefaultsLite,
[switch]$RunSavedSettings, [switch]$RunSavedSettings,
[string]$Config,
[string]$Apps, [string]$Apps,
[string]$AppRemovalTarget, [string]$AppRemovalTarget,
[switch]$RemoveApps, [switch]$RemoveApps,
@@ -95,7 +96,11 @@ param (
[switch]$HideMusic, [switch]$HideMusic,
[switch]$HideIncludeInLibrary, [switch]$HideIncludeInLibrary,
[switch]$HideGiveAccessTo, [switch]$HideGiveAccessTo,
[switch]$HideShare [switch]$HideShare,
[switch]$ShowDriveLettersFirst,
[switch]$ShowDriveLettersLast,
[switch]$ShowNetworkDriveLettersFirst,
[switch]$HideDriveLetters
) )
# Show error if current powershell environment does not have LanguageMode set to FullLanguage # Show error if current powershell environment does not have LanguageMode set to FullLanguage
@@ -215,7 +220,7 @@ if (Test-Path "$env:TEMP/Win11Debloat") {
Write-Output "> Cleaning up..." Write-Output "> Cleaning up..."
# Cleanup, remove Win11Debloat directory # Cleanup, remove Win11Debloat directory
Get-ChildItem -Path "$env:TEMP/Win11Debloat" -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Config,Logs | Remove-Item -Recurse -Force Get-ChildItem -Path "$env:TEMP/Win11Debloat" -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Win11Debloat-Run.log,Config,Logs | Remove-Item -Recurse -Force
} }
Write-Output "" Write-Output ""

View File

@@ -0,0 +1,15 @@
# Add parameter to script and write to file
function AddParameter {
param (
$parameterName,
$value = $true
)
# Add parameter or update its value if key already exists
if (-not $script:Params.ContainsKey($parameterName)) {
$script:Params.Add($parameterName, $value)
}
else {
$script:Params[$parameterName] = $value
}
}

View File

@@ -0,0 +1,32 @@
function CheckIfUserExists {
param (
$userName
)
if ($userName -match '[<>:"|?*]') {
return $false
}
if ([string]::IsNullOrWhiteSpace($userName)) {
return $false
}
try {
$userExists = Test-Path "$env:SystemDrive\Users\$userName"
if ($userExists) {
return $true
}
$userExists = Test-Path ($env:USERPROFILE -Replace ('\\' + $env:USERNAME + '$'), "\$userName")
if ($userExists) {
return $true
}
}
catch {
Write-Error "Something went wrong when trying to find the user directory path for user $userName. Please ensure the user exists on this system"
}
return $false
}

View File

@@ -0,0 +1,27 @@
# Check if this machine supports S0 Modern Standby power state. Returns true if S0 Modern Standby is supported, false otherwise.
function CheckModernStandbySupport {
$count = 0
try {
switch -Regex (powercfg /a) {
':' {
$count += 1
}
'(.*S0.{1,}\))' {
if ($count -eq 1) {
return $true
}
}
}
}
catch {
Write-Host "Error: Unable to check for S0 Modern Standby support, powercfg command failed" -ForegroundColor Red
Write-Host ""
Write-Host "Press any key to continue..."
$null = [System.Console]::ReadKey()
return $true
}
return $false
}

View File

@@ -0,0 +1,20 @@
# Generates a list of apps to remove based on the Apps parameter
function GenerateAppsList {
if (-not ($script:Params["Apps"] -and $script:Params["Apps"] -is [string])) {
return @()
}
$appMode = $script:Params["Apps"].toLower()
switch ($appMode) {
'default' {
$appsList = LoadAppsFromFile $script:AppsListFilePath
return $appsList
}
default {
$appsList = $script:Params["Apps"].Split(',') | ForEach-Object { $_.Trim() }
$validatedAppsList = ValidateAppslist $appsList
return $validatedAppsList
}
}
}

View File

@@ -0,0 +1,9 @@
function GetFriendlyTargetUserName {
$target = GetTargetUserForAppRemoval
switch ($target) {
"AllUsers" { return "all users" }
"CurrentUser" { return "the current user" }
default { return "user $target" }
}
}

View File

@@ -0,0 +1,9 @@
# Target is determined from $script:Params["AppRemovalTarget"] or defaults to "AllUsers"
# Target values: "AllUsers" (removes for all users + from image), "CurrentUser", or a specific username
function GetTargetUserForAppRemoval {
if ($script:Params.ContainsKey("AppRemovalTarget")) {
return $script:Params["AppRemovalTarget"]
}
return "AllUsers"
}

View File

@@ -0,0 +1,36 @@
# Returns the directory path of the specified user, exits script if user path can't be found
function GetUserDirectory {
param (
$userName,
$fileName = "",
$exitIfPathNotFound = $true
)
try {
if (-not (CheckIfUserExists -userName $userName) -and $userName -ne "*") {
Write-Error "User $userName does not exist on this system"
AwaitKeyToExit
}
$userDirectoryExists = Test-Path "$env:SystemDrive\Users\$userName"
$userPath = "$env:SystemDrive\Users\$userName\$fileName"
if ((Test-Path $userPath) -or ($userDirectoryExists -and (-not $exitIfPathNotFound))) {
return $userPath
}
$userDirectoryExists = Test-Path ($env:USERPROFILE -Replace ('\\' + $env:USERNAME + '$'), "\$userName")
$userPath = $env:USERPROFILE -Replace ('\\' + $env:USERNAME + '$'), "\$userName\$fileName"
if ((Test-Path $userPath) -or ($userDirectoryExists -and (-not $exitIfPathNotFound))) {
return $userPath
}
}
catch {
Write-Error "Something went wrong when trying to find the user directory path for user $userName. Please ensure the user exists on this system"
AwaitKeyToExit
}
Write-Error "Unable to find user directory path for user $userName"
AwaitKeyToExit
}

View File

@@ -0,0 +1,7 @@
function GetUserName {
if ($script:Params.ContainsKey("User")) {
return $script:Params.Item("User")
}
return $env:USERNAME
}

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
# Processes all pending WPF window messages (input, render, etc.) to keep the UI responsive
# during long-running operations on the UI thread. Equivalent to Application.DoEvents().
function DoEvents {
if (-not $script:GuiWindow) { return }
$frame = [System.Windows.Threading.DispatcherFrame]::new()
$null = [System.Windows.Threading.Dispatcher]::CurrentDispatcher.BeginInvoke(
[System.Windows.Threading.DispatcherPriority]::Background,
[System.Windows.Threading.DispatcherOperationCallback]{
param($f)
$f.Continue = $false
return $null
},
$frame
)
$null = [System.Windows.Threading.Dispatcher]::PushFrame($frame)
}

View File

@@ -0,0 +1,55 @@
# Runs a scriptblock in a background PowerShell runspace while keeping the UI responsive.
# In GUI mode, the work executes on a separate thread and the UI thread pumps messages (~60fps).
# In CLI mode, the scriptblock runs directly in the current session.
function Invoke-NonBlocking {
param(
[scriptblock]$ScriptBlock,
[object[]]$ArgumentList = @(),
[int]$TimeoutSeconds = 0
)
# CLI mode without timeout: run directly in-process
if (-not $script:GuiWindow -and $TimeoutSeconds -eq 0) {
return (& $ScriptBlock @ArgumentList)
}
$ps = [powershell]::Create()
try {
$null = $ps.AddScript($ScriptBlock.ToString())
foreach ($arg in $ArgumentList) {
$null = $ps.AddArgument($arg)
}
$handle = $ps.BeginInvoke()
if ($script:GuiWindow) {
# GUI mode: pump UI messages while waiting
$stopwatch = if ($TimeoutSeconds -gt 0) { [System.Diagnostics.Stopwatch]::StartNew() } else { $null }
while (-not $handle.IsCompleted) {
if ($stopwatch -and $stopwatch.Elapsed.TotalSeconds -ge $TimeoutSeconds) {
$ps.Stop()
throw "Operation timed out after $TimeoutSeconds seconds"
}
DoEvents
Start-Sleep -Milliseconds 16
}
}
else {
# CLI mode with timeout: block until completion or timeout
if (-not $handle.AsyncWaitHandle.WaitOne($TimeoutSeconds * 1000)) {
$ps.Stop()
throw "Operation timed out after $TimeoutSeconds seconds"
}
}
$result = $ps.EndInvoke($handle)
if ($result.Count -eq 0) { return $null }
if ($result.Count -eq 1) { return $result[0] }
return @($result)
}
finally {
$ps.Dispose()
}
}

545
Win11Debloat.ps1 Executable file → Normal file
View File

@@ -13,6 +13,7 @@ param (
[switch]$RunDefaults, [switch]$RunDefaults,
[switch]$RunDefaultsLite, [switch]$RunDefaultsLite,
[switch]$RunSavedSettings, [switch]$RunSavedSettings,
[string]$Config,
[string]$Apps, [string]$Apps,
[string]$AppRemovalTarget, [string]$AppRemovalTarget,
[switch]$RemoveApps, [switch]$RemoveApps,
@@ -97,13 +98,17 @@ param (
[switch]$HideMusic, [switch]$HideMusic,
[switch]$HideIncludeInLibrary, [switch]$HideIncludeInLibrary,
[switch]$HideGiveAccessTo, [switch]$HideGiveAccessTo,
[switch]$HideShare [switch]$HideShare,
[switch]$ShowDriveLettersFirst,
[switch]$ShowDriveLettersLast,
[switch]$ShowNetworkDriveLettersFirst,
[switch]$HideDriveLetters
) )
# Define script-level variables & paths # Define script-level variables & paths
$script:Version = "2026.03.09" $script:Version = "2026.03.15"
$script:AppsListFilePath = "$PSScriptRoot/Config/Apps.json" $script:AppsListFilePath = "$PSScriptRoot/Config/Apps.json"
$script:DefaultSettingsFilePath = "$PSScriptRoot/Config/DefaultSettings.json" $script:DefaultSettingsFilePath = "$PSScriptRoot/Config/DefaultSettings.json"
$script:FeaturesFilePath = "$PSScriptRoot/Config/Features.json" $script:FeaturesFilePath = "$PSScriptRoot/Config/Features.json"
@@ -118,8 +123,11 @@ $script:MessageBoxSchema = "$PSScriptRoot/Schemas/MessageBoxWindow.xaml"
$script:AboutWindowSchema = "$PSScriptRoot/Schemas/AboutWindow.xaml" $script:AboutWindowSchema = "$PSScriptRoot/Schemas/AboutWindow.xaml"
$script:ApplyChangesWindowSchema = "$PSScriptRoot/Schemas/ApplyChangesWindow.xaml" $script:ApplyChangesWindowSchema = "$PSScriptRoot/Schemas/ApplyChangesWindow.xaml"
$script:SharedStylesSchema = "$PSScriptRoot/Schemas/SharedStyles.xaml" $script:SharedStylesSchema = "$PSScriptRoot/Schemas/SharedStyles.xaml"
$script:BubbleHintSchema = "$PSScriptRoot/Schemas/BubbleHint.xaml"
$script:ImportExportConfigSchema = "$PSScriptRoot/Schemas/ImportExportConfigWindow.xaml"
$script:LoadAppsDetailsScriptPath = "$PSScriptRoot/Scripts/FileIO/LoadAppsDetailsFromJson.ps1"
$script:ControlParams = 'WhatIf', 'Confirm', 'Verbose', 'Debug', 'LogPath', 'Silent', 'Sysprep', 'User', 'NoRestartExplorer', 'RunDefaults', 'RunDefaultsLite', 'RunSavedSettings', 'RunAppsListGenerator', 'CLI', 'AppRemovalTarget' $script:ControlParams = 'WhatIf', 'Confirm', 'Verbose', 'Debug', 'LogPath', 'Silent', 'Sysprep', 'User', 'NoRestartExplorer', 'RunDefaults', 'RunDefaultsLite', 'RunSavedSettings', 'Config', 'RunAppsListGenerator', 'CLI', 'AppRemovalTarget'
# Script-level variables for GUI elements # Script-level variables for GUI elements
$script:GuiWindow = $null $script:GuiWindow = $null
@@ -167,7 +175,7 @@ else {
} }
# Check if script has all required files # Check if script has all required files
if (-not ((Test-Path $script:DefaultSettingsFilePath) -and (Test-Path $script:AppsListFilePath) -and (Test-Path $script:RegfilesPath) -and (Test-Path $script:AssetsPath) -and (Test-Path $script:AppSelectionSchema) -and (Test-Path $script:ApplyChangesWindowSchema) -and (Test-Path $script:SharedStylesSchema) -and (Test-Path $script:FeaturesFilePath))) { if (-not ((Test-Path $script:DefaultSettingsFilePath) -and (Test-Path $script:AppsListFilePath) -and (Test-Path $script:RegfilesPath) -and (Test-Path $script:AssetsPath) -and (Test-Path $script:AppSelectionSchema) -and (Test-Path $script:ApplyChangesWindowSchema) -and (Test-Path $script:SharedStylesSchema) -and (Test-Path $script:BubbleHintSchema) -and (Test-Path $script:FeaturesFilePath))) {
Write-Error "Win11Debloat is unable to find required files, please ensure all script files are present" Write-Error "Win11Debloat is unable to find required files, please ensure all script files are present"
Write-Output "" Write-Output ""
Write-Output "Press any key to exit..." Write-Output "Press any key to exit..."
@@ -217,15 +225,16 @@ if (-not $script:WingetInstalled -and -not $Silent) {
################################################################################################################## ##################################################################################################################
# # # #
# FUNCTION IMPORTS/DEFINITIONS # # FUNCTION IMPORTS #
# # # #
################################################################################################################## ##################################################################################################################
# Load app removal functions # App removal functions
. "$PSScriptRoot/Scripts/AppRemoval/ForceRemoveEdge.ps1" . "$PSScriptRoot/Scripts/AppRemoval/ForceRemoveEdge.ps1"
. "$PSScriptRoot/Scripts/AppRemoval/RemoveApps.ps1" . "$PSScriptRoot/Scripts/AppRemoval/RemoveApps.ps1"
. "$PSScriptRoot/Scripts/AppRemoval/GetInstalledAppsViaWinget.ps1"
# Load CLI functions # CLI functions
. "$PSScriptRoot/Scripts/CLI/AwaitKeyToExit.ps1" . "$PSScriptRoot/Scripts/CLI/AwaitKeyToExit.ps1"
. "$PSScriptRoot/Scripts/CLI/ShowCLILastUsedSettings.ps1" . "$PSScriptRoot/Scripts/CLI/ShowCLILastUsedSettings.ps1"
. "$PSScriptRoot/Scripts/CLI/ShowCLIDefaultModeAppRemovalOptions.ps1" . "$PSScriptRoot/Scripts/CLI/ShowCLIDefaultModeAppRemovalOptions.ps1"
@@ -235,7 +244,8 @@ if (-not $script:WingetInstalled -and -not $Silent) {
. "$PSScriptRoot/Scripts/CLI/PrintPendingChanges.ps1" . "$PSScriptRoot/Scripts/CLI/PrintPendingChanges.ps1"
. "$PSScriptRoot/Scripts/CLI/PrintHeader.ps1" . "$PSScriptRoot/Scripts/CLI/PrintHeader.ps1"
# Load Feature functions # Features functions
. "$PSScriptRoot/Scripts/Features/ExecuteChanges.ps1"
. "$PSScriptRoot/Scripts/Features/CreateSystemRestorePoint.ps1" . "$PSScriptRoot/Scripts/Features/CreateSystemRestorePoint.ps1"
. "$PSScriptRoot/Scripts/Features/DisableStoreSearchSuggestions.ps1" . "$PSScriptRoot/Scripts/Features/DisableStoreSearchSuggestions.ps1"
. "$PSScriptRoot/Scripts/Features/EnableWindowsFeature.ps1" . "$PSScriptRoot/Scripts/Features/EnableWindowsFeature.ps1"
@@ -243,478 +253,45 @@ if (-not $script:WingetInstalled -and -not $Silent) {
. "$PSScriptRoot/Scripts/Features/ReplaceStartMenu.ps1" . "$PSScriptRoot/Scripts/Features/ReplaceStartMenu.ps1"
. "$PSScriptRoot/Scripts/Features/RestartExplorer.ps1" . "$PSScriptRoot/Scripts/Features/RestartExplorer.ps1"
# Load GUI functions # File I/O functions
. "$PSScriptRoot/Scripts/GUI/GetSystemUsesDarkMode.ps1"
. "$PSScriptRoot/Scripts/GUI/SetWindowThemeResources.ps1"
. "$PSScriptRoot/Scripts/GUI/AttachShiftClickBehavior.ps1"
. "$PSScriptRoot/Scripts/GUI/ApplySettingsToUiControls.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-MessageBox.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-ApplyModal.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-AppSelectionWindow.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-MainWindow.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-AboutDialog.ps1"
# Load File I/O functions
. "$PSScriptRoot/Scripts/FileIO/LoadJsonFile.ps1" . "$PSScriptRoot/Scripts/FileIO/LoadJsonFile.ps1"
. "$PSScriptRoot/Scripts/FileIO/SaveToFile.ps1"
. "$PSScriptRoot/Scripts/FileIO/SaveSettings.ps1" . "$PSScriptRoot/Scripts/FileIO/SaveSettings.ps1"
. "$PSScriptRoot/Scripts/FileIO/LoadSettings.ps1" . "$PSScriptRoot/Scripts/FileIO/LoadSettings.ps1"
. "$PSScriptRoot/Scripts/FileIO/SaveCustomAppsListToFile.ps1" . "$PSScriptRoot/Scripts/FileIO/SaveCustomAppsListToFile.ps1"
. "$PSScriptRoot/Scripts/FileIO/ValidateAppslist.ps1" . "$PSScriptRoot/Scripts/FileIO/ValidateAppslist.ps1"
. "$PSScriptRoot/Scripts/FileIO/LoadAppsFromFile.ps1" . "$PSScriptRoot/Scripts/FileIO/LoadAppsFromFile.ps1"
. "$PSScriptRoot/Scripts/FileIO/LoadAppsDetailsFromJson.ps1" . "$PSScriptRoot/Scripts/FileIO/LoadAppsDetailsFromJson.ps1"
. "$PSScriptRoot/Scripts/FileIO/LoadAppPresetsFromJson.ps1"
# Processes all pending WPF window messages (input, render, etc.) to keep the UI responsive
# during long-running operations on the UI thread. Equivalent to Application.DoEvents(). # GUI functions
function DoEvents { . "$PSScriptRoot/Scripts/GUI/GetSystemUsesDarkMode.ps1"
if (-not $script:GuiWindow) { return } . "$PSScriptRoot/Scripts/GUI/SetWindowThemeResources.ps1"
$frame = [System.Windows.Threading.DispatcherFrame]::new() . "$PSScriptRoot/Scripts/GUI/AttachShiftClickBehavior.ps1"
[System.Windows.Threading.Dispatcher]::CurrentDispatcher.BeginInvoke( . "$PSScriptRoot/Scripts/GUI/ApplySettingsToUiControls.ps1"
[System.Windows.Threading.DispatcherPriority]::Background, . "$PSScriptRoot/Scripts/GUI/Show-MessageBox.ps1"
[System.Windows.Threading.DispatcherOperationCallback]{ . "$PSScriptRoot/Scripts/GUI/Show-ConfigWindow.ps1"
param($f) . "$PSScriptRoot/Scripts/GUI/Show-ApplyModal.ps1"
$f.Continue = $false . "$PSScriptRoot/Scripts/GUI/Show-AppSelectionWindow.ps1"
return $null . "$PSScriptRoot/Scripts/GUI/Show-MainWindow.ps1"
}, . "$PSScriptRoot/Scripts/GUI/Show-AboutDialog.ps1"
$frame . "$PSScriptRoot/Scripts/GUI/Show-Bubble.ps1"
)
[System.Windows.Threading.Dispatcher]::PushFrame($frame) # Helper functions
} . "$PSScriptRoot/Scripts/Helpers/AddParameter.ps1"
. "$PSScriptRoot/Scripts/Helpers/CheckIfUserExists.ps1"
. "$PSScriptRoot/Scripts/Helpers/CheckModernStandbySupport.ps1"
# Runs a scriptblock in a background PowerShell runspace while keeping the UI responsive. . "$PSScriptRoot/Scripts/Helpers/GenerateAppsList.ps1"
# In GUI mode, the work executes on a separate thread and the UI thread pumps messages (~60fps). . "$PSScriptRoot/Scripts/Helpers/GetFriendlyTargetUserName.ps1"
# In CLI mode, the scriptblock runs directly in the current session. . "$PSScriptRoot/Scripts/Helpers/ImportConfigToParams.ps1"
function Invoke-NonBlocking { . "$PSScriptRoot/Scripts/Helpers/GetTargetUserForAppRemoval.ps1"
param( . "$PSScriptRoot/Scripts/Helpers/GetUserDirectory.ps1"
[scriptblock]$ScriptBlock, . "$PSScriptRoot/Scripts/Helpers/GetUserName.ps1"
[object[]]$ArgumentList = @() . "$PSScriptRoot/Scripts/Helpers/TestIfUserIsLoggedIn.ps1"
)
# Threading functions
if (-not $script:GuiWindow) { . "$PSScriptRoot/Scripts/Threading/DoEvents.ps1"
return (& $ScriptBlock @ArgumentList) . "$PSScriptRoot/Scripts/Threading/Invoke-NonBlocking.ps1"
}
$ps = [powershell]::Create()
try {
$null = $ps.AddScript($ScriptBlock.ToString())
foreach ($arg in $ArgumentList) {
$null = $ps.AddArgument($arg)
}
$handle = $ps.BeginInvoke()
while (-not $handle.IsCompleted) {
DoEvents
Start-Sleep -Milliseconds 16
}
$result = $ps.EndInvoke($handle)
if ($result.Count -eq 0) { return $null }
if ($result.Count -eq 1) { return $result[0] }
return @($result)
}
finally {
$ps.Dispose()
}
}
# Add parameter to script and write to file
function AddParameter {
param (
$parameterName,
$value = $true
)
# Add parameter or update its value if key already exists
if (-not $script:Params.ContainsKey($parameterName)) {
$script:Params.Add($parameterName, $value)
}
else {
$script:Params[$parameterName] = $value
}
}
# Run winget list and return installed apps (sync or async)
function GetInstalledAppsViaWinget {
param (
[int]$TimeOut = 10,
[switch]$Async
)
if (-not $script:WingetInstalled) { return $null }
if ($Async) {
$wingetListJob = Start-Job { return winget list --accept-source-agreements --disable-interactivity }
return @{ Job = $wingetListJob; StartTime = Get-Date }
}
else {
$wingetListJob = Start-Job { return winget list --accept-source-agreements --disable-interactivity }
$jobDone = $wingetListJob | Wait-Job -TimeOut $TimeOut
if (-not $jobDone) {
Remove-Job -Job $wingetListJob -Force -ErrorAction SilentlyContinue
return $null
}
$result = Receive-Job -Job $wingetListJob
Remove-Job -Job $wingetListJob -ErrorAction SilentlyContinue
return $result
}
}
function GetUserName {
if ($script:Params.ContainsKey("User")) {
return $script:Params.Item("User")
}
return $env:USERNAME
}
# Returns the directory path of the specified user, exits script if user path can't be found
function GetUserDirectory {
param (
$userName,
$fileName = "",
$exitIfPathNotFound = $true
)
try {
if (-not (CheckIfUserExists -userName $userName) -and $userName -ne "*") {
Write-Error "User $userName does not exist on this system"
AwaitKeyToExit
}
$userDirectoryExists = Test-Path "$env:SystemDrive\Users\$userName"
$userPath = "$env:SystemDrive\Users\$userName\$fileName"
if ((Test-Path $userPath) -or ($userDirectoryExists -and (-not $exitIfPathNotFound))) {
return $userPath
}
$userDirectoryExists = Test-Path ($env:USERPROFILE -Replace ('\\' + $env:USERNAME + '$'), "\$userName")
$userPath = $env:USERPROFILE -Replace ('\\' + $env:USERNAME + '$'), "\$userName\$fileName"
if ((Test-Path $userPath) -or ($userDirectoryExists -and (-not $exitIfPathNotFound))) {
return $userPath
}
}
catch {
Write-Error "Something went wrong when trying to find the user directory path for user $userName. Please ensure the user exists on this system"
AwaitKeyToExit
}
Write-Error "Unable to find user directory path for user $userName"
AwaitKeyToExit
}
function CheckIfUserExists {
param (
$userName
)
if ($userName -match '[<>:"|?*]') {
return $false
}
if ([string]::IsNullOrWhiteSpace($userName)) {
return $false
}
try {
$userExists = Test-Path "$env:SystemDrive\Users\$userName"
if ($userExists) {
return $true
}
$userExists = Test-Path ($env:USERPROFILE -Replace ('\\' + $env:USERNAME + '$'), "\$userName")
if ($userExists) {
return $true
}
}
catch {
Write-Error "Something went wrong when trying to find the user directory path for user $userName. Please ensure the user exists on this system"
}
return $false
}
# Target is determined from $script:Params["AppRemovalTarget"] or defaults to "AllUsers"
# Target values: "AllUsers" (removes for all users + from image), "CurrentUser", or a specific username
function GetTargetUserForAppRemoval {
if ($script:Params.ContainsKey("AppRemovalTarget")) {
return $script:Params["AppRemovalTarget"]
}
return "AllUsers"
}
function GetFriendlyTargetUserName {
$target = GetTargetUserForAppRemoval
switch ($target) {
"AllUsers" { return "all users" }
"CurrentUser" { return "the current user" }
default { return "user $target" }
}
}
# Check if this machine supports S0 Modern Standby power state. Returns true if S0 Modern Standby is supported, false otherwise.
function CheckModernStandbySupport {
$count = 0
try {
switch -Regex (powercfg /a) {
':' {
$count += 1
}
'(.*S0.{1,}\))' {
if ($count -eq 1) {
return $true
}
}
}
}
catch {
Write-Host "Error: Unable to check for S0 Modern Standby support, powercfg command failed" -ForegroundColor Red
Write-Host ""
Write-Host "Press any key to continue..."
$null = [System.Console]::ReadKey()
return $true
}
return $false
}
# Generates a list of apps to remove based on the Apps parameter
function GenerateAppsList {
if (-not ($script:Params["Apps"] -and $script:Params["Apps"] -is [string])) {
return @()
}
$appMode = $script:Params["Apps"].toLower()
switch ($appMode) {
'default' {
$appsList = LoadAppsFromFile $script:AppsListFilePath
return $appsList
}
default {
$appsList = $script:Params["Apps"].Split(',') | ForEach-Object { $_.Trim() }
$validatedAppsList = ValidateAppslist $appsList
return $validatedAppsList
}
}
}
# 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'
}
'DisableWidgets' {
# Also remove the app package for Widgets
RemoveApps 'Microsoft.StartExperiencesApp'
}
}
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
}
"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
# Parameters:
function ExecuteAllChanges {
# 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
}
$totalSteps = $actionableKeys.Count
if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ }
$currentStep = 0
# 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"
}
Write-Host "> Attempting to create 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: construct a name from Action and Label, or just Label
if ($feature.Action) {
$stepName = "$($feature.Action) $($feature.Label)"
} else {
$stepName = $feature.Label
}
}
}
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $currentStep $totalSteps $stepName
}
ExecuteParameter -paramKey $paramKey
}
}
@@ -784,6 +361,9 @@ if ((Test-Path $script:SavedSettingsFilePath) -and ([String]::IsNullOrWhiteSpace
Remove-Item -Path $script:SavedSettingsFilePath -recurse Remove-Item -Path $script:SavedSettingsFilePath -recurse
} }
# Default to CLI mode for deployment-targeted parameters.
$launchInCLI = $CLI -or $script:Params.ContainsKey("User") -or $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("AppRemovalTarget")
# Only run the app selection form if the 'RunAppsListGenerator' parameter was passed to the script # Only run the app selection form if the 'RunAppsListGenerator' parameter was passed to the script
if ($RunAppsListGenerator) { if ($RunAppsListGenerator) {
PrintHeader "Custom Apps List Generator" PrintHeader "Custom Apps List Generator"
@@ -803,7 +383,7 @@ if ($RunAppsListGenerator) {
} }
# Change script execution based on provided parameters or user input # Change script execution based on provided parameters or user input
if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSavedSettings -or ($controlParamsCount -eq $script:Params.Count)) { if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSavedSettings -or $Config -or ($controlParamsCount -eq $script:Params.Count)) {
if ($RunDefaults -or $RunDefaultsLite) { if ($RunDefaults -or $RunDefaultsLite) {
ShowCLIDefaultModeOptions ShowCLIDefaultModeOptions
} }
@@ -816,8 +396,23 @@ if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSa
ShowCLILastUsedSettings ShowCLILastUsedSettings
} }
elseif ($Config) {
try {
ImportConfigToParams -ConfigPath $Config -CurrentBuild $WinVersion -ExpectedVersion '1.0'
}
catch {
Write-Error "$_"
AwaitKeyToExit
}
if (-not $Silent) {
PrintHeader 'Custom Mode'
PrintPendingChanges
PrintHeader 'Custom Mode'
}
}
else { else {
if ($CLI) { if ($launchInCLI) {
$Mode = ShowCLIMenuOptions $Mode = ShowCLIMenuOptions
} }
else { else {