From 65aabbc05048bab89baf705049f566c4333e3f4a Mon Sep 17 00:00:00 2001 From: Jeffrey <9938813+Raphire@users.noreply.github.com> Date: Sun, 15 Feb 2026 23:08:54 +0100 Subject: [PATCH] Refactor code structure for improved readability and maintainability (#473) * Add ToolTips to Tweaks --- .gitignore | 1 + Assets/Features.json | 66 +- Run.bat | 5 +- {Assets/Schemas => Schemas}/AboutWindow.xaml | 12 +- .../AppSelectionWindow.xaml | 0 {Assets/Schemas => Schemas}/MainWindow.xaml | 0 .../Schemas => Schemas}/MessageBoxWindow.xaml | 0 Scripts/CLI/PrintHeader.ps1 | 20 + Scripts/CLI/PrintPendingChanges.ps1 | 66 + Scripts/CLI/ShowCLIAppRemoval.ps1 | 28 + .../ShowCLIDefaultModeAppRemovalOptions.ps1 | 33 + Scripts/CLI/ShowCLIDefaultModeOptions.ps1 | 55 + Scripts/CLI/ShowCLILastUsedSettings.ps1 | 16 + Scripts/CLI/ShowCLIMenuOptions.ps1 | 30 + Scripts/FileIO/LoadAppsDetailsFromJson.ps1 | 44 + Scripts/FileIO/LoadAppsFromFile.ps1 | 45 + Scripts/FileIO/LoadJsonFile.ps1 | 32 + Scripts/FileIO/LoadSettings.ps1 | 31 + Scripts/FileIO/SaveCustomAppsListToFile.ps1 | 15 + Scripts/FileIO/SaveSettings.ps1 | 26 + Scripts/FileIO/ValidateAppslist.ps1 | 24 + Scripts/GUI/ApplySettingsToUiControls.ps1 | 80 + Scripts/GUI/AttachShiftClickBehavior.ps1 | 71 + Scripts/GUI/GetSystemUsesDarkMode.ps1 | 9 + Scripts/GUI/SetWindowThemeResources.ps1 | 69 + Scripts/GUI/Show-AboutDialog.ps1 | 95 + Scripts/GUI/Show-AppSelectionWindow.ps1 | 161 + Scripts/GUI/Show-MainWindow.ps1 | 1501 +++++++++ Scripts/GUI/Show-MessageBox.ps1 | 154 + Get.ps1 => Scripts/Get.ps1 | 4 +- Win11Debloat.ps1 | 2997 ++--------------- 31 files changed, 2877 insertions(+), 2813 deletions(-) rename {Assets/Schemas => Schemas}/AboutWindow.xaml (95%) rename {Assets/Schemas => Schemas}/AppSelectionWindow.xaml (100%) rename {Assets/Schemas => Schemas}/MainWindow.xaml (100%) rename {Assets/Schemas => Schemas}/MessageBoxWindow.xaml (100%) create mode 100644 Scripts/CLI/PrintHeader.ps1 create mode 100644 Scripts/CLI/PrintPendingChanges.ps1 create mode 100644 Scripts/CLI/ShowCLIAppRemoval.ps1 create mode 100644 Scripts/CLI/ShowCLIDefaultModeAppRemovalOptions.ps1 create mode 100644 Scripts/CLI/ShowCLIDefaultModeOptions.ps1 create mode 100644 Scripts/CLI/ShowCLILastUsedSettings.ps1 create mode 100644 Scripts/CLI/ShowCLIMenuOptions.ps1 create mode 100644 Scripts/FileIO/LoadAppsDetailsFromJson.ps1 create mode 100644 Scripts/FileIO/LoadAppsFromFile.ps1 create mode 100644 Scripts/FileIO/LoadJsonFile.ps1 create mode 100644 Scripts/FileIO/LoadSettings.ps1 create mode 100644 Scripts/FileIO/SaveCustomAppsListToFile.ps1 create mode 100644 Scripts/FileIO/SaveSettings.ps1 create mode 100644 Scripts/FileIO/ValidateAppslist.ps1 create mode 100644 Scripts/GUI/ApplySettingsToUiControls.ps1 create mode 100644 Scripts/GUI/AttachShiftClickBehavior.ps1 create mode 100644 Scripts/GUI/GetSystemUsesDarkMode.ps1 create mode 100644 Scripts/GUI/SetWindowThemeResources.ps1 create mode 100644 Scripts/GUI/Show-AboutDialog.ps1 create mode 100644 Scripts/GUI/Show-AppSelectionWindow.ps1 create mode 100644 Scripts/GUI/Show-MainWindow.ps1 create mode 100644 Scripts/GUI/Show-MessageBox.ps1 rename Get.ps1 => Scripts/Get.ps1 (97%) diff --git a/.gitignore b/.gitignore index 8418a12..bce4f49 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ LastSettings SavedSettings LastUsedSettings.json CustomAppsList +Logs/* Win11Debloat.log \ No newline at end of file diff --git a/Assets/Features.json b/Assets/Features.json index db74d7c..89963f8 100644 --- a/Assets/Features.json +++ b/Assets/Features.json @@ -50,6 +50,7 @@ { "GroupId": "SearchIcon", "Label": "Taskbar search style", + "ToolTip": "This setting allows you to customize the appearance of the search box on the taskbar.", "Category": "Taskbar", "Priority": 2, "Values": [ @@ -82,6 +83,7 @@ { "GroupId": "MultiMon", "Label": "Show taskbar apps on", + "ToolTip": "This setting allows you to choose where taskbar app buttons are shown when using multiple monitors.", "Category": "Taskbar", "Values": [ { @@ -107,6 +109,7 @@ { "GroupId": "CombineButtons", "Label": "Combine taskbar buttons on the main display", + "ToolTip": "This setting allows you to choose how taskbar buttons are combined on the main display.", "Category": "Taskbar", "Values": [ { @@ -132,6 +135,7 @@ { "GroupId": "CombineMMButtons", "Label": "Combine taskbar buttons on secondary displays", + "ToolTip": "This setting allows you to choose how taskbar buttons are combined on secondary displays.", "Category": "Taskbar", "Values": [ { @@ -157,6 +161,7 @@ { "GroupId": "ClearStart", "Label": "Remove pinned apps from the start menu", + "ToolTip": "This setting allows you to quickly remove all pinned apps from the start menu.", "Category": "Start Menu", "Values": [ { @@ -176,6 +181,7 @@ { "GroupId": "ExplorerLocation", "Label": "Open File Explorer to", + "ToolTip": "This setting allows you to choose the default location that File Explorer opens to.", "Category": "File Explorer", "Values": [ { @@ -207,6 +213,7 @@ { "GroupId": "ShowTabsInAltTab", "Label": "Show tabs from apps when snapping or pressing Alt+Tab", + "ToolTip": "This setting allows you to choose whether to show tabs from apps (such as Edge browser tabs) when snapping windows or pressing Alt+Tab.", "Category": "Multi-tasking", "Priority": 10, "Values": [ @@ -337,6 +344,7 @@ { "FeatureId": "DisableTelemetry", "Label": "telemetry, tracking & targeted ads", + "ToolTip": "This settings disables telemetry, diagnostic data collection, activity history, app-launch tracking and targeted ads. This limits the data that is sent to Microsoft about your device and usage.", "Category": "Privacy & Suggested Content", "Action": "Disable", "RegistryKey": "Disable_Telemetry.reg", @@ -349,6 +357,7 @@ { "FeatureId": "DisableSuggestions", "Label": "tips, tricks & suggested content throughout Windows", + "ToolTip": "Disabling this will disable various tips, tricks, suggestions and ads throughout Windows.", "Category": "Privacy & Suggested Content", "Action": "Disable", "RegistryKey": "Disable_Windows_Suggestions.reg", @@ -361,6 +370,7 @@ { "FeatureId": "DisableLockscreenTips", "Label": "tips & tricks on the lock screen", + "ToolTip": "Disabling this will turn off the tips, tricks and fun facts that appear on the lock screen.", "Category": "Privacy & Suggested Content", "Action": "Disable", "RegistryKey": "Disable_Lockscreen_Tips.reg", @@ -373,6 +383,7 @@ { "FeatureId": "DisableDesktopSpotlight", "Label": "Windows Spotlight for desktop", + "ToolTip": "Disabling this will turn off the 'Windows Spotlight' feature for the desktop background, which shows different background images and occasionally tips and fun facts on the desktop.", "Category": "Privacy & Suggested Content", "Action": "Disable", "RegistryKey": "Disable_Desktop_Spotlight.reg", @@ -385,6 +396,7 @@ { "FeatureId": "DisableEdgeAds", "Label": "ads, suggestions and newsfeed in Edge", + "ToolTip": "Disabling this will turn off various distractions from Microsoft Edge such as ads, suggestions and the MSN news feed. This setting uses policies, which means some settings will be locked.", "Category": "Privacy & Suggested Content", "Action": "Disable", "RegistryKey": "Disable_Edge_Ads_And_Suggestions.reg", @@ -397,6 +409,7 @@ { "FeatureId": "DisableCopilot", "Label": "Microsoft Copilot", + "ToolTip": "Disabling this will turn off and uninstall Microsoft Copilot, the AI assistant integrated throughout Windows.", "Category": "AI", "Action": "Disable", "RegistryKey": "Disable_Copilot.reg", @@ -409,6 +422,7 @@ { "FeatureId": "DisableRecall", "Label": "Windows Recall", + "ToolTip": "Disabling this will turn off Windows Recall, an AI-powered feature that provides quick access to recently used files, apps and activities.", "Category": "AI", "Action": "Disable", "RegistryKey": "Disable_AI_Recall.reg", @@ -421,6 +435,7 @@ { "FeatureId": "DisableClickToDo", "Label": "Click To Do, AI text & image analysis", + "ToolTip": "Disabling this will turn off Click To Do, which provides AI-powered text and image analysis features in Windows.", "Category": "AI", "Action": "Disable", "RegistryKey": "Disable_Click_to_Do.reg", @@ -433,6 +448,7 @@ { "FeatureId": "DisableDVR", "Label": "Xbox game/screen recording", + "ToolTip": "Disabling this will turn off the Xbox game/screen recording features included with the Game Bar app.", "Category": "Gaming", "Action": "Disable", "RegistryKey": "Disable_DVR.reg", @@ -445,6 +461,7 @@ { "FeatureId": "DisableGameBarIntegration", "Label": "Game Bar integration", + "ToolTip": "Disabling this will turn off the Game Bar integration features such as the Game Bar overlay that appears when pressing Win+G.", "Category": "Gaming", "Action": "Disable", "RegistryKey": "Disable_Game_Bar_Integration.reg", @@ -505,6 +522,7 @@ { "FeatureId": "DisableStartRecommended", "Label": "recommended section in the start menu", + "ToolTip": "Disabling this will hide the recommended section in the start menu, which shows recently added apps, recently opened files and app recommendations.", "Category": "Start Menu", "Action": "Hide", "RegistryKey": "Disable_Start_Recommended.reg", @@ -517,6 +535,7 @@ { "FeatureId": "DisableBing", "Label": "Bing web search & Copilot integration", + "ToolTip": "Disabling this will turn off Bing web search results and Copilot integration in the Windows search experience.", "Category": "Start Menu", "Action": "Disable", "RegistryKey": "Disable_Bing_Cortana_In_Search.reg", @@ -529,6 +548,7 @@ { "FeatureId": "DisableStartPhoneLink", "Label": "Phone Link integration in the start menu", + "ToolTip": "Disabling this will remove the Phone Link integration that appears in the start menu when you have a mobile device linked to your PC.", "Category": "Start Menu", "Action": "Disable", "RegistryKey": "Disable_Phone_Link_In_Start.reg", @@ -540,11 +560,12 @@ }, { "FeatureId": "DisableSettings365Ads", - "Label": "Microsoft 365 ads in Settings Home", + "Label": "Microsoft 365 Copilot ads in Settings Home", + "ToolTip": "Disabling this will turn off the Microsoft 365 Copilot ads that appear in the Settings Home page.", "Category": "Privacy & Suggested Content", "Action": "Hide", "RegistryKey": "Disable_Settings_365_Ads.reg", - "ApplyText": "> Disabling Microsoft 365 ads in Settings Home...", + "ApplyText": "> Disabling Microsoft 365 Copilot ads in Settings Home...", "UndoAction": "Show", "RegistryUndoKey": "Enable_Settings_365_Ads.reg", "MinVersion": null, @@ -553,7 +574,8 @@ { "FeatureId": "DisableSettingsHome", "Label": "Settings 'Home' page", - "Category": "Privacy & Suggested Content", + "ToolTip": "Removes the 'Home' page from the Settings app.", + "Category": "Other", "Action": "Hide", "RegistryKey": "Disable_Settings_Home.reg", "ApplyText": "> Disabling the Settings Home page...", @@ -565,6 +587,7 @@ { "FeatureId": "DisableEdgeAI", "Label": "AI features in Microsoft Edge", + "ToolTip": "Disabling this will turn off AI features in Microsoft Edge, such as the AI-powered sidebar and Copilot features. This setting uses policies, which means some settings will be locked.", "Category": "AI", "Action": "Disable", "RegistryKey": "Disable_Edge_AI_Features.reg", @@ -577,6 +600,7 @@ { "FeatureId": "DisablePaintAI", "Label": "AI features in Paint", + "ToolTip": "Disabling this will turn off AI features in Paint, such as the AI-powered image generation and editing tools.", "Category": "AI", "Action": "Disable", "RegistryKey": "Disable_Paint_AI_Features.reg", @@ -589,6 +613,7 @@ { "FeatureId": "DisableNotepadAI", "Label": "AI features in Notepad", + "ToolTip": "Disabling this will turn off AI features in Notepad, such as the AI-powered writing suggestions.", "Category": "AI", "Action": "Disable", "RegistryKey": "Disable_Notepad_AI_Features.reg", @@ -601,6 +626,7 @@ { "FeatureId": "EnableDarkMode", "Label": "theme for system and apps", + "ToolTip": "Enabling this will set the default app mode and system theme to dark mode, which changes the appearance of Windows and supported apps.", "Category": "Appearance", "Action": "Enable dark", "RegistryKey": "Enable_Dark_Mode.reg", @@ -613,6 +639,7 @@ { "FeatureId": "DisableDragTray", "Label": "'Drag Tray' for sharing & moving files", + "ToolTip": "The Drag Tray is a new feature for sharing & moving files in Windows 11, it appears at the top of the screen when dragging files.", "Category": "System", "Action": "Disable", "RegistryKey": "Disable_Share_Drag_Tray.reg", @@ -625,6 +652,7 @@ { "FeatureId": "RevertContextMenu", "Label": "context menu style", + "ToolTip": "This will restore the classic Windows 10 style context menu instead of the modern Windows 11 style menu.", "Category": "System", "Action": "Use classic Windows 10", "RegistryKey": "Disable_Show_More_Options_Context_Menu.reg", @@ -637,6 +665,7 @@ { "FeatureId": "DisableMouseAcceleration", "Label": "Enhance Pointer Precision (mouse acceleration)", + "ToolTip": "By default, Windows has a mouse acceleration enabled, which can make the mouse movement less predictable, especially for gaming. Disabling this will make the mouse movement more consistent.", "Category": "System", "Action": "Disable", "RegistryKey": "Disable_Enhance_Pointer_Precision.reg", @@ -649,6 +678,7 @@ { "FeatureId": "DisableStickyKeys", "Label": "Sticky Keys keyboard shortcut (5x shift)", + "ToolTip": "Disabling this will prevent the Sticky Keys dialog from appearing when you press the Shift key 5 times in a row.", "Category": "System", "Action": "Disable", "RegistryKey": "Disable_Sticky_Keys_Shortcut.reg", @@ -661,6 +691,7 @@ { "FeatureId": "DisableWindowSnapping", "Label": "window snapping", + "ToolTip": "Disabling this will turn off the ability to snap windows to the sides or corners of the screen.", "Category": "Multi-tasking", "Priority": 1, "Action": "Disable", @@ -674,6 +705,7 @@ { "FeatureId": "DisableSnapAssist", "Label": "showing app suggestions when snapping windows", + "ToolTip": "Disabling this will turn off app suggestions when you snap windows to the sides or corners of the screen.", "Category": "Multi-tasking", "Priority": 2, "Action": "Disable", @@ -687,6 +719,7 @@ { "FeatureId": "DisableSnapLayouts", "Label": "snap layout flyout at top of screen and on maximize button", + "ToolTip": "Disabling this will turn off the snap layout flyout that appears when you hover over the maximize button or drag windows to the top of the screen.", "Category": "Multi-tasking", "Priority": 3, "Action": "Hide", @@ -748,6 +781,7 @@ { "FeatureId": "TaskbarAlignLeft", "Label": "taskbar alignment", + "ToolTip": "By default, Windows 11 has the taskbar buttons centered. Enabling this will align the taskbar buttons to the left, similar to previous versions of Windows.", "Category": "Taskbar", "Priority": 1, "Action": "Left", @@ -809,6 +843,7 @@ { "FeatureId": "HideTaskview", "Label": "'Task view' button on the taskbar", + "ToolTip": "Disabling this will turn off the 'Task view' button on the taskbar, which allows you to see all your open windows and virtual desktops.", "Category": "Taskbar", "Priority": 3, "Action": "Hide", @@ -822,6 +857,7 @@ { "FeatureId": "DisableWidgets", "Label": "widgets on the taskbar & lock screen", + "ToolTip": "Disabling this will turn off the widgets features in Windows, including the widgets button on the taskbar and the widgets that can appear on the lock screen.", "Category": "Taskbar", "Priority": 4, "Action": "Disable", @@ -835,6 +871,7 @@ { "FeatureId": "HideChat", "Label": "Chat (meet now) icon on the taskbar", + "ToolTip": "Disabling this will turn off the Chat (meet now) icon on the taskbar.", "Category": "Taskbar", "Priority": 5, "Action": "Hide", @@ -848,6 +885,7 @@ { "FeatureId": "DisableFastStartup", "Label": "fast start-up", + "ToolTip": "Fast Start-up helps your PC start faster after shutdown by saving a system image to disk. Disabling Fast Start-up can help with certain issues, but may result in slightly longer boot times.", "Category": "System", "Action": "Disable", "RegistryKey": "Disable_Fast_Startup.reg", @@ -860,6 +898,7 @@ { "FeatureId": "DisableBitlockerAutoEncryption", "Label": "BitLocker automatic device encryption", + "ToolTip": "For devices that support it, Windows 11 automatically enables BitLocker device encryption. Disabling this will turn off automatic encryption of the device, but you can still manually enable BitLocker encryption if desired.", "Category": "System", "Action": "Disable", "RegistryKey": "Disable_Bitlocker_Auto_Encryption.reg", @@ -872,6 +911,7 @@ { "FeatureId": "DisableModernStandbyNetworking", "Label": "Modern Standby network connectivity", + "ToolTip": "By default, devices that support Modern Standby maintain network connectivity while in sleep mode to allow for features like receiving calls or messages while asleep. Disabling network connectivity during Modern Standby can help save battery life.", "Category": "System", "Action": "Disable", "RegistryKey": "Disable_Modern_Standby_Networking.reg", @@ -884,6 +924,7 @@ { "FeatureId": "EnableEndTask", "Label": "'End Task' option in taskbar context menu", + "ToolTip": "When enabled, adds an 'End Task' option to the right-click context menu for apps in the taskbar, allowing you to quickly force close apps without opening Task Manager.", "Category": "Taskbar", "Priority": 6, "Action": "Show", @@ -897,6 +938,7 @@ { "FeatureId": "EnableLastActiveClick", "Label": "'Last Active Click' behavior for taskbar apps", + "ToolTip": "When enabled, clicking on an app in the taskbar will switch to the last active window of that app, instead of showing the thumbnail preview.", "Category": "Taskbar", "Priority": 7, "Action": "Enable", @@ -1066,6 +1108,7 @@ { "FeatureId": "ShowKnownFileExt", "Label": "file extensions for known file types", + "ToolTip": "Enabling 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", "Action": "Show", "RegistryKey": "Show_Extensions_For_Known_File_Types.reg", @@ -1078,6 +1121,7 @@ { "FeatureId": "ShowHiddenFolders", "Label": "hidden files, folders and drives", + "ToolTip": "By default, Windows hides certain files, folders and drives to prevent accidental modification or deletion. Enabling this will show hidden files, folders and drives in File Explorer.", "Category": "File Explorer", "Action": "Show", "RegistryKey": "Show_Hidden_Folders.reg", @@ -1090,6 +1134,7 @@ { "FeatureId": "HideHome", "Label": "'Home' from navigation pane", + "ToolTip": "Hides the 'Home' section from the File Explorer navigation pane.", "Category": "File Explorer", "Action": "Hide", "RegistryKey": "Hide_Home_from_Explorer.reg", @@ -1102,6 +1147,7 @@ { "FeatureId": "HideGallery", "Label": "'Gallery' from navigation pane", + "ToolTip": "Hides the 'Gallery' section from the File Explorer navigation pane.", "Category": "File Explorer", "Action": "Hide", "RegistryKey": "Hide_Gallery_from_Explorer.reg", @@ -1114,6 +1160,7 @@ { "FeatureId": "HideDupliDrive", "Label": "duplicate removable drive entries", + "ToolTip": "When you connect a removable drive, Windows shows the drive both under 'This PC' and in the navigation pane with its own entry. Hiding duplicate removable drive entries will only show the drive under 'This PC' and remove it from the navigation pane.", "Category": "File Explorer", "Action": "Hide", "RegistryKey": "Hide_duplicate_removable_drives_from_navigation_pane_of_File_Explorer.reg", @@ -1126,6 +1173,7 @@ { "FeatureId": "AddFoldersToThisPC", "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.", "Category": "File Explorer", "Action": "Add", "RegistryKey": "Add_All_Folders_Under_This_PC.reg", @@ -1138,6 +1186,7 @@ { "FeatureId": "DisableTransparency", "Label": "transparency effects", + "ToolTip": "Disabling this will turn off transparency effects on Windows and interfaces. Which can help improve performance on older hardware.", "Category": "Appearance", "Action": "Disable", "RegistryKey": "Disable_Transparency.reg", @@ -1150,6 +1199,7 @@ { "FeatureId": "DisableAnimations", "Label": "animations and visual effects", + "ToolTip": "Disabling this will turn off animations and some visual effects in Windows, which can make the interface feel snappier and more responsive, especially on older hardware.", "Category": "Appearance", "Action": "Disable", "RegistryKey": "Disable_Animations.reg", @@ -1162,6 +1212,7 @@ { "FeatureId": "DisableUpdateASAP", "Label": "updates as soon as they're available", + "ToolTip": "Disabling this will prevent your PC from being among the first to receive new non-security updates. Your PC will still receive these updates eventually.", "Category": "Windows Update", "Action": "Prevent getting", "RegistryKey": "Disable_Update_ASAP.reg", @@ -1174,6 +1225,7 @@ { "FeatureId": "PreventUpdateAutoReboot", "Label": "automatic restarts after updates while signed in", + "ToolTip": "Disabling this will prevent your PC from automatically restarting after updates while any user is signed in.", "Category": "Windows Update", "Action": "Prevent", "RegistryKey": "Prevent_Auto_Reboot.reg", @@ -1186,6 +1238,7 @@ { "FeatureId": "DisableDeliveryOptimization", "Label": "sharing downloaded updates with other PCs", + "ToolTip": "Disabling this will prevent your PC from sharing downloaded updates with other PCs on the local network or on the internet. This also prevents your PC from downloading updates from other PCs.", "Category": "Windows Update", "Action": "Disable", "RegistryKey": "Disable_Delivery_Optimization.reg", @@ -1210,6 +1263,7 @@ { "FeatureId": "HideIncludeInLibrary", "Label": "'Include in library' option in the context menu", + "ToolTip": "Hides the 'Include in library' option from the File Explorer context menu.", "Category": "File Explorer", "Action": "Hide", "RegistryKey": "Disable_Include_in_library_from_context_menu.reg", @@ -1222,6 +1276,7 @@ { "FeatureId": "HideGiveAccessTo", "Label": "'Give access to' option in the context menu", + "ToolTip": "Hides the 'Give access to' option from the File Explorer context menu.", "Category": "File Explorer", "Action": "Hide", "RegistryKey": "Disable_Give_access_to_context_menu.reg", @@ -1234,6 +1289,7 @@ { "FeatureId": "HideShare", "Label": "'Share' option in the context menu", + "ToolTip": "Hides the 'Share' option from the File Explorer context menu.", "Category": "File Explorer", "Action": "Hide", "RegistryKey": "Disable_Share_from_context_menu.reg", @@ -1246,6 +1302,7 @@ { "FeatureId": "HideOnedrive", "Label": "'OneDrive' folder from navigation pane", + "ToolTip": "Hides the 'OneDrive' folder from the File Explorer navigation pane.", "Category": "File Explorer", "Action": "Hide", "RegistryKey": "Hide_Onedrive_Folder.reg", @@ -1258,6 +1315,7 @@ { "FeatureId": "Hide3dObjects", "Label": "'3D objects' folder under 'This PC'", + "ToolTip": "Hides the '3D objects' folder from the File Explorer navigation pane.", "Category": "File Explorer", "Action": "Hide", "RegistryKey": "Hide_3D_Objects_Folder.reg", @@ -1270,6 +1328,7 @@ { "FeatureId": "HideMusic", "Label": "'Music' folder under 'This PC'", + "ToolTip": "Hides the 'Music' folder from the File Explorer navigation pane.", "Category": "File Explorer", "Action": "Hide", "RegistryKey": "Hide_Music_Folder.reg", @@ -1282,6 +1341,7 @@ { "FeatureId": "DisableBraveBloat", "Label": "bloat in Brave browser (AI, Crypto, etc.)", + "ToolTip": "Disabling this will turn off Brave's built-in AI features, Crypto wallet, News, Rewards, Talk and VPN.", "Category": "Other", "Action": "Disable", "RegistryKey": "Disable_Brave_Bloat.reg", diff --git a/Run.bat b/Run.bat index e766580..e4debf5 100644 --- a/Run.bat +++ b/Run.bat @@ -4,7 +4,10 @@ setlocal EnableDelayedExpansion :: Set Windows Terminal installation paths. (Default and Scoop installation) set "wtDefaultPath=%LOCALAPPDATA%\Microsoft\WindowsApps\wt.exe" set "wtScoopPath=%USERPROFILE%\scoop\apps\windows-terminal\current\wt.exe" -set "logFile=%~dp0Win11Debloat.log" +set "logFile=%~dp0Logs\Win11Debloat-Run.log" + +:: Ensure Logs folder exists +if not exist "%~dp0Logs" mkdir "%~dp0Logs" :: Determine which terminal exists if exist "%wtDefaultPath%" ( diff --git a/Assets/Schemas/AboutWindow.xaml b/Schemas/AboutWindow.xaml similarity index 95% rename from Assets/Schemas/AboutWindow.xaml rename to Schemas/AboutWindow.xaml index 828433a..5b82ea9 100644 --- a/Assets/Schemas/AboutWindow.xaml +++ b/Schemas/AboutWindow.xaml @@ -13,11 +13,11 @@ @@ -196,7 +196,7 @@ - + Text="Support me on Ko-fi"/> diff --git a/Assets/Schemas/AppSelectionWindow.xaml b/Schemas/AppSelectionWindow.xaml similarity index 100% rename from Assets/Schemas/AppSelectionWindow.xaml rename to Schemas/AppSelectionWindow.xaml diff --git a/Assets/Schemas/MainWindow.xaml b/Schemas/MainWindow.xaml similarity index 100% rename from Assets/Schemas/MainWindow.xaml rename to Schemas/MainWindow.xaml diff --git a/Assets/Schemas/MessageBoxWindow.xaml b/Schemas/MessageBoxWindow.xaml similarity index 100% rename from Assets/Schemas/MessageBoxWindow.xaml rename to Schemas/MessageBoxWindow.xaml diff --git a/Scripts/CLI/PrintHeader.ps1 b/Scripts/CLI/PrintHeader.ps1 new file mode 100644 index 0000000..42d3130 --- /dev/null +++ b/Scripts/CLI/PrintHeader.ps1 @@ -0,0 +1,20 @@ +# Prints the header for the script +function PrintHeader { + param ( + $title + ) + + $fullTitle = " Win11Debloat Script - $title" + + if ($script:Params.ContainsKey("Sysprep")) { + $fullTitle = "$fullTitle (Sysprep mode)" + } + else { + $fullTitle = "$fullTitle (User: $(GetUserName))" + } + + Clear-Host + Write-Host "-------------------------------------------------------------------------------------------" + Write-Host $fullTitle + Write-Host "-------------------------------------------------------------------------------------------" +} \ No newline at end of file diff --git a/Scripts/CLI/PrintPendingChanges.ps1 b/Scripts/CLI/PrintPendingChanges.ps1 new file mode 100644 index 0000000..a8b210d --- /dev/null +++ b/Scripts/CLI/PrintPendingChanges.ps1 @@ -0,0 +1,66 @@ +# Prints all pending changes that will be made by the script +function PrintPendingChanges { + Write-Output "Win11Debloat will make the following changes:" + + if ($script:Params['CreateRestorePoint']) { + Write-Output "- $($script:Features['CreateRestorePoint'].Label)" + } + foreach ($parameterName in $script:Params.Keys) { + if ($script:ControlParams -contains $parameterName) { + continue + } + + # Print parameter description + switch ($parameterName) { + 'Apps' { + continue + } + 'CreateRestorePoint' { + continue + } + 'RemoveApps' { + $appsList = GenerateAppsList + + if ($appsList.Count -eq 0) { + Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow + Write-Output "" + continue + } + + Write-Output "- Remove $($appsList.Count) apps:" + Write-Host $appsList -ForegroundColor DarkGray + continue + } + 'RemoveAppsCustom' { + $appsList = LoadAppsFromFile $script:CustomAppsListFilePath + + if ($appsList.Count -eq 0) { + Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow + Write-Output "" + continue + } + + Write-Output "- Remove $($appsList.Count) apps:" + Write-Host $appsList -ForegroundColor DarkGray + continue + } + default { + if ($script:Features -and $script:Features.ContainsKey($parameterName)) { + $action = $script:Features[$parameterName].Action + $message = $script:Features[$parameterName].Label + Write-Output "- $action $message" + } + else { + # Fallback: show the parameter name if no feature description is available + Write-Output "- $parameterName" + } + continue + } + } + } + + Write-Output "" + Write-Output "" + Write-Output "Press enter to execute the script or press CTRL+C to quit..." + Read-Host | Out-Null +} \ No newline at end of file diff --git a/Scripts/CLI/ShowCLIAppRemoval.ps1 b/Scripts/CLI/ShowCLIAppRemoval.ps1 new file mode 100644 index 0000000..a857142 --- /dev/null +++ b/Scripts/CLI/ShowCLIAppRemoval.ps1 @@ -0,0 +1,28 @@ +# Shows the CLI app removal menu and prompts the user to select which apps to remove. +function ShowCLIAppRemoval { + PrintHeader "App Removal" + + Write-Output "> Opening app selection form..." + + $result = Show-AppSelectionWindow + + if ($result -eq $true) { + Write-Output "You have selected $($script:SelectedApps.Count) apps for removal" + AddParameter 'RemoveAppsCustom' + + SaveSettings + + # Suppress prompt if Silent parameter was passed + if (-not $Silent) { + Write-Output "" + Write-Output "" + Write-Output "Press enter to remove the selected apps or press CTRL+C to quit..." + Read-Host | Out-Null + PrintHeader "App Removal" + } + } + else { + Write-Host "Selection was cancelled, no apps have been removed" -ForegroundColor Red + Write-Output "" + } +} \ No newline at end of file diff --git a/Scripts/CLI/ShowCLIDefaultModeAppRemovalOptions.ps1 b/Scripts/CLI/ShowCLIDefaultModeAppRemovalOptions.ps1 new file mode 100644 index 0000000..635feba --- /dev/null +++ b/Scripts/CLI/ShowCLIDefaultModeAppRemovalOptions.ps1 @@ -0,0 +1,33 @@ +# Shows the CLI default mode app removal options. Loops until a valid option is selected. +function ShowCLIDefaultModeAppRemovalOptions { + PrintHeader 'Default Mode' + + Write-Host "Please note: The default selection of apps includes Microsoft Teams, Spotify, Sticky Notes and more. Select option 2 to verify and change what apps are removed by the script" -ForegroundColor DarkGray + Write-Host "" + + Do { + Write-Host "Options:" -ForegroundColor Yellow + Write-Host " (n) Don't remove any apps" -ForegroundColor Yellow + Write-Host " (1) Only remove the default selection of apps" -ForegroundColor Yellow + Write-Host " (2) Manually select which apps to remove" -ForegroundColor Yellow + $RemoveAppsInput = Read-Host "Do you want to remove any apps? Apps will be removed for all users (n/1/2)" + + # Show app selection form if user entered option 3 + if ($RemoveAppsInput -eq '2') { + $result = Show-AppSelectionWindow + + if ($result -ne $true) { + # User cancelled or closed app selection, change RemoveAppsInput so the menu will be shown again + Write-Host "" + Write-Host "Cancelled application selection, please try again" -ForegroundColor Red + + $RemoveAppsInput = 'c' + } + + Write-Host "" + } + } + while ($RemoveAppsInput -ne 'n' -and $RemoveAppsInput -ne '0' -and $RemoveAppsInput -ne '1' -and $RemoveAppsInput -ne '2') + + return $RemoveAppsInput +} \ No newline at end of file diff --git a/Scripts/CLI/ShowCLIDefaultModeOptions.ps1 b/Scripts/CLI/ShowCLIDefaultModeOptions.ps1 new file mode 100644 index 0000000..f7e7dd4 --- /dev/null +++ b/Scripts/CLI/ShowCLIDefaultModeOptions.ps1 @@ -0,0 +1,55 @@ +# Show CLI default mode options for removing apps, or set selection if RunDefaults or RunDefaultsLite parameter was passed +function ShowCLIDefaultModeOptions { + if ($RunDefaults) { + $RemoveAppsInput = '1' + } + elseif ($RunDefaultsLite) { + $RemoveAppsInput = '0' + } + else { + $RemoveAppsInput = ShowCLIDefaultModeAppRemovalOptions + + if ($RemoveAppsInput -eq '2' -and ($script:SelectedApps.contains('Microsoft.XboxGameOverlay') -or $script:SelectedApps.contains('Microsoft.XboxGamingOverlay')) -and + $( Read-Host -Prompt "Disable Game Bar integration and game/screen recording? This also stops ms-gamingoverlay and ms-gamebar popups (y/n)" ) -eq 'y') { + $DisableGameBarIntegrationInput = $true; + } + } + + PrintHeader 'Default Mode' + + # Add default settings based on user input + try { + # Select app removal options based on user input + switch ($RemoveAppsInput) { + '1' { + AddParameter 'RemoveApps' + AddParameter 'Apps' 'Default' + } + '2' { + AddParameter 'RemoveAppsCustom' + + if ($DisableGameBarIntegrationInput) { + AddParameter 'DisableDVR' + AddParameter 'DisableGameBarIntegration' + } + } + } + + # Load settings from DefaultSettings.json and add to params + LoadSettings -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0" + } + catch { + Write-Error "Failed to load settings from DefaultSettings.json file: $_" + AwaitKeyToExit + } + + SaveSettings + + # Skip change summary if Silent parameter was passed + if ($Silent) { + return + } + + PrintPendingChanges + PrintHeader 'Default Mode' +} \ No newline at end of file diff --git a/Scripts/CLI/ShowCLILastUsedSettings.ps1 b/Scripts/CLI/ShowCLILastUsedSettings.ps1 new file mode 100644 index 0000000..06f9022 --- /dev/null +++ b/Scripts/CLI/ShowCLILastUsedSettings.ps1 @@ -0,0 +1,16 @@ +# Shows the CLI last used settings from LastUsedSettings.json file, displays pending changes and prompts the user to apply them. +function ShowCLILastUsedSettings { + PrintHeader 'Custom Mode' + + try { + # Load settings from LastUsedSettings.json and add to params + LoadSettings -filePath $script:SavedSettingsFilePath -expectedVersion "1.0" + } + catch { + Write-Error "Failed to load settings from LastUsedSettings.json file: $_" + AwaitKeyToExit + } + + PrintPendingChanges + PrintHeader 'Custom Mode' +} \ No newline at end of file diff --git a/Scripts/CLI/ShowCLIMenuOptions.ps1 b/Scripts/CLI/ShowCLIMenuOptions.ps1 new file mode 100644 index 0000000..e978190 --- /dev/null +++ b/Scripts/CLI/ShowCLIMenuOptions.ps1 @@ -0,0 +1,30 @@ +# Shows the CLI menu options and prompts the user to select one. Loops until a valid option is selected. +function ShowCLIMenuOptions { + Do { + $ModeSelectionMessage = "Please select an option (1/2)" + + PrintHeader 'Menu' + + Write-Host "(1) Default mode: Quickly apply the recommended changes" + Write-Host "(2) App removal mode: Select & remove apps, without making other changes" + + # Only show this option if SavedSettings file exists + if (Test-Path $script:SavedSettingsFilePath) { + Write-Host "(3) Quickly apply your last used settings" + + $ModeSelectionMessage = "Please select an option (1/2/3)" + } + + Write-Host "" + Write-Host "" + + $Mode = Read-Host $ModeSelectionMessage + + if (($Mode -eq '3') -and -not (Test-Path $script:SavedSettingsFilePath)) { + $Mode = $null + } + } + while ($Mode -ne '1' -and $Mode -ne '2' -and $Mode -ne '3') + + return $Mode +} \ No newline at end of file diff --git a/Scripts/FileIO/LoadAppsDetailsFromJson.ps1 b/Scripts/FileIO/LoadAppsDetailsFromJson.ps1 new file mode 100644 index 0000000..1e297df --- /dev/null +++ b/Scripts/FileIO/LoadAppsDetailsFromJson.ps1 @@ -0,0 +1,44 @@ +# Read Apps.json and return list of app objects with optional filtering +function LoadAppsDetailsFromJson { + param ( + [switch]$OnlyInstalled, + [string]$InstalledList = "", + [switch]$InitialCheckedFromJson + ) + + $apps = @() + try { + $jsonContent = Get-Content -Path $script:AppsListFilePath -Raw | ConvertFrom-Json + } + catch { + Write-Error "Failed to read Apps.json: $_" + return $apps + } + + foreach ($appData in $jsonContent.Apps) { + $appId = $appData.AppId.Trim() + if ($appId.length -eq 0) { continue } + + if ($OnlyInstalled) { + if (-not ($InstalledList -like ("*$appId*")) -and -not (Get-AppxPackage -Name $appId)) { + continue + } + if (($appId -eq "Microsoft.Edge") -and -not ($InstalledList -like "* Microsoft.Edge *")) { + continue + } + } + + $displayName = if ($appData.FriendlyName) { "$($appData.FriendlyName) ($appId)" } else { $appId } + $isChecked = if ($InitialCheckedFromJson) { $appData.SelectedByDefault } else { $false } + + $apps += [PSCustomObject]@{ + AppId = $appId + DisplayName = $displayName + IsChecked = $isChecked + Description = $appData.Description + SelectedByDefault = $appData.SelectedByDefault + } + } + + return $apps +} diff --git a/Scripts/FileIO/LoadAppsFromFile.ps1 b/Scripts/FileIO/LoadAppsFromFile.ps1 new file mode 100644 index 0000000..a5a98f3 --- /dev/null +++ b/Scripts/FileIO/LoadAppsFromFile.ps1 @@ -0,0 +1,45 @@ +# Returns list of apps from the specified file, it trims the app names and removes any comments +function LoadAppsFromFile { + param ( + $appsFilePath + ) + + $appsList = @() + + if (-not (Test-Path $appsFilePath)) { + return $appsList + } + + try { + # Check if file is JSON or text format + if ($appsFilePath -like "*.json") { + # JSON file format + $jsonContent = Get-Content -Path $appsFilePath -Raw | ConvertFrom-Json + Foreach ($appData in $jsonContent.Apps) { + $appId = $appData.AppId.Trim() + $selectedByDefault = $appData.SelectedByDefault + if ($selectedByDefault -and $appId.length -gt 0) { + $appsList += $appId + } + } + } + else { + # Legacy text file format + Foreach ($app in (Get-Content -Path $appsFilePath | Where-Object { $_ -notmatch '^#.*' -and $_ -notmatch '^\s*$' } )) { + if (-not ($app.IndexOf('#') -eq -1)) { + $app = $app.Substring(0, $app.IndexOf('#')) + } + + $app = $app.Trim() + $appString = $app.Trim('*') + $appsList += $appString + } + } + + return $appsList + } + catch { + Write-Error "Unable to read apps list from file: $appsFilePath" + AwaitKeyToExit + } +} diff --git a/Scripts/FileIO/LoadJsonFile.ps1 b/Scripts/FileIO/LoadJsonFile.ps1 new file mode 100644 index 0000000..8af93cf --- /dev/null +++ b/Scripts/FileIO/LoadJsonFile.ps1 @@ -0,0 +1,32 @@ +# Loads a JSON file from the specified path and returns the parsed object +# Returns $null if the file doesn't exist or if parsing fails +function LoadJsonFile { + param ( + [string]$filePath, + [string]$expectedVersion = $null, + [switch]$optionalFile + ) + + if (-not (Test-Path $filePath)) { + if (-not $optionalFile) { + Write-Error "File not found: $filePath" + } + return $null + } + + try { + $jsonContent = Get-Content -Path $filePath -Raw | ConvertFrom-Json + + # Validate version if specified + if ($expectedVersion -and $jsonContent.Version -and $jsonContent.Version -ne $expectedVersion) { + Write-Error "$(Split-Path $filePath -Leaf) version mismatch (expected $expectedVersion, found $($jsonContent.Version))" + return $null + } + + return $jsonContent + } + catch { + Write-Error "Failed to parse JSON file: $filePath" + return $null + } +} diff --git a/Scripts/FileIO/LoadSettings.ps1 b/Scripts/FileIO/LoadSettings.ps1 new file mode 100644 index 0000000..9c18ac4 --- /dev/null +++ b/Scripts/FileIO/LoadSettings.ps1 @@ -0,0 +1,31 @@ +# Loads settings from a JSON file and adds them to script params +function LoadSettings { + param ( + [string]$filePath, + [string]$expectedVersion = "1.0" + ) + + $settingsJson = LoadJsonFile -filePath $filePath -expectedVersion $expectedVersion + + if (-not $settingsJson -or -not $settingsJson.Settings) { + throw "Failed to load settings from $(Split-Path $filePath -Leaf)" + } + + # Get current Windows build version + $WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' CurrentBuild + + foreach ($setting in $settingsJson.Settings) { + if ($setting.Value -eq $false) { + continue + } + + $feature = $script:Features[$setting.Name] + + # Check version and feature compatibility using Features.json + if (($feature.MinVersion -and $WinVersion -lt $feature.MinVersion) -or ($feature.MaxVersion -and $WinVersion -gt $feature.MaxVersion) -or ($feature.FeatureId -eq 'DisableModernStandbyNetworking' -and (-not $script:ModernStandbySupported))) { + continue + } + + AddParameter $setting.Name $setting.Value + } +} diff --git a/Scripts/FileIO/SaveCustomAppsListToFile.ps1 b/Scripts/FileIO/SaveCustomAppsListToFile.ps1 new file mode 100644 index 0000000..7ac8bdf --- /dev/null +++ b/Scripts/FileIO/SaveCustomAppsListToFile.ps1 @@ -0,0 +1,15 @@ +# Saves the provided appsList to the CustomAppsList file +function SaveCustomAppsListToFile { + param ( + $appsList + ) + + $script:SelectedApps = $appsList + + # Create file that stores selected apps if it doesn't exist + if (-not (Test-Path $script:CustomAppsListFilePath)) { + $null = New-Item $script:CustomAppsListFilePath -ItemType File + } + + Set-Content -Path $script:CustomAppsListFilePath -Value $script:SelectedApps +} diff --git a/Scripts/FileIO/SaveSettings.ps1 b/Scripts/FileIO/SaveSettings.ps1 new file mode 100644 index 0000000..4970ab9 --- /dev/null +++ b/Scripts/FileIO/SaveSettings.ps1 @@ -0,0 +1,26 @@ +# Saves the current settings, excluding control parameters, to 'LastUsedSettings.json' file +function SaveSettings { + $settings = @{ + "Version" = "1.0" + "Settings" = @() + } + + foreach ($param in $script:Params.Keys) { + if ($script:ControlParams -notcontains $param) { + $value = $script:Params[$param] + + $settings.Settings += @{ + "Name" = $param + "Value" = $value + } + } + } + + try { + $settings | ConvertTo-Json -Depth 10 | Set-Content $script:SavedSettingsFilePath + } + catch { + Write-Output "" + Write-Host "Error: Failed to save settings to LastUsedSettings.json file" -ForegroundColor Red + } +} \ No newline at end of file diff --git a/Scripts/FileIO/ValidateAppslist.ps1 b/Scripts/FileIO/ValidateAppslist.ps1 new file mode 100644 index 0000000..3985f62 --- /dev/null +++ b/Scripts/FileIO/ValidateAppslist.ps1 @@ -0,0 +1,24 @@ +# Returns a validated list of apps based on the provided appsList and the supported apps from Apps.json +function ValidateAppslist { + param ( + $appsList + ) + + $supportedAppsList = (LoadAppsDetailsFromJson | ForEach-Object { $_.AppId }) + $validatedAppsList = @() + + # Validate provided appsList against supportedAppsList + Foreach ($app in $appsList) { + $app = $app.Trim() + $appString = $app.Trim('*') + + if ($supportedAppsList -notcontains $appString) { + Write-Host "Removal of app '$appString' is not supported and will be skipped" -ForegroundColor Yellow + continue + } + + $validatedAppsList += $appString + } + + return $validatedAppsList +} diff --git a/Scripts/GUI/ApplySettingsToUiControls.ps1 b/Scripts/GUI/ApplySettingsToUiControls.ps1 new file mode 100644 index 0000000..0f94fd0 --- /dev/null +++ b/Scripts/GUI/ApplySettingsToUiControls.ps1 @@ -0,0 +1,80 @@ +# Applies settings from a JSON object to UI controls (checkboxes and comboboxes) +# Used by LoadDefaultsBtn and LoadLastUsedBtn in the UI +function ApplySettingsToUiControls { + param ( + $window, + $settingsJson, + $uiControlMappings + ) + + if (-not $settingsJson -or -not $settingsJson.Settings) { + return $false + } + + # First, reset all tweaks to "No Change" (index 0) or unchecked + if ($uiControlMappings) { + foreach ($comboName in $uiControlMappings.Keys) { + $control = $window.FindName($comboName) + if ($control -is [System.Windows.Controls.CheckBox]) { + $control.IsChecked = $false + } + elseif ($control -is [System.Windows.Controls.ComboBox]) { + $control.SelectedIndex = 0 + } + } + } + + # Also uncheck RestorePointCheckBox + $restorePointCheckBox = $window.FindName('RestorePointCheckBox') + if ($restorePointCheckBox) { + $restorePointCheckBox.IsChecked = $false + } + + # Apply settings from JSON + foreach ($setting in $settingsJson.Settings) { + if ($setting.Value -ne $true) { continue } + $paramName = $setting.Name + + # Handle RestorePointCheckBox separately + if ($paramName -eq 'CreateRestorePoint') { + if ($restorePointCheckBox) { $restorePointCheckBox.IsChecked = $true } + continue + } + + if ($uiControlMappings) { + foreach ($comboName in $uiControlMappings.Keys) { + $mapping = $uiControlMappings[$comboName] + if ($mapping.Type -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]) { + $control.SelectedIndex = $i + } + } + break + } + $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]) { + $control.IsChecked = $true + } + elseif ($control -is [System.Windows.Controls.ComboBox]) { + $control.SelectedIndex = 1 + } + } + } + } + } + } + } + + return $true +} diff --git a/Scripts/GUI/AttachShiftClickBehavior.ps1 b/Scripts/GUI/AttachShiftClickBehavior.ps1 new file mode 100644 index 0000000..4ec834f --- /dev/null +++ b/Scripts/GUI/AttachShiftClickBehavior.ps1 @@ -0,0 +1,71 @@ +# Attaches shift-click selection behavior to a checkbox in an apps panel +# Parameters: +# - $checkbox: The checkbox to attach the behavior to +# - $appsPanel: The StackPanel containing checkbox items +# - $lastSelectedCheckboxRef: A reference to a variable storing the last clicked checkbox +# - $updateStatusCallback: Optional callback to update selection status +function AttachShiftClickBehavior { + param ( + [System.Windows.Controls.CheckBox]$checkbox, + [System.Windows.Controls.StackPanel]$appsPanel, + [ref]$lastSelectedCheckboxRef, + [scriptblock]$updateStatusCallback = $null + ) + + # Use a closure to capture the parameters + $checkbox.Add_PreviewMouseLeftButtonDown({ + param( + $sender, + $e + ) + + $isShiftPressed = [System.Windows.Input.Keyboard]::IsKeyDown([System.Windows.Input.Key]::LeftShift) -or + [System.Windows.Input.Keyboard]::IsKeyDown([System.Windows.Input.Key]::RightShift) + + if ($isShiftPressed -and $null -ne $lastSelectedCheckboxRef.Value) { + # Get all visible checkboxes in the panel + $visibleCheckboxes = @() + foreach ($child in $appsPanel.Children) { + if ($child -is [System.Windows.Controls.CheckBox] -and $child.Visibility -eq 'Visible') { + $visibleCheckboxes += $child + } + } + + # Find indices of the last selected and current checkbox + $lastIndex = -1 + $currentIndex = -1 + + for ($i = 0; $i -lt $visibleCheckboxes.Count; $i++) { + if ($visibleCheckboxes[$i] -eq $lastSelectedCheckboxRef.Value) { + $lastIndex = $i + } + if ($visibleCheckboxes[$i] -eq $sender) { + $currentIndex = $i + } + } + + if ($lastIndex -ge 0 -and $currentIndex -ge 0 -and $lastIndex -ne $currentIndex) { + $startIndex = [Math]::Min($lastIndex, $currentIndex) + $endIndex = [Math]::Max($lastIndex, $currentIndex) + + $shouldDeselect = $sender.IsChecked + + # Set all checkboxes in the range to the appropriate state + for ($i = $startIndex; $i -le $endIndex; $i++) { + $visibleCheckboxes[$i].IsChecked = -not $shouldDeselect + } + + if ($updateStatusCallback) { + & $updateStatusCallback + } + + # Mark the event as handled to prevent the default toggle behavior + $e.Handled = $true + return + } + } + + # Update the last selected checkbox reference for next time + $lastSelectedCheckboxRef.Value = $sender + }.GetNewClosure()) +} diff --git a/Scripts/GUI/GetSystemUsesDarkMode.ps1 b/Scripts/GUI/GetSystemUsesDarkMode.ps1 new file mode 100644 index 0000000..5f4263b --- /dev/null +++ b/Scripts/GUI/GetSystemUsesDarkMode.ps1 @@ -0,0 +1,9 @@ +# Checks if the system is set to use dark mode for apps +function GetSystemUsesDarkMode { + try { + return (Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize' -Name 'AppsUseLightTheme').AppsUseLightTheme -eq 0 + } + catch { + return $false + } +} diff --git a/Scripts/GUI/SetWindowThemeResources.ps1 b/Scripts/GUI/SetWindowThemeResources.ps1 new file mode 100644 index 0000000..9bb87aa --- /dev/null +++ b/Scripts/GUI/SetWindowThemeResources.ps1 @@ -0,0 +1,69 @@ +# Sets resource colors for a WPF window based on dark mode preference +function SetWindowThemeResources { + param ( + $window, + [bool]$usesDarkMode + ) + + if ($usesDarkMode) { + $window.Resources.Add("BgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#202020"))) + $window.Resources.Add("FgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFFFFF"))) + $window.Resources.Add("CardBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#2b2b2b"))) + $window.Resources.Add("BorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#404040"))) + $window.Resources.Add("ButtonBorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#404040"))) + $window.Resources.Add("CheckBoxBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#272727"))) + $window.Resources.Add("CheckBoxBorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#808080"))) + $window.Resources.Add("CheckBoxHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#343434"))) + $window.Resources.Add("ComboBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#373737"))) + $window.Resources.Add("ComboHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#434343"))) + $window.Resources.Add("ComboItemBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#2c2c2c"))) + $window.Resources.Add("ComboItemHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#383838"))) + $window.Resources.Add("ComboItemSelectedColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#343434"))) + $window.Resources.Add("AccentColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFD700"))) + $window.Resources.Add("ButtonDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#434343"))) + $window.Resources.Add("ButtonTextDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#989898"))) + $window.Resources.Add("SecondaryButtonBg", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#393939"))) + $window.Resources.Add("SecondaryButtonHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#2a2a2a"))) + $window.Resources.Add("SecondaryButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#1e1e1e"))) + $window.Resources.Add("SecondaryButtonDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#3b3b3b"))) + $window.Resources.Add("SecondaryButtonTextDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#787878"))) + $window.Resources.Add("InputFocusColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#1f1f1f"))) + $window.Resources.Add("ScrollBarThumbColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#3d3d3d"))) + $window.Resources.Add("ScrollBarThumbHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#4b4b4b"))) + } + else { + $window.Resources.Add("BgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f3f3f3"))) + $window.Resources.Add("FgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#000000"))) + $window.Resources.Add("CardBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#fbfbfb"))) + $window.Resources.Add("BorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#ededed"))) + $window.Resources.Add("ButtonBorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#d3d3d3"))) + $window.Resources.Add("CheckBoxBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f5f5f5"))) + $window.Resources.Add("CheckBoxBorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#898989"))) + $window.Resources.Add("CheckBoxHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#ececec"))) + $window.Resources.Add("ComboBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFFFFF"))) + $window.Resources.Add("ComboHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f8f8f8"))) + $window.Resources.Add("ComboItemBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f9f9f9"))) + $window.Resources.Add("ComboItemHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f0f0f0"))) + $window.Resources.Add("ComboItemSelectedColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f3f3f3"))) + $window.Resources.Add("AccentColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#ffae00"))) + $window.Resources.Add("ButtonDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#bfbfbf"))) + $window.Resources.Add("ButtonTextDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#ffffff"))) + $window.Resources.Add("SecondaryButtonBg", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#fbfbfb"))) + $window.Resources.Add("SecondaryButtonHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f6f6f6"))) + $window.Resources.Add("SecondaryButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f0f0f0"))) + $window.Resources.Add("SecondaryButtonDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f7f7f7"))) + $window.Resources.Add("SecondaryButtonTextDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#b7b7b7"))) + $window.Resources.Add("InputFocusColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#fbfbfb"))) + $window.Resources.Add("ScrollBarThumbColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#b9b9b9"))) + $window.Resources.Add("ScrollBarThumbHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#8b8b8b"))) + } + + $window.Resources.Add("ButtonBg", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0067c0"))) + $window.Resources.Add("ButtonHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#1E88E5"))) + $window.Resources.Add("ButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#3284cc"))) + $window.Resources.Add("CloseHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#c42b1c"))) + $window.Resources.Add("InformationIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0078D4"))) + $window.Resources.Add("WarningIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFB900"))) + $window.Resources.Add("ErrorIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#E81123"))) + $window.Resources.Add("QuestionIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0078D4"))) +} diff --git a/Scripts/GUI/Show-AboutDialog.ps1 b/Scripts/GUI/Show-AboutDialog.ps1 new file mode 100644 index 0000000..71d5ce2 --- /dev/null +++ b/Scripts/GUI/Show-AboutDialog.ps1 @@ -0,0 +1,95 @@ +function Show-AboutDialog { + param ( + [Parameter(Mandatory=$false)] + [System.Windows.Window]$Owner = $null + ) + + Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null + + $usesDarkMode = GetSystemUsesDarkMode + + # Determine owner window + $ownerWindow = if ($Owner) { $Owner } else { $script:GuiWindow } + + # Show overlay if owner window exists + $overlay = $null + if ($ownerWindow) { + try { + $overlay = $ownerWindow.FindName('ModalOverlay') + if ($overlay) { + $ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Visible' }) + } + } + catch { } + } + + # Load XAML from file + $xaml = Get-Content -Path $script:AboutWindowSchema -Raw + $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml)) + try { + $aboutWindow = [System.Windows.Markup.XamlReader]::Load($reader) + } + finally { + $reader.Close() + } + + # Set owner to owner window if it exists + if ($ownerWindow) { + try { + $aboutWindow.Owner = $ownerWindow + } + catch { } + } + + # Apply theme resources + SetWindowThemeResources -window $aboutWindow -usesDarkMode $usesDarkMode + + # Get UI elements + $titleBar = $aboutWindow.FindName('TitleBar') + $versionText = $aboutWindow.FindName('VersionText') + $projectLink = $aboutWindow.FindName('ProjectLink') + $kofiLink = $aboutWindow.FindName('KofiLink') + $closeButton = $aboutWindow.FindName('CloseButton') + + # Set version + $versionText.Text = $script:Version + + # Title bar drag to move window + $titleBar.Add_MouseLeftButtonDown({ + $aboutWindow.DragMove() + }) + + # Project link click handler + $projectLink.Add_MouseLeftButtonDown({ + Start-Process "https://github.com/Raphire/Win11Debloat" + }) + + # Ko-fi link click handler + $kofiLink.Add_MouseLeftButtonDown({ + Start-Process "https://ko-fi.com/raphire" + }) + + # Close button handler + $closeButton.Add_Click({ + $aboutWindow.Close() + }) + + # Handle Escape key to close + $aboutWindow.Add_KeyDown({ + param($sender, $e) + if ($e.Key -eq 'Escape') { + $aboutWindow.Close() + } + }) + + # Show dialog + $aboutWindow.ShowDialog() | Out-Null + + # Hide overlay after dialog closes + if ($overlay) { + try { + $ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' }) + } + catch { } + } +} \ No newline at end of file diff --git a/Scripts/GUI/Show-AppSelectionWindow.ps1 b/Scripts/GUI/Show-AppSelectionWindow.ps1 new file mode 100644 index 0000000..f10882a --- /dev/null +++ b/Scripts/GUI/Show-AppSelectionWindow.ps1 @@ -0,0 +1,161 @@ +# Shows application selection window that allows the user to select what apps they want to remove or keep +function Show-AppSelectionWindow { + Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null + + $usesDarkMode = GetSystemUsesDarkMode + + # Show overlay if main window exists + $overlay = $null + if ($script:GuiWindow) { + try { + $overlay = $script:GuiWindow.FindName('ModalOverlay') + if ($overlay) { + $script:GuiWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Visible' }) + } + } + catch { } + } + + # Load XAML from file + $xaml = Get-Content -Path $script:AppSelectionSchema -Raw + $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml)) + try { + $window = [System.Windows.Markup.XamlReader]::Load($reader) + } + finally { + $reader.Close() + } + + # Set owner to main window if it exists + if ($script:GuiWindow) { + try { + $window.Owner = $script:GuiWindow + } + catch { } + } + + SetWindowThemeResources -window $window -usesDarkMode $usesDarkMode + + $appsPanel = $window.FindName('AppsPanel') + $checkAllBox = $window.FindName('CheckAllBox') + $onlyInstalledBox = $window.FindName('OnlyInstalledBox') + $confirmBtn = $window.FindName('ConfirmBtn') + $loadingIndicator = $window.FindName('LoadingAppsIndicator') + $titleBar = $window.FindName('TitleBar') + + # Track the last selected checkbox for shift-click range selection + $script:AppSelectionWindowLastSelectedCheckbox = $null + + # Loads apps into the apps UI + function LoadApps { + # Show loading indicator + $loadingIndicator.Visibility = 'Visible' + $window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{}) + + $appsPanel.Children.Clear() + $listOfApps = "" + + if ($onlyInstalledBox.IsChecked -and ($script:WingetInstalled -eq $true)) { + # Attempt to get a list of installed apps via WinGet, times out after 10 seconds + $listOfApps = GetInstalledAppsViaWinget -TimeOut 10 + if (-not $listOfApps) { + # Show error that the script was unable to get list of apps from WinGet + Show-MessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' -Owner $window | Out-Null + $onlyInstalledBox.IsChecked = $false + } + } + + $appsToAdd = LoadAppsDetailsFromJson -OnlyInstalled:$onlyInstalledBox.IsChecked -InstalledList $listOfApps -InitialCheckedFromJson:$true + + # Reset the last selected checkbox when loading a new list + $script:AppSelectionWindowLastSelectedCheckbox = $null + + # Sort apps alphabetically and add to panel + $appsToAdd | Sort-Object -Property DisplayName | ForEach-Object { + $checkbox = New-Object System.Windows.Controls.CheckBox + $checkbox.Content = $_.DisplayName + $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $_.DisplayName) + $checkbox.Tag = $_.AppId + $checkbox.IsChecked = $_.IsChecked + $checkbox.ToolTip = $_.Description + $checkbox.Style = $window.Resources["AppsPanelCheckBoxStyle"] + + # Attach shift-click behavior for range selection + AttachShiftClickBehavior -checkbox $checkbox -appsPanel $appsPanel -lastSelectedCheckboxRef ([ref]$script:AppSelectionWindowLastSelectedCheckbox) + + $appsPanel.Children.Add($checkbox) | Out-Null + } + + # Hide loading indicator + $loadingIndicator.Visibility = 'Collapsed' + } + + # Event handlers + $titleBar.Add_MouseLeftButtonDown({ + $window.DragMove() + }) + + $checkAllBox.Add_Checked({ + foreach ($child in $appsPanel.Children) { + if ($child -is [System.Windows.Controls.CheckBox]) { + $child.IsChecked = $true + } + } + }) + + $checkAllBox.Add_Unchecked({ + foreach ($child in $appsPanel.Children) { + if ($child -is [System.Windows.Controls.CheckBox]) { + $child.IsChecked = $false + } + } + }) + + $onlyInstalledBox.Add_Checked({ LoadApps }) + $onlyInstalledBox.Add_Unchecked({ LoadApps }) + + $confirmBtn.Add_Click({ + $selectedApps = @() + foreach ($child in $appsPanel.Children) { + if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) { + $selectedApps += $child.Tag + } + } + + # Close form without saving if no apps were selected + if ($selectedApps.Count -eq 0) { + $window.Close() + return + } + + if ($selectedApps -contains "Microsoft.WindowsStore" -and -not $Silent) { + $result = Show-MessageBox -Message 'Are you sure you wish to uninstall the Microsoft Store? This app cannot easily be reinstalled.' -Title 'Are you sure?' -Button 'YesNo' -Icon 'Warning' -Owner $window + + if ($result -eq 'No') { + return + } + } + + SaveCustomAppsListToFile -appsList $selectedApps + + $window.DialogResult = $true + }) + + # Load apps after window is shown (allows UI to render first) + $window.Add_ContentRendered({ + $window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ LoadApps }) + }) + + # Show the window and return dialog result + $result = $window.ShowDialog() + + # Hide overlay after dialog closes + if ($overlay) { + try { + $script:GuiWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' }) + } + catch { } + } + + return $result +} diff --git a/Scripts/GUI/Show-MainWindow.ps1 b/Scripts/GUI/Show-MainWindow.ps1 new file mode 100644 index 0000000..c90e532 --- /dev/null +++ b/Scripts/GUI/Show-MainWindow.ps1 @@ -0,0 +1,1501 @@ +function Show-MainWindow { + Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase,System.Windows.Forms | Out-Null + + # Get current Windows build version + $WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' CurrentBuild + + $usesDarkMode = GetSystemUsesDarkMode + + # Load XAML from file + $xaml = Get-Content -Path $script:MainWindowSchema -Raw + $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml)) + try { + $window = [System.Windows.Markup.XamlReader]::Load($reader) + } + finally { + $reader.Close() + } + + SetWindowThemeResources -window $window -usesDarkMode $usesDarkMode + + # Get named elements + $titleBar = $window.FindName('TitleBar') + $kofiBtn = $window.FindName('KofiBtn') + $menuBtn = $window.FindName('MenuBtn') + $closeBtn = $window.FindName('CloseBtn') + $menuDocumentation = $window.FindName('MenuDocumentation') + $menuReportBug = $window.FindName('MenuReportBug') + $menuLogs = $window.FindName('MenuLogs') + $menuAbout = $window.FindName('MenuAbout') + + # Title bar event handlers + $titleBar.Add_MouseLeftButtonDown({ + if ($_.OriginalSource -is [System.Windows.Controls.Grid] -or $_.OriginalSource -is [System.Windows.Controls.Border] -or $_.OriginalSource -is [System.Windows.Controls.TextBlock]) { + $window.DragMove() + } + }) + + $kofiBtn.Add_Click({ + Start-Process "https://ko-fi.com/raphire" + }) + + $menuBtn.Add_Click({ + $menuBtn.ContextMenu.PlacementTarget = $menuBtn + $menuBtn.ContextMenu.Placement = [System.Windows.Controls.Primitives.PlacementMode]::Bottom + $menuBtn.ContextMenu.IsOpen = $true + }) + + $menuDocumentation.Add_Click({ + Start-Process "https://github.com/Raphire/Win11Debloat/wiki" + }) + + $menuReportBug.Add_Click({ + Start-Process "https://github.com/Raphire/Win11Debloat/issues" + }) + + $menuLogs.Add_Click({ + $logsFolder = Join-Path $PSScriptRoot "../../Logs" + if (Test-Path $logsFolder) { + Start-Process "explorer.exe" -ArgumentList $logsFolder + } + else { + Show-MessageBox -Message "No logs folder found at: $logsFolder" -Title "Logs" -Button 'OK' -Icon 'Information' + } + }) + + $menuAbout.Add_Click({ + Show-AboutDialog -Owner $window + }) + + $closeBtn.Add_Click({ + $window.Close() + }) + + # Ensure closing the main window stops all execution + $window.Add_Closing({ + $script:CancelRequested = $true + }) + + # Implement window resize functionality + $resizeLeft = $window.FindName('ResizeLeft') + $resizeRight = $window.FindName('ResizeRight') + $resizeTop = $window.FindName('ResizeTop') + $resizeBottom = $window.FindName('ResizeBottom') + $resizeTopLeft = $window.FindName('ResizeTopLeft') + $resizeTopRight = $window.FindName('ResizeTopRight') + $resizeBottomLeft = $window.FindName('ResizeBottomLeft') + $resizeBottomRight = $window.FindName('ResizeBottomRight') + + $script:resizing = $false + $script:resizeEdges = $null + $script:resizeStart = $null + $script:windowStart = $null + $script:resizeElement = $null + + $resizeHandler = { + param($sender, $e) + + $script:resizing = $true + $script:resizeElement = $sender + $script:resizeStart = [System.Windows.Forms.Cursor]::Position + $script:windowStart = @{ + Left = $window.Left + Top = $window.Top + Width = $window.ActualWidth + Height = $window.ActualHeight + } + + # Parse direction tag into edge flags for cleaner resize logic + $direction = $sender.Tag + $script:resizeEdges = @{ + Left = $direction -match 'Left' + Right = $direction -match 'Right' + Top = $direction -match 'Top' + Bottom = $direction -match 'Bottom' + } + + $sender.CaptureMouse() + $e.Handled = $true + } + + $moveHandler = { + param($sender, $e) + if (-not $script:resizing) { return } + + $current = [System.Windows.Forms.Cursor]::Position + $deltaX = $current.X - $script:resizeStart.X + $deltaY = $current.Y - $script:resizeStart.Y + + # Handle horizontal resize + if ($script:resizeEdges.Left) { + $newWidth = [Math]::Max($window.MinWidth, $script:windowStart.Width - $deltaX) + if ($newWidth -ne $window.Width) { + $window.Left = $script:windowStart.Left + ($script:windowStart.Width - $newWidth) + $window.Width = $newWidth + } + } + elseif ($script:resizeEdges.Right) { + $window.Width = [Math]::Max($window.MinWidth, $script:windowStart.Width + $deltaX) + } + + # Handle vertical resize + if ($script:resizeEdges.Top) { + $newHeight = [Math]::Max($window.MinHeight, $script:windowStart.Height - $deltaY) + if ($newHeight -ne $window.Height) { + $window.Top = $script:windowStart.Top + ($script:windowStart.Height - $newHeight) + $window.Height = $newHeight + } + } + elseif ($script:resizeEdges.Bottom) { + $window.Height = [Math]::Max($window.MinHeight, $script:windowStart.Height + $deltaY) + } + + $e.Handled = $true + } + + $releaseHandler = { + param($sender, $e) + if ($script:resizing -and $script:resizeElement) { + $script:resizing = $false + $script:resizeEdges = $null + $script:resizeElement.ReleaseMouseCapture() + $script:resizeElement = $null + $e.Handled = $true + } + } + + # Set tags and add event handlers for resize borders + $resizeLeft.Tag = 'Left' + $resizeLeft.Add_PreviewMouseLeftButtonDown($resizeHandler) + $resizeLeft.Add_MouseMove($moveHandler) + $resizeLeft.Add_MouseLeftButtonUp($releaseHandler) + + $resizeRight.Tag = 'Right' + $resizeRight.Add_PreviewMouseLeftButtonDown($resizeHandler) + $resizeRight.Add_MouseMove($moveHandler) + $resizeRight.Add_MouseLeftButtonUp($releaseHandler) + + $resizeTop.Tag = 'Top' + $resizeTop.Add_PreviewMouseLeftButtonDown($resizeHandler) + $resizeTop.Add_MouseMove($moveHandler) + $resizeTop.Add_MouseLeftButtonUp($releaseHandler) + + $resizeBottom.Tag = 'Bottom' + $resizeBottom.Add_PreviewMouseLeftButtonDown($resizeHandler) + $resizeBottom.Add_MouseMove($moveHandler) + $resizeBottom.Add_MouseLeftButtonUp($releaseHandler) + + $resizeTopLeft.Tag = 'TopLeft' + $resizeTopLeft.Add_PreviewMouseLeftButtonDown($resizeHandler) + $resizeTopLeft.Add_MouseMove($moveHandler) + $resizeTopLeft.Add_MouseLeftButtonUp($releaseHandler) + + $resizeTopRight.Tag = 'TopRight' + $resizeTopRight.Add_PreviewMouseLeftButtonDown($resizeHandler) + $resizeTopRight.Add_MouseMove($moveHandler) + $resizeTopRight.Add_MouseLeftButtonUp($releaseHandler) + + $resizeBottomLeft.Tag = 'BottomLeft' + $resizeBottomLeft.Add_PreviewMouseLeftButtonDown($resizeHandler) + $resizeBottomLeft.Add_MouseMove($moveHandler) + $resizeBottomLeft.Add_MouseLeftButtonUp($releaseHandler) + + $resizeBottomRight.Tag = 'BottomRight' + $resizeBottomRight.Add_PreviewMouseLeftButtonDown($resizeHandler) + $resizeBottomRight.Add_MouseMove($moveHandler) + $resizeBottomRight.Add_MouseLeftButtonUp($releaseHandler) + + # Integrated App Selection UI + $appsPanel = $window.FindName('AppSelectionPanel') + $onlyInstalledAppsBox = $window.FindName('OnlyInstalledAppsBox') + $loadingAppsIndicator = $window.FindName('LoadingAppsIndicator') + $appSelectionStatus = $window.FindName('AppSelectionStatus') + $defaultAppsBtn = $window.FindName('DefaultAppsBtn') + $clearAppSelectionBtn = $window.FindName('ClearAppSelectionBtn') + + # Track the last selected checkbox for shift-click range selection + $script:MainWindowLastSelectedCheckbox = $null + + # Track current app loading operation to prevent race conditions + $script:CurrentAppLoadTimer = $null + $script:CurrentAppLoadJob = $null + $script:CurrentAppLoadJobStartTime = $null + + # Apply Tab UI Elements + $consoleOutput = $window.FindName('ConsoleOutput') + $consoleScrollViewer = $window.FindName('ConsoleScrollViewer') + $finishBtn = $window.FindName('FinishBtn') + $finishBtnText = $window.FindName('FinishBtnText') + + # Set script-level variables for Write-ToConsole function + $script:GuiConsoleOutput = $consoleOutput + $script:GuiConsoleScrollViewer = $consoleScrollViewer + $script:GuiWindow = $window + + # Updates app selection status text in the App Selection tab + function UpdateAppSelectionStatus { + $selectedCount = 0 + foreach ($child in $appsPanel.Children) { + if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) { + $selectedCount++ + } + } + $appSelectionStatus.Text = "$selectedCount app(s) selected for removal" + } + + # Dynamically builds Tweaks UI from Features.json + function BuildDynamicTweaks { + $featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0" + + if (-not $featuresJson) { + Show-MessageBox -Message "Unable to load Features.json file!" -Title "Error" -Button 'OK' -Icon 'Error' | Out-Null + Exit + } + + # Column containers + $col0 = $window.FindName('Column0Panel') + $col1 = $window.FindName('Column1Panel') + $col2 = $window.FindName('Column2Panel') + $columns = @($col0, $col1, $col2) | Where-Object { $_ -ne $null } + + # Clear all columns for fully dynamic panel creation + foreach ($col in $columns) { + if ($col) { $col.Children.Clear() } + } + + $script:UiControlMappings = @{} + $script:CategoryCardMap = @{} + + function CreateLabeledCombo($parent, $labelText, $comboName, $items) { + # If only 2 items (No Change + one option), use a checkbox instead + if ($items.Count -eq 2) { + $checkbox = New-Object System.Windows.Controls.CheckBox + $checkbox.Content = $labelText + $checkbox.Name = $comboName + $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $labelText) + $checkbox.IsChecked = $false + $checkbox.Style = $window.Resources["FeatureCheckboxStyle"] + $parent.Children.Add($checkbox) | Out-Null + + # Register the checkbox with the window's name scope + try { + [System.Windows.NameScope]::SetNameScope($checkbox, [System.Windows.NameScope]::GetNameScope($window)) + $window.RegisterName($comboName, $checkbox) + } + catch { + # Name might already be registered, ignore + } + + return $checkbox + } + + # Otherwise use a combobox for multiple options + # Wrap label in a Border for search highlighting + $lblBorder = New-Object System.Windows.Controls.Border + $lblBorder.Style = $window.Resources['LabelBorderStyle'] + $lblBorderName = "$comboName`_LabelBorder" + $lblBorder.Name = $lblBorderName + + $lbl = New-Object System.Windows.Controls.TextBlock + $lbl.Text = $labelText + $lbl.Style = $window.Resources['LabelStyle'] + $labelName = "$comboName`_Label" + $lbl.Name = $labelName + + $lblBorder.Child = $lbl + $parent.Children.Add($lblBorder) | Out-Null + + # Register the label border with the window's name scope + try { + [System.Windows.NameScope]::SetNameScope($lblBorder, [System.Windows.NameScope]::GetNameScope($window)) + $window.RegisterName($lblBorderName, $lblBorder) + } + catch { + # Name might already be registered, ignore + } + + $combo = New-Object System.Windows.Controls.ComboBox + $combo.Name = $comboName + $combo.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $labelText) + foreach ($it in $items) { $cbItem = New-Object System.Windows.Controls.ComboBoxItem; $cbItem.Content = $it; $combo.Items.Add($cbItem) | Out-Null } + $combo.SelectedIndex = 0 + $parent.Children.Add($combo) | Out-Null + + # Register the combo box with the window's name scope + try { + [System.Windows.NameScope]::SetNameScope($combo, [System.Windows.NameScope]::GetNameScope($window)) + $window.RegisterName($comboName, $combo) + } + catch { + # Name might already be registered, ignore + } + + return $combo + } + + function GetWikiUrlForCategory($category) { + if (-not $category) { return 'https://github.com/Raphire/Win11Debloat/wiki/Features' } + + $slug = $category.ToLowerInvariant() + $slug = $slug -replace '&', '' + $slug = $slug -replace '[^a-z0-9\s-]', '' + $slug = $slug -replace '\s', '-' + + return "https://github.com/Raphire/Win11Debloat/wiki/Features#$slug" + } + + function GetOrCreateCategoryCard($categoryObj) { + $categoryName = $categoryObj.Name + $categoryIcon = $categoryObj.Icon + + if ($script:CategoryCardMap.ContainsKey($categoryName)) { return $script:CategoryCardMap[$categoryName] } + + # Create a new card Border + StackPanel and add to shortest column + $target = $columns | Sort-Object @{Expression={$_.Children.Count}; Ascending=$true}, @{Expression={$columns.IndexOf($_)}; Ascending=$true} | Select-Object -First 1 + + $border = New-Object System.Windows.Controls.Border + $border.Style = $window.Resources['CategoryCardBorderStyle'] + $border.Tag = 'DynamicCategory' + + $panel = New-Object System.Windows.Controls.StackPanel + $safe = ($categoryName -replace '[^a-zA-Z0-9_]','_') + $panel.Name = "Category_{0}_Panel" -f $safe + + $headerRow = New-Object System.Windows.Controls.StackPanel + $headerRow.Orientation = 'Horizontal' + + # Add category icon + $icon = New-Object System.Windows.Controls.TextBlock + # Convert HTML entity to character (e.g.,  -> actual character) + if ($categoryIcon -match '&#x([0-9A-Fa-f]+);') { + $hexValue = [Convert]::ToInt32($matches[1], 16) + $icon.Text = [char]$hexValue + } + $icon.Style = $window.Resources['CategoryHeaderIcon'] + $headerRow.Children.Add($icon) | Out-Null + + $header = New-Object System.Windows.Controls.TextBlock + $header.Text = $categoryName + $header.Style = $window.Resources['CategoryHeaderTextBlock'] + $headerRow.Children.Add($header) | Out-Null + + $helpIcon = New-Object System.Windows.Controls.TextBlock + $helpIcon.Text = '(?)' + $helpIcon.Style = $window.Resources['CategoryHelpLinkTextStyle'] + + $helpBtn = New-Object System.Windows.Controls.Button + $helpBtn.Content = $helpIcon + $helpBtn.ToolTip = "Open wiki for more info on '$categoryName' tweaks" + $helpBtn.Tag = (GetWikiUrlForCategory -category $categoryName) + $helpBtn.Style = $window.Resources['CategoryHelpLinkButtonStyle'] + $helpBtn.Add_Click({ + param($sender, $e) + if ($sender.Tag) { Start-Process $sender.Tag } + }) + $headerRow.Children.Add($helpBtn) | Out-Null + + $panel.Children.Add($headerRow) | Out-Null + + $border.Child = $panel + $target.Children.Add($border) | Out-Null + + $script:CategoryCardMap[$categoryName] = $panel + return $panel + } + + # Determine categories present (from lists and features) + $categoriesPresent = @{} + if ($featuresJson.UiGroups) { + foreach ($g in $featuresJson.UiGroups) { if ($g.Category) { $categoriesPresent[$g.Category] = $true } } + } + foreach ($f in $featuresJson.Features) { if ($f.Category) { $categoriesPresent[$f.Category] = $true } } + + # Create cards in the order defined in Features.json Categories (if present) + $orderedCategories = @() + if ($featuresJson.Categories) { + foreach ($c in $featuresJson.Categories) { + $categoryName = if ($c -is [string]) { $c } else { $c.Name } + if ($categoriesPresent.ContainsKey($categoryName)) { + # Store the full category object (or create one with default icon for string categories) + $categoryObj = if ($c -is [string]) { @{Name = $c; Icon = ''} } else { $c } + $orderedCategories += $categoryObj + } + } + } else { + # For backward compatibility, create category objects from keys + foreach ($catName in $categoriesPresent.Keys) { + $orderedCategories += @{Name = $catName; Icon = ''} + } + } + + foreach ($categoryObj in $orderedCategories) { + $categoryName = $categoryObj.Name + + # Create/get card for this category + $panel = GetOrCreateCategoryCard -categoryObj $categoryObj + if (-not $panel) { continue } + + # Collect groups and features for this category, then sort by priority + $categoryItems = @() + + # Add any groups for this category + if ($featuresJson.UiGroups) { + $groupIndex = 0 + foreach ($group in $featuresJson.UiGroups) { + if ($group.Category -ne $categoryName) { $groupIndex++; continue } + $categoryItems += [PSCustomObject]@{ + Type = 'group' + Data = $group + Priority = if ($null -ne $group.Priority) { $group.Priority } else { [int]::MaxValue } + OriginalIndex = $groupIndex + } + $groupIndex++ + } + } + + # Add individual features for this category + $featureIndex = 0 + foreach ($feature in $featuresJson.Features) { + if ($feature.Category -ne $categoryName) { $featureIndex++; continue } + + # Check version and feature compatibility using Features.json + if (($feature.MinVersion -and $WinVersion -lt $feature.MinVersion) -or ($feature.MaxVersion -and $WinVersion -gt $feature.MaxVersion) -or ($feature.FeatureId -eq 'DisableModernStandbyNetworking' -and (-not $script:ModernStandbySupported))) { + $featureIndex++; continue + } + + # Skip if feature part of a group + $inGroup = $false + if ($featuresJson.UiGroups) { + foreach ($g in $featuresJson.UiGroups) { foreach ($val in $g.Values) { if ($val.FeatureIds -contains $feature.FeatureId) { $inGroup = $true; break } }; if ($inGroup) { break } } + } + if ($inGroup) { $featureIndex++; continue } + + $categoryItems += [PSCustomObject]@{ + Type = 'feature' + Data = $feature + Priority = if ($null -ne $feature.Priority) { $feature.Priority } else { [int]::MaxValue } + OriginalIndex = $featureIndex + } + $featureIndex++ + } + + # Sort by priority first, then by original index for items with same/no priority + $sortedItems = $categoryItems | Sort-Object -Property Priority, OriginalIndex + + # Render sorted items + foreach ($item in $sortedItems) { + if ($item.Type -eq 'group') { + $group = $item.Data + $items = @('No Change') + ($group.Values | ForEach-Object { $_.Label }) + $comboName = 'Group_{0}Combo' -f $group.GroupId + $combo = CreateLabeledCombo -parent $panel -labelText $group.Label -comboName $comboName -items $items + # attach tooltip from UiGroups if present + if ($group.ToolTip) { + $tipBlock = New-Object System.Windows.Controls.TextBlock + $tipBlock.Text = $group.ToolTip + $tipBlock.TextWrapping = 'Wrap' + $tipBlock.MaxWidth = 420 + $combo.ToolTip = $tipBlock + $lblBorderObj = $null + try { $lblBorderObj = $window.FindName("$comboName`_LabelBorder") } catch {} + if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock } + } + $script:UiControlMappings[$comboName] = @{ Type='group'; Values = $group.Values; Label = $group.Label } + } + elseif ($item.Type -eq 'feature') { + $feature = $item.Data + $opt = 'Apply' + if ($feature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($feature.FeatureId -match '^Enable') { $opt = 'Enable' } + $items = @('No Change', $opt) + $comboName = ("Feature_{0}_Combo" -f $feature.FeatureId) -replace '[^a-zA-Z0-9_]','' + $combo = CreateLabeledCombo -parent $panel -labelText ($feature.Action + ' ' + $feature.Label) -comboName $comboName -items $items + # attach tooltip from Features.json if present + if ($feature.ToolTip) { + $tipBlock = New-Object System.Windows.Controls.TextBlock + $tipBlock.Text = $feature.ToolTip + $tipBlock.TextWrapping = 'Wrap' + $tipBlock.MaxWidth = 420 + $combo.ToolTip = $tipBlock + $lblBorderObj = $null + try { $lblBorderObj = $window.FindName("$comboName`_LabelBorder") } catch {} + if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock } + } + $script:UiControlMappings[$comboName] = @{ Type='feature'; FeatureId = $feature.FeatureId; Action = $feature.Action } + } + } + } + } + + # Helper function to complete app loading with the WinGet list + function script:LoadAppsWithList($listOfApps) { + $appsToAdd = LoadAppsDetailsFromJson -OnlyInstalled:$onlyInstalledAppsBox.IsChecked -InstalledList $listOfApps -InitialCheckedFromJson:$false + + # Reset the last selected checkbox when loading a new list + $script:MainWindowLastSelectedCheckbox = $null + + # Sort apps alphabetically and add to panel + $appsToAdd | Sort-Object -Property DisplayName | ForEach-Object { + $checkbox = New-Object System.Windows.Controls.CheckBox + $checkbox.Content = $_.DisplayName + $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $_.DisplayName) + $checkbox.Tag = $_.AppId + $checkbox.IsChecked = $_.IsChecked + $checkbox.ToolTip = $_.Description + $checkbox.Style = $window.Resources["AppsPanelCheckBoxStyle"] + + # Store metadata in checkbox for later use + Add-Member -InputObject $checkbox -MemberType NoteProperty -Name "SelectedByDefault" -Value $_.SelectedByDefault + + # Add event handler to update status + $checkbox.Add_Checked({ UpdateAppSelectionStatus }) + $checkbox.Add_Unchecked({ UpdateAppSelectionStatus }) + + # Attach shift-click behavior for range selection + AttachShiftClickBehavior -checkbox $checkbox -appsPanel $appsPanel -lastSelectedCheckboxRef ([ref]$script:MainWindowLastSelectedCheckbox) -updateStatusCallback { UpdateAppSelectionStatus } + + $appsPanel.Children.Add($checkbox) | Out-Null + } + + # Hide loading indicator and navigation blocker, update status + $loadingAppsIndicator.Visibility = 'Collapsed' + + UpdateAppSelectionStatus + } + + # Loads apps into the UI + function LoadAppsIntoMainUI { + # Cancel any existing load operation to prevent race conditions + if ($script:CurrentAppLoadTimer -and $script:CurrentAppLoadTimer.IsEnabled) { + $script:CurrentAppLoadTimer.Stop() + } + if ($script:CurrentAppLoadJob) { + Remove-Job -Job $script:CurrentAppLoadJob -Force -ErrorAction SilentlyContinue + } + $script:CurrentAppLoadTimer = $null + $script:CurrentAppLoadJob = $null + $script:CurrentAppLoadJobStartTime = $null + + # Show loading indicator and navigation blocker, clear existing apps immediately + $loadingAppsIndicator.Visibility = 'Visible' + $appsPanel.Children.Clear() + + # Update navigation buttons to disable Next/Previous + UpdateNavigationButtons + + # Force UI to update and render all changes (loading indicator, blocker, disabled buttons) + $window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Render, [action]{}) + + # Schedule the actual loading work to run after UI has updated + $window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ + $listOfApps = "" + + if ($onlyInstalledAppsBox.IsChecked -and ($script:WingetInstalled -eq $true)) { + # Start job to get list of installed apps via WinGet (async helper) + $asyncJob = GetInstalledAppsViaWinget -Async + $script:CurrentAppLoadJob = $asyncJob.Job + $script:CurrentAppLoadJobStartTime = $asyncJob.StartTime + + # Create timer to poll job status without blocking UI + $script:CurrentAppLoadTimer = New-Object System.Windows.Threading.DispatcherTimer + $script:CurrentAppLoadTimer.Interval = [TimeSpan]::FromMilliseconds(100) + + $script:CurrentAppLoadTimer.Add_Tick({ + # Check if this timer was cancelled (another load started) + if (-not $script:CurrentAppLoadJob -or -not $script:CurrentAppLoadTimer -or -not $script:CurrentAppLoadJobStartTime) { + if ($script:CurrentAppLoadTimer) { $script:CurrentAppLoadTimer.Stop() } + return + } + + $elapsed = (Get-Date) - $script:CurrentAppLoadJobStartTime + + # Check if job is complete or timed out (10 seconds) + if ($script:CurrentAppLoadJob.State -eq 'Completed') { + $script:CurrentAppLoadTimer.Stop() + $listOfApps = Receive-Job -Job $script:CurrentAppLoadJob + Remove-Job -Job $script:CurrentAppLoadJob -ErrorAction SilentlyContinue + $script:CurrentAppLoadJob = $null + $script:CurrentAppLoadTimer = $null + $script:CurrentAppLoadJobStartTime = $null + + # Continue with loading apps + LoadAppsWithList $listOfApps + } + elseif ($elapsed.TotalSeconds -gt 10 -or $script:CurrentAppLoadJob.State -eq 'Failed') { + $script:CurrentAppLoadTimer.Stop() + Remove-Job -Job $script:CurrentAppLoadJob -Force -ErrorAction SilentlyContinue + $script:CurrentAppLoadJob = $null + $script:CurrentAppLoadTimer = $null + $script:CurrentAppLoadJobStartTime = $null + + # Show error that the script was unable to get list of apps from WinGet + Show-MessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null + $onlyInstalledAppsBox.IsChecked = $false + + # Continue with loading all apps (unchecked now) + LoadAppsWithList "" + } + }) + + $script:CurrentAppLoadTimer.Start() + return # Exit here, timer will continue the work + } + + # If checkbox is not checked or winget not installed, load all apps immediately + LoadAppsWithList $listOfApps + }) | Out-Null + } + + # Event handlers for app selection + $onlyInstalledAppsBox.Add_Checked({ + LoadAppsIntoMainUI + }) + $onlyInstalledAppsBox.Add_Unchecked({ + LoadAppsIntoMainUI + }) + + # Quick selection buttons - only select apps actually in those categories + $defaultAppsBtn.Add_Click({ + foreach ($child in $appsPanel.Children) { + if ($child -is [System.Windows.Controls.CheckBox]) { + if ($child.SelectedByDefault -eq $true) { + $child.IsChecked = $true + } else { + $child.IsChecked = $false + } + } + } + }) + + $clearAppSelectionBtn.Add_Click({ + foreach ($child in $appsPanel.Children) { + if ($child -is [System.Windows.Controls.CheckBox]) { + $child.IsChecked = $false + } + } + }) + + # Shared search highlighting configuration + $script:SearchHighlightColor = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFF4CE")) + $script:SearchHighlightColorDark = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#4A4A2A")) + + # Helper function to get the appropriate highlight brush based on theme + function GetSearchHighlightBrush { + if ($usesDarkMode) { return $script:SearchHighlightColorDark } + return $script:SearchHighlightColor + } + + # Helper function to scroll to an item if it's not visible, centering it in the viewport + function ScrollToItemIfNotVisible { + param ( + [System.Windows.Controls.ScrollViewer]$scrollViewer, + [System.Windows.UIElement]$item, + [System.Windows.UIElement]$container + ) + + if (-not $scrollViewer -or -not $item -or -not $container) { return } + + try { + $itemPosition = $item.TransformToAncestor($container).Transform([System.Windows.Point]::new(0, 0)).Y + $viewportHeight = $scrollViewer.ViewportHeight + $itemHeight = $item.ActualHeight + $currentOffset = $scrollViewer.VerticalOffset + + # Check if the item is currently visible in the viewport + $itemTop = $itemPosition - $currentOffset + $itemBottom = $itemTop + $itemHeight + + $isVisible = ($itemTop -ge 0) -and ($itemBottom -le $viewportHeight) + + # Only scroll if the item is not visible + if (-not $isVisible) { + # Center the item in the viewport + $targetOffset = $itemPosition - ($viewportHeight / 2) + ($itemHeight / 2) + $scrollViewer.ScrollToVerticalOffset([Math]::Max(0, $targetOffset)) + } + } + catch { + # Fallback to simple bring into view + $item.BringIntoView() + } + } + + # Helper function to find the parent ScrollViewer of an element + function FindParentScrollViewer { + param ([System.Windows.UIElement]$element) + + $parent = [System.Windows.Media.VisualTreeHelper]::GetParent($element) + while ($null -ne $parent) { + if ($parent -is [System.Windows.Controls.ScrollViewer]) { + return $parent + } + $parent = [System.Windows.Media.VisualTreeHelper]::GetParent($parent) + } + return $null + } + + # App Search Box functionality + $appSearchBox = $window.FindName('AppSearchBox') + $appSearchPlaceholder = $window.FindName('AppSearchPlaceholder') + + $appSearchBox.Add_TextChanged({ + $searchText = $appSearchBox.Text.ToLower().Trim() + + # Show/hide placeholder + $appSearchPlaceholder.Visibility = if ([string]::IsNullOrWhiteSpace($appSearchBox.Text)) { 'Visible' } else { 'Collapsed' } + + # Clear all highlights first + foreach ($child in $appsPanel.Children) { + if ($child -is [System.Windows.Controls.CheckBox]) { + $child.Background = [System.Windows.Media.Brushes]::Transparent + } + } + + if ([string]::IsNullOrWhiteSpace($searchText)) { return } + + # Find and highlight all matching apps + $firstMatch = $null + $highlightBrush = GetSearchHighlightBrush + + foreach ($child in $appsPanel.Children) { + if ($child -is [System.Windows.Controls.CheckBox] -and $child.Visibility -eq 'Visible') { + if ($child.Content.ToString().ToLower().Contains($searchText)) { + $child.Background = $highlightBrush + if ($null -eq $firstMatch) { $firstMatch = $child } + } + } + } + + # Scroll to first match if not visible + if ($firstMatch) { + $scrollViewer = FindParentScrollViewer -element $appsPanel + if ($scrollViewer) { + ScrollToItemIfNotVisible -scrollViewer $scrollViewer -item $firstMatch -container $appsPanel + } + } + }) + + # Tweak Search Box functionality + $tweakSearchBox = $window.FindName('TweakSearchBox') + $tweakSearchPlaceholder = $window.FindName('TweakSearchPlaceholder') + $tweakSearchBorder = $window.FindName('TweakSearchBorder') + $tweaksScrollViewer = $window.FindName('TweaksScrollViewer') + $tweaksGrid = $window.FindName('TweaksGrid') + $col0 = $window.FindName('Column0Panel') + $col1 = $window.FindName('Column1Panel') + $col2 = $window.FindName('Column2Panel') + + # Monitor scrollbar visibility and adjust searchbar margin + $tweaksScrollViewer.Add_ScrollChanged({ + if ($tweaksScrollViewer.ScrollableHeight -gt 0) { + # The 17px accounts for the scrollbar width + some padding + $tweakSearchBorder.Margin = [System.Windows.Thickness]::new(0, 0, 17, 0) + } else { + $tweakSearchBorder.Margin = [System.Windows.Thickness]::new(0, 0, 0, 0) + } + }) + + # Helper function to clear all tweak highlights + function ClearTweakHighlights { + $columns = @($col0, $col1, $col2) | Where-Object { $_ -ne $null } + foreach ($column in $columns) { + foreach ($card in $column.Children) { + if ($card -is [System.Windows.Controls.Border] -and $card.Child -is [System.Windows.Controls.StackPanel]) { + foreach ($control in $card.Child.Children) { + if ($control -is [System.Windows.Controls.CheckBox] -or + ($control -is [System.Windows.Controls.Border] -and $control.Name -like '*_LabelBorder')) { + $control.Background = [System.Windows.Media.Brushes]::Transparent + } + } + } + } + } + } + + # Helper function to check if a ComboBox contains matching items + function ComboBoxContainsMatch { + param ([System.Windows.Controls.ComboBox]$comboBox, [string]$searchText) + + foreach ($item in $comboBox.Items) { + $itemText = if ($item -is [System.Windows.Controls.ComboBoxItem]) { $item.Content.ToString().ToLower() } else { $item.ToString().ToLower() } + if ($itemText.Contains($searchText)) { return $true } + } + return $false + } + + $tweakSearchBox.Add_TextChanged({ + $searchText = $tweakSearchBox.Text.ToLower().Trim() + + # Show/hide placeholder + $tweakSearchPlaceholder.Visibility = if ([string]::IsNullOrWhiteSpace($tweakSearchBox.Text)) { 'Visible' } else { 'Collapsed' } + + # Clear all highlights + ClearTweakHighlights + + if ([string]::IsNullOrWhiteSpace($searchText)) { return } + + # Find and highlight all matching tweaks + $firstMatch = $null + $highlightBrush = GetSearchHighlightBrush + $columns = @($col0, $col1, $col2) | Where-Object { $_ -ne $null } + + foreach ($column in $columns) { + foreach ($card in $column.Children) { + if ($card -is [System.Windows.Controls.Border] -and $card.Child -is [System.Windows.Controls.StackPanel]) { + $controlsList = @($card.Child.Children) + for ($i = 0; $i -lt $controlsList.Count; $i++) { + $control = $controlsList[$i] + $matchFound = $false + $controlToHighlight = $null + + if ($control -is [System.Windows.Controls.CheckBox]) { + if ($control.Content.ToString().ToLower().Contains($searchText)) { + $matchFound = $true + $controlToHighlight = $control + } + } + elseif ($control -is [System.Windows.Controls.Border] -and $control.Name -like '*_LabelBorder') { + $labelText = if ($control.Child) { $control.Child.Text.ToLower() } else { "" } + $comboBox = if ($i + 1 -lt $controlsList.Count -and $controlsList[$i + 1] -is [System.Windows.Controls.ComboBox]) { $controlsList[$i + 1] } else { $null } + + # Check label text or combo box items + if ($labelText.Contains($searchText) -or ($comboBox -and (ComboBoxContainsMatch -comboBox $comboBox -searchText $searchText))) { + $matchFound = $true + $controlToHighlight = $control + } + } + + if ($matchFound -and $controlToHighlight) { + $controlToHighlight.Background = $highlightBrush + if ($null -eq $firstMatch) { $firstMatch = $controlToHighlight } + } + } + } + } + } + + # Scroll to first match if not visible + if ($firstMatch -and $tweaksScrollViewer) { + ScrollToItemIfNotVisible -scrollViewer $tweaksScrollViewer -item $firstMatch -container $tweaksGrid + } + }) + + # Add Ctrl+F keyboard shortcut to focus search box on current tab + $window.Add_KeyDown({ + param($sender, $e) + + # Check if Ctrl+F was pressed + if ($e.Key -eq [System.Windows.Input.Key]::F -and + ([System.Windows.Input.Keyboard]::Modifiers -band [System.Windows.Input.ModifierKeys]::Control)) { + + $currentTab = $tabControl.SelectedItem + + # Focus AppSearchBox if on App Removal tab + if ($currentTab.Header -eq "App Removal" -and $appSearchBox) { + $appSearchBox.Focus() + $e.Handled = $true + } + # Focus TweakSearchBox if on Tweaks tab + elseif ($currentTab.Header -eq "Tweaks" -and $tweakSearchBox) { + $tweakSearchBox.Focus() + $e.Handled = $true + } + } + }) + + # Wizard Navigation + $tabControl = $window.FindName('MainTabControl') + $previousBtn = $window.FindName('PreviousBtn') + $nextBtn = $window.FindName('NextBtn') + $userSelectionCombo = $window.FindName('UserSelectionCombo') + $userSelectionDescription = $window.FindName('UserSelectionDescription') + $otherUserPanel = $window.FindName('OtherUserPanel') + $otherUsernameTextBox = $window.FindName('OtherUsernameTextBox') + $usernameTextBoxPlaceholder = $window.FindName('UsernameTextBoxPlaceholder') + $usernameValidationMessage = $window.FindName('UsernameValidationMessage') + $appRemovalScopeCombo = $window.FindName('AppRemovalScopeCombo') + $appRemovalScopeDescription = $window.FindName('AppRemovalScopeDescription') + $appRemovalScopeSection = $window.FindName('AppRemovalScopeSection') + $appRemovalScopeCurrentUser = $window.FindName('AppRemovalScopeCurrentUser') + $appRemovalScopeTargetUser = $window.FindName('AppRemovalScopeTargetUser') + + # Navigation button handlers + function UpdateNavigationButtons { + $currentIndex = $tabControl.SelectedIndex + $totalTabs = $tabControl.Items.Count + + $homeIndex = 0 + $overviewIndex = $totalTabs - 2 + $applyIndex = $totalTabs - 1 + + # Navigation button visibility + if ($currentIndex -eq $homeIndex) { + $nextBtn.Visibility = 'Collapsed' + $previousBtn.Visibility = 'Collapsed' + } elseif ($currentIndex -eq $overviewIndex) { + $nextBtn.Visibility = 'Collapsed' + $previousBtn.Visibility = 'Visible' + } elseif ($currentIndex -eq $applyIndex) { + $nextBtn.Visibility = 'Collapsed' + $previousBtn.Visibility = 'Collapsed' + } else { + $nextBtn.Visibility = 'Visible' + $previousBtn.Visibility = 'Visible' + } + + # Update progress indicators + # Tab indices: 0=Home, 1=App Removal, 2=Tweaks, 3=Overview, 4=Apply + $blueColor = "#0067c0" + $greyColor = "#808080" + + $progressIndicator1 = $window.FindName('ProgressIndicator1') # App Removal + $progressIndicator2 = $window.FindName('ProgressIndicator2') # Tweaks + $progressIndicator3 = $window.FindName('ProgressIndicator3') # Overview + $bottomNavGrid = $window.FindName('BottomNavGrid') + + # Hide bottom navigation on home page and apply tab + if ($currentIndex -eq 0 -or $currentIndex -eq $applyIndex) { + $bottomNavGrid.Visibility = 'Collapsed' + } else { + $bottomNavGrid.Visibility = 'Visible' + } + + # Update indicator colors based on current tab + # Indicator 1 (App Removal) - tab index 1 + if ($currentIndex -ge 1) { + $progressIndicator1.Fill = $blueColor + } else { + $progressIndicator1.Fill = $greyColor + } + + # Indicator 2 (Tweaks) - tab index 2 + if ($currentIndex -ge 2) { + $progressIndicator2.Fill = $blueColor + } else { + $progressIndicator2.Fill = $greyColor + } + + # Indicator 3 (Overview) - tab index 3 + if ($currentIndex -ge 3) { + $progressIndicator3.Fill = $blueColor + } else { + $progressIndicator3.Fill = $greyColor + } + } + + # Update user selection description and show/hide other user panel + $userSelectionCombo.Add_SelectionChanged({ + switch ($userSelectionCombo.SelectedIndex) { + 0 { + $userSelectionDescription.Text = "Changes will be applied to the currently logged-in user profile." + $otherUserPanel.Visibility = 'Collapsed' + $usernameValidationMessage.Text = "" + # Show "Current user only" option, hide "Target user only" option + $appRemovalScopeCurrentUser.Visibility = 'Visible' + $appRemovalScopeTargetUser.Visibility = 'Collapsed' + # Enable app removal scope selection for current user + $appRemovalScopeCombo.IsEnabled = $true + $appRemovalScopeCombo.SelectedIndex = 0 + } + 1 { + $userSelectionDescription.Text = "Changes will be applied to a different user profile on this system." + $otherUserPanel.Visibility = 'Visible' + $usernameValidationMessage.Text = "" + # Hide "Current user only" option, show "Target user only" option + $appRemovalScopeCurrentUser.Visibility = 'Collapsed' + $appRemovalScopeTargetUser.Visibility = 'Visible' + # Enable app removal scope selection for other user + $appRemovalScopeCombo.IsEnabled = $true + $appRemovalScopeCombo.SelectedIndex = 0 + } + 2 { + $userSelectionDescription.Text = "Changes will be applied to the default user template, affecting all new users created after this point. Useful for Sysprep deployment." + $otherUserPanel.Visibility = 'Collapsed' + $usernameValidationMessage.Text = "" + # Hide other user options since they don't apply to default user template + $appRemovalScopeCurrentUser.Visibility = 'Collapsed' + $appRemovalScopeTargetUser.Visibility = 'Collapsed' + # Lock app removal scope to "All users" when applying to sysprep + $appRemovalScopeCombo.IsEnabled = $false + $appRemovalScopeCombo.SelectedIndex = 0 + } + } + }) + + # Helper function to update app removal scope description + function UpdateAppRemovalScopeDescription { + $selectedItem = $appRemovalScopeCombo.SelectedItem + if ($selectedItem) { + switch ($selectedItem.Content) { + "All users" { + $appRemovalScopeDescription.Text = "Apps will be removed for all users and from the Windows image to prevent reinstallation for new users." + } + "Current user only" { + $appRemovalScopeDescription.Text = "Apps will only be removed for the current user. Other users and new users will not be affected." + } + "Target user only" { + $appRemovalScopeDescription.Text = "Apps will only be removed for the specified target user. Other users and new users will not be affected." + } + } + } + } + + # Update app removal scope description + $appRemovalScopeCombo.Add_SelectionChanged({ + UpdateAppRemovalScopeDescription + }) + + $otherUsernameTextBox.Add_TextChanged({ + # Show/hide placeholder + if ([string]::IsNullOrWhiteSpace($otherUsernameTextBox.Text)) { + $usernameTextBoxPlaceholder.Visibility = 'Visible' + } else { + $usernameTextBoxPlaceholder.Visibility = 'Collapsed' + } + + ValidateOtherUsername + }) + + function ValidateOtherUsername { + # Only validate if "Other User" is selected + if ($userSelectionCombo.SelectedIndex -ne 1) { + return $true + } + + $username = $otherUsernameTextBox.Text.Trim() + + $errorBrush = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#c42b1c")) + $successBrush = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#28a745")) + + if ($username.Length -eq 0) { + $usernameValidationMessage.Text = "[X] Please enter a username" + $usernameValidationMessage.Foreground = $errorBrush + return $false + } + + if ($username -eq $env:USERNAME) { + $usernameValidationMessage.Text = "[X] Cannot enter your own username, use 'Current User' option instead" + $usernameValidationMessage.Foreground = $errorBrush + return $false + } + + $userExists = CheckIfUserExists -Username $username + + if ($userExists) { + $usernameValidationMessage.Text = "[OK] User found: $username" + $usernameValidationMessage.Foreground = $successBrush + return $true + } + + $usernameValidationMessage.Text = "[X] User not found, please enter a valid username" + $usernameValidationMessage.Foreground = $errorBrush + return $false + } + + function GenerateOverview { + # Load Features.json + $featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0" + $overviewChangesPanel = $window.FindName('OverviewChangesPanel') + $overviewChangesPanel.Children.Clear() + + $changesList = @() + + # Collect selected apps + $selectedAppsCount = 0 + foreach ($child in $appsPanel.Children) { + if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) { + $selectedAppsCount++ + } + } + if ($selectedAppsCount -gt 0) { + $changesList += "Remove $selectedAppsCount selected application(s)" + } + + # Update app removal scope section based on whether apps are selected + if ($selectedAppsCount -gt 0) { + # Enable app removal scope selection (unless locked by sysprep mode) + if ($userSelectionCombo.SelectedIndex -ne 2) { + $appRemovalScopeCombo.IsEnabled = $true + } + $appRemovalScopeSection.Opacity = 1.0 + UpdateAppRemovalScopeDescription + } + else { + # Disable app removal scope selection when no apps selected + $appRemovalScopeCombo.IsEnabled = $false + $appRemovalScopeSection.Opacity = 0.5 + $appRemovalScopeDescription.Text = "No apps selected for removal." + } + + # Collect all ComboBox/CheckBox selections from dynamically created controls + if ($script:UiControlMappings) { + foreach ($mappingKey in $script:UiControlMappings.Keys) { + $control = $window.FindName($mappingKey) + $isSelected = $false + + # Check if it's a checkbox or combobox + if ($control -is [System.Windows.Controls.CheckBox]) { + $isSelected = $control.IsChecked -eq $true + } + elseif ($control -is [System.Windows.Controls.ComboBox]) { + $isSelected = $control.SelectedIndex -gt 0 + } + + if ($control -and $isSelected) { + $mapping = $script:UiControlMappings[$mappingKey] + if ($mapping.Type -eq 'group') { + # For combobox: SelectedIndex 0 = No Change, so subtract 1 to index into Values + $selectedValue = $mapping.Values[$control.SelectedIndex - 1] + foreach ($fid in $selectedValue.FeatureIds) { + $feature = $featuresJson.Features | Where-Object { $_.FeatureId -eq $fid } + if ($feature) { $changesList += ($feature.Action + ' ' + $feature.Label) } + } + } + elseif ($mapping.Type -eq 'feature') { + $feature = $featuresJson.Features | Where-Object { $_.FeatureId -eq $mapping.FeatureId } + if ($feature) { $changesList += ($feature.Action + ' ' + $feature.Label) } + } + } + } + } + + if ($changesList.Count -eq 0) { + $textBlock = New-Object System.Windows.Controls.TextBlock + $textBlock.Text = "No changes selected" + $textBlock.Style = $window.Resources["OverviewNoChangesTextStyle"] + $overviewChangesPanel.Children.Add($textBlock) | Out-Null + } + else { + foreach ($change in $changesList) { + $bullet = New-Object System.Windows.Controls.TextBlock + $bullet.Text = "- $change" + $bullet.Style = $window.Resources["OverviewChangeBulletStyle"] + $overviewChangesPanel.Children.Add($bullet) | Out-Null + } + } + } + + $previousBtn.Add_Click({ + if ($tabControl.SelectedIndex -gt 0) { + $tabControl.SelectedIndex-- + UpdateNavigationButtons + } + }) + + $nextBtn.Add_Click({ + if ($tabControl.SelectedIndex -lt ($tabControl.Items.Count - 1)) { + $tabControl.SelectedIndex++ + + UpdateNavigationButtons + } + }) + + # Handle Home Start button + $homeStartBtn = $window.FindName('HomeStartBtn') + $homeStartBtn.Add_Click({ + # Navigate to first tab after home (App Removal) + $tabControl.SelectedIndex = 1 + UpdateNavigationButtons + }) + + # Handle Overview Apply Changes button - validates and immediately starts applying changes + $overviewApplyBtn = $window.FindName('OverviewApplyBtn') + $overviewApplyBtn.Add_Click({ + if (-not (ValidateOtherUsername)) { + Show-MessageBox -Message "Please enter a valid username." -Title "Invalid Username" -Button 'OK' -Icon 'Warning' | Out-Null + return + } + + # App Removal - collect selected apps from integrated UI + $selectedApps = @() + foreach ($child in $appsPanel.Children) { + if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) { + $selectedApps += $child.Tag + } + } + + if ($selectedApps.Count -gt 0) { + # Check if Microsoft Store is selected + if ($selectedApps -contains "Microsoft.WindowsStore") { + $result = Show-MessageBox -Message 'Are you sure you wish to uninstall the Microsoft Store? This app cannot easily be reinstalled.' -Title 'Are you sure?' -Button 'YesNo' -Icon 'Warning' + + if ($result -eq 'No') { + return + } + } + + AddParameter 'RemoveApps' + AddParameter 'Apps' ($selectedApps -join ',') + + # Add app removal target parameter based on selection + $selectedScopeItem = $appRemovalScopeCombo.SelectedItem + if ($selectedScopeItem) { + switch ($selectedScopeItem.Content) { + "All users" { + AddParameter 'AppRemovalTarget' 'AllUsers' + } + "Current user only" { + AddParameter 'AppRemovalTarget' 'CurrentUser' + } + "Target user only" { + # Use the target username from Other User panel + AddParameter 'AppRemovalTarget' ($otherUsernameTextBox.Text.Trim()) + } + } + } + } + + # Apply dynamic tweaks selections + if ($script:UiControlMappings) { + foreach ($mappingKey in $script:UiControlMappings.Keys) { + $control = $window.FindName($mappingKey) + $isSelected = $false + $selectedIndex = 0 + + # Check if it's a checkbox or combobox + if ($control -is [System.Windows.Controls.CheckBox]) { + $isSelected = $control.IsChecked -eq $true + $selectedIndex = if ($isSelected) { 1 } else { 0 } + } + elseif ($control -is [System.Windows.Controls.ComboBox]) { + $isSelected = $control.SelectedIndex -gt 0 + $selectedIndex = $control.SelectedIndex + } + + if ($control -and $isSelected) { + $mapping = $script:UiControlMappings[$mappingKey] + if ($mapping.Type -eq 'group') { + if ($selectedIndex -gt 0 -and $selectedIndex -le $mapping.Values.Count) { + $selectedValue = $mapping.Values[$selectedIndex - 1] + foreach ($fid in $selectedValue.FeatureIds) { + AddParameter $fid + } + } + } + elseif ($mapping.Type -eq 'feature') { + AddParameter $mapping.FeatureId + } + } + } + } + + $controlParamsCount = 0 + foreach ($Param in $script:ControlParams) { + if ($script:Params.ContainsKey($Param)) { + $controlParamsCount++ + } + } + + # Check if any changes were selected + $totalChanges = $script:Params.Count - $controlParamsCount + + # Apps parameter does not count as a change itself + if ($script:Params.ContainsKey('Apps')) { + $totalChanges = $totalChanges - 1 + } + + if ($totalChanges -eq 0) { + Show-MessageBox -Message 'No changes have been selected, please select at least one option to proceed.' -Title 'No Changes Selected' -Button 'OK' -Icon 'Information' + return + } + + # Check RestorePointCheckBox + $restorePointCheckBox = $window.FindName('RestorePointCheckBox') + if ($restorePointCheckBox -and $restorePointCheckBox.IsChecked) { + AddParameter 'CreateRestorePoint' + } + + # Store selected user mode + switch ($userSelectionCombo.SelectedIndex) { + 1 { AddParameter User ($otherUsernameTextBox.Text.Trim()) } + 2 { AddParameter Sysprep } + } + + SaveSettings + + # Navigate to Apply tab (last tab) and start applying changes + $tabControl.SelectedIndex = $tabControl.Items.Count - 1 + + # Clear console and set initial status + $consoleOutput.Text = "" + + Write-ToConsole "Applying changes to $(if ($script:Params.ContainsKey("Sysprep")) { "default user template" } else { "user $(GetUserName)" })" + Write-ToConsole "Total changes to apply: $totalChanges" + Write-ToConsole "" + + # Run changes in background to keep UI responsive + $window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ + try { + ExecuteAllChanges + + # Check if user wants to restart explorer (from checkbox) + $restartExplorerCheckBox = $window.FindName('RestartExplorerCheckBox') + if ($restartExplorerCheckBox -and $restartExplorerCheckBox.IsChecked -and -not $script:CancelRequested) { + RestartExplorer + } + + Write-ToConsole "" + if ($script:CancelRequested) { + Write-ToConsole "Script execution was cancelled by the user. Some changes may not have been applied." + } else { + Write-ToConsole "All changes have been applied. Please check the output above for any errors." + } + + $finishBtn.Dispatcher.Invoke([action]{ + $finishBtn.IsEnabled = $true + $finishBtnText.Text = "Close Win11Debloat" + }) + } + catch { + Write-ToConsole "Error: $($_.Exception.Message)" + $finishBtn.Dispatcher.Invoke([action]{ + $finishBtn.IsEnabled = $true + $finishBtnText.Text = "Close Win11Debloat" + }) + } + }) + }) + + # Initialize UI elements on window load + $window.Add_Loaded({ + BuildDynamicTweaks + + LoadAppsIntoMainUI + + # Update Current User label with username + if ($userSelectionCombo -and $userSelectionCombo.Items.Count -gt 0) { + $currentUserItem = $userSelectionCombo.Items[0] + if ($currentUserItem -is [System.Windows.Controls.ComboBoxItem]) { + $currentUserItem.Content = "Current User ($(GetUserName))" + } + } + + # Disable Restart Explorer option if NoRestartExplorer parameter is set + $restartExplorerCheckBox = $window.FindName('RestartExplorerCheckBox') + if ($restartExplorerCheckBox -and $script:Params.ContainsKey("NoRestartExplorer")) { + $restartExplorerCheckBox.IsChecked = $false + $restartExplorerCheckBox.IsEnabled = $false + } + + # Force Apply Changes To setting if Sysprep or User parameters are set + if ($script:Params.ContainsKey("Sysprep")) { + $userSelectionCombo.SelectedIndex = 2 + $userSelectionCombo.IsEnabled = $false + } + elseif ($script:Params.ContainsKey("User")) { + $userSelectionCombo.SelectedIndex = 1 + $userSelectionCombo.IsEnabled = $false + $otherUsernameTextBox.Text = $script:Params.Item("User") + $otherUsernameTextBox.IsEnabled = $false + } + + UpdateNavigationButtons + }) + + # Add event handler for tab changes + $tabControl.Add_SelectionChanged({ + # Regenerate overview when switching to Overview tab + if ($tabControl.SelectedIndex -eq ($tabControl.Items.Count - 2)) { + GenerateOverview + } + UpdateNavigationButtons + }) + + # Handle Load Defaults button + $loadDefaultsBtn = $window.FindName('LoadDefaultsBtn') + $loadDefaultsBtn.Add_Click({ + $defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0" + + if (-not $defaultsJson) { + Show-MessageBox -Message "Failed to load default settings file" -Title "Error" -Button 'OK' -Icon 'Error' + return + } + + ApplySettingsToUiControls -window $window -settingsJson $defaultsJson -uiControlMappings $script:UiControlMappings + }) + + # Handle Load Last Used settings and Load Last Used apps + $loadLastUsedBtn = $window.FindName('LoadLastUsedBtn') + $loadLastUsedAppsBtn = $window.FindName('LoadLastUsedAppsBtn') + + $lastUsedSettingsJson = LoadJsonFile -filePath $script:SavedSettingsFilePath -expectedVersion "1.0" -optionalFile + + $hasSettings = $false + $appsSetting = $null + if ($lastUsedSettingsJson -and $lastUsedSettingsJson.Settings) { + foreach ($s in $lastUsedSettingsJson.Settings) { + # Only count as hasSettings if a setting other than RemoveApps/Apps is present and true + if ($s.Value -eq $true -and $s.Name -ne 'RemoveApps' -and $s.Name -ne 'Apps') { $hasSettings = $true } + if ($s.Name -eq 'Apps' -and $s.Value) { $appsSetting = $s.Value } + } + } + + # Show option to load last used settings if they exist + if ($hasSettings) { + $loadLastUsedBtn.Add_Click({ + try { + ApplySettingsToUiControls -window $window -settingsJson $lastUsedSettingsJson -uiControlMappings $script:UiControlMappings + } + catch { + Show-MessageBox -Message "Failed to load last used settings: $_" -Title "Error" -Button 'OK' -Icon 'Error' + } + }) + } + else { + $loadLastUsedBtn.Visibility = 'Collapsed' + } + + # Show option to load last used apps if they exist + if ($appsSetting -and $appsSetting.ToString().Trim().Length -gt 0) { + $loadLastUsedAppsBtn.Add_Click({ + try { + $savedApps = @() + if ($appsSetting -is [string]) { $savedApps = $appsSetting.Split(',') } + elseif ($appsSetting -is [array]) { $savedApps = $appsSetting } + $savedApps = $savedApps | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } + + foreach ($child in $appsPanel.Children) { + if ($child -is [System.Windows.Controls.CheckBox]) { + if ($savedApps -contains $child.Tag) { $child.IsChecked = $true } else { $child.IsChecked = $false } + } + } + } + catch { + Show-MessageBox -Message "Failed to load last used app selection: $_" -Title "Error" -Button 'OK' -Icon 'Error' + } + }) + } + else { + $loadLastUsedAppsBtn.Visibility = 'Collapsed' + } + + # Clear All Tweaks button + $clearAllTweaksBtn = $window.FindName('ClearAllTweaksBtn') + $clearAllTweaksBtn.Add_Click({ + # Reset all ComboBoxes to index 0 (No Change) and uncheck all CheckBoxes + if ($script:UiControlMappings) { + foreach ($comboName in $script:UiControlMappings.Keys) { + $control = $window.FindName($comboName) + if ($control -is [System.Windows.Controls.CheckBox]) { + $control.IsChecked = $false + } + elseif ($control -is [System.Windows.Controls.ComboBox]) { + $control.SelectedIndex = 0 + } + } + } + + # Also uncheck RestorePointCheckBox + $restorePointCheckBox = $window.FindName('RestorePointCheckBox') + if ($restorePointCheckBox) { + $restorePointCheckBox.IsChecked = $false + } + }) + + # Finish (Close Win11Debloat) button handler + $finishBtn.Add_Click({ + $window.Close() + }) + + # Show the window + return $window.ShowDialog() +} diff --git a/Scripts/GUI/Show-MessageBox.ps1 b/Scripts/GUI/Show-MessageBox.ps1 new file mode 100644 index 0000000..bff6df6 --- /dev/null +++ b/Scripts/GUI/Show-MessageBox.ps1 @@ -0,0 +1,154 @@ +# Shows a Windows 11 styled custom message box +function Show-MessageBox { + param ( + [Parameter(Mandatory=$true)] + [string]$Message, + + [Parameter(Mandatory=$false)] + [string]$Title = "Win11Debloat", + + [Parameter(Mandatory=$false)] + [ValidateSet('OK', 'OKCancel', 'YesNo')] + [string]$Button = 'OK', + + [Parameter(Mandatory=$false)] + [ValidateSet('None', 'Information', 'Warning', 'Error', 'Question')] + [string]$Icon = 'None', + + [Parameter(Mandatory=$false)] + [System.Windows.Window]$Owner = $null + ) + + Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null + + $usesDarkMode = GetSystemUsesDarkMode + + # Determine owner window - use provided Owner, or fall back to main GUI window + $ownerWindow = if ($Owner) { $Owner } else { $script:GuiWindow } + + # Show overlay if owner window exists + $overlay = $null + if ($ownerWindow) { + try { + $overlay = $ownerWindow.FindName('ModalOverlay') + if ($overlay) { + $ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Visible' }) + } + } + catch { } + } + + # Load XAML from file + $xaml = Get-Content -Path $script:MessageBoxSchema -Raw + $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml)) + try { + $msgWindow = [System.Windows.Markup.XamlReader]::Load($reader) + } + finally { + $reader.Close() + } + + # Set owner to owner window if it exists + if ($ownerWindow) { + try { + $msgWindow.Owner = $ownerWindow + } + catch { } + } + + # Apply theme resources + SetWindowThemeResources -window $msgWindow -usesDarkMode $usesDarkMode + + # Get UI elements + $titleText = $msgWindow.FindName('TitleText') + $messageText = $msgWindow.FindName('MessageText') + $iconText = $msgWindow.FindName('IconText') + $button1 = $msgWindow.FindName('Button1') + $button2 = $msgWindow.FindName('Button2') + $titleBar = $msgWindow.FindName('TitleBar') + + # Set title and message + $titleText.Text = $Title + $messageText.Text = $Message + + # Configure icon + switch ($Icon) { + 'Information' { + $iconText.Text = [char]0xE946 + $iconText.Foreground = $msgWindow.FindResource('InformationIconColor') + $iconText.Visibility = 'Visible' + } + 'Warning' { + $iconText.Text = [char]0xE7BA + $iconText.Foreground = $msgWindow.FindResource('WarningIconColor') + $iconText.Visibility = 'Visible' + } + 'Error' { + $iconText.Text = [char]0xEA39 + $iconText.Foreground = $msgWindow.FindResource('ErrorIconColor') + $iconText.Visibility = 'Visible' + } + 'Question' { + $iconText.Text = [char]0xE897 + $iconText.Foreground = $msgWindow.FindResource('QuestionIconColor') + $iconText.Visibility = 'Visible' + } + default { + $iconText.Visibility = 'Collapsed' + } + } + + # Configure buttons - store result in window's Tag property + switch ($Button) { + 'OK' { + $button1.Content = 'OK' + $button1.Add_Click({ $msgWindow.Tag = 'OK'; $msgWindow.Close() }) + $button2.Visibility = 'Collapsed' + } + 'OKCancel' { + $button1.Content = 'OK' + $button2.Content = 'Cancel' + $button1.Add_Click({ $msgWindow.Tag = 'OK'; $msgWindow.Close() }) + $button2.Add_Click({ $msgWindow.Tag = 'Cancel'; $msgWindow.Close() }) + $button2.Visibility = 'Visible' + } + 'YesNo' { + $button1.Content = 'Yes' + $button2.Content = 'No' + $button1.Add_Click({ $msgWindow.Tag = 'Yes'; $msgWindow.Close() }) + $button2.Add_Click({ $msgWindow.Tag = 'No'; $msgWindow.Close() }) + $button2.Visibility = 'Visible' + } + } + + # Title bar drag to move window + $titleBar.Add_MouseLeftButtonDown({ + $msgWindow.DragMove() + }) + + # Handle Escape key to close + $msgWindow.Add_KeyDown({ + param($sender, $e) + if ($e.Key -eq 'Escape') { + if ($Button -eq 'OK') { + $msgWindow.Tag = 'OK' + } else { + $msgWindow.Tag = 'Cancel' + } + $msgWindow.Close() + } + }) + + # Show dialog and return result from Tag + $msgWindow.ShowDialog() | Out-Null + + # Hide overlay after dialog closes + if ($overlay) { + try { + $ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' }) + } + catch { } + } + + return $msgWindow.Tag +} diff --git a/Get.ps1 b/Scripts/Get.ps1 similarity index 97% rename from Get.ps1 rename to Scripts/Get.ps1 index 4970394..9e1ab5c 100644 --- a/Get.ps1 +++ b/Scripts/Get.ps1 @@ -121,7 +121,7 @@ catch { if (Test-Path "$env:TEMP/Win11Debloat") { Write-Output "" Write-Output "> Cleaning up old Win11Debloat folder..." - Get-ChildItem -Path "$env:TEMP/Win11Debloat" -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log | Remove-Item -Recurse -Force + Get-ChildItem -Path "$env:TEMP/Win11Debloat" -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Logs | Remove-Item -Recurse -Force } Write-Output "" @@ -171,7 +171,7 @@ if (Test-Path "$env:TEMP/Win11Debloat") { Write-Output "> Cleaning up..." # Cleanup, remove Win11Debloat directory - Get-ChildItem -Path "$env:TEMP/Win11Debloat" -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log | Remove-Item -Recurse -Force + Get-ChildItem -Path "$env:TEMP/Win11Debloat" -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Logs | Remove-Item -Recurse -Force } Write-Output "" diff --git a/Win11Debloat.ps1 b/Win11Debloat.ps1 index 796b25a..d369b24 100755 --- a/Win11Debloat.ps1 +++ b/Win11Debloat.ps1 @@ -98,13 +98,13 @@ $script:DefaultSettingsFilePath = "$PSScriptRoot/DefaultSettings.json" $script:AppsListFilePath = "$PSScriptRoot/Apps.json" $script:SavedSettingsFilePath = "$PSScriptRoot/LastUsedSettings.json" $script:CustomAppsListFilePath = "$PSScriptRoot/CustomAppsList" -$script:DefaultLogPath = "$PSScriptRoot/Win11Debloat.log" +$script:DefaultLogPath = "$PSScriptRoot/Logs/Win11Debloat.log" $script:RegfilesPath = "$PSScriptRoot/Regfiles" $script:AssetsPath = "$PSScriptRoot/Assets" -$script:AppSelectionSchema = "$script:AssetsPath/Schemas/AppSelectionWindow.xaml" -$script:MainWindowSchema = "$script:AssetsPath/Schemas/MainWindow.xaml" -$script:MessageBoxSchema = "$script:AssetsPath/Schemas/MessageBoxWindow.xaml" -$script:AboutWindowSchema = "$script:AssetsPath/Schemas/AboutWindow.xaml" +$script:AppSelectionSchema = "$PSScriptRoot/Schemas/AppSelectionWindow.xaml" +$script:MainWindowSchema = "$PSScriptRoot/Schemas/MainWindow.xaml" +$script:MessageBoxSchema = "$PSScriptRoot/Schemas/MessageBoxWindow.xaml" +$script:AboutWindowSchema = "$PSScriptRoot/Schemas/AboutWindow.xaml" $script:FeaturesFilePath = "$script:AssetsPath/Features.json" $script:ControlParams = 'WhatIf', 'Confirm', 'Verbose', 'Debug', 'LogPath', 'Silent', 'Sysprep', 'User', 'NoRestartExplorer', 'RunDefaults', 'RunDefaultsLite', 'RunSavedSettings', 'RunAppsListGenerator', 'CLI', 'AppRemovalTarget' @@ -204,11 +204,37 @@ if (-not $script:WingetInstalled -and -not $Silent) { ################################################################################################################## # # -# FUNCTION DEFINITIONS # +# FUNCTION IMPORTS/DEFINITIONS # # # ################################################################################################################## +# Load CLI functions +. "$PSScriptRoot/Scripts/CLI/ShowCLILastUsedSettings.ps1" +. "$PSScriptRoot/Scripts/CLI/ShowCLIDefaultModeAppRemovalOptions.ps1" +. "$PSScriptRoot/Scripts/CLI/ShowCLIDefaultModeOptions.ps1" +. "$PSScriptRoot/Scripts/CLI/ShowCLIAppRemoval.ps1" +. "$PSScriptRoot/Scripts/CLI/ShowCLIMenuOptions.ps1" +. "$PSScriptRoot/Scripts/CLI/PrintPendingChanges.ps1" +. "$PSScriptRoot/Scripts/CLI/PrintHeader.ps1" +# Load GUI 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-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/SaveSettings.ps1" +. "$PSScriptRoot/Scripts/FileIO/LoadSettings.ps1" +. "$PSScriptRoot/Scripts/FileIO/SaveCustomAppsListToFile.ps1" +. "$PSScriptRoot/Scripts/FileIO/ValidateAppslist.ps1" +. "$PSScriptRoot/Scripts/FileIO/LoadAppsFromFile.ps1" +. "$PSScriptRoot/Scripts/FileIO/LoadAppsDetailsFromJson.ps1" # Writes to both GUI console output and standard console function Write-ToConsole { @@ -267,2320 +293,23 @@ function Write-ToConsole { } -# Loads a JSON file from the specified path and returns the parsed object -# Returns $null if the file doesn't exist or if parsing fails -function LoadJsonFile { +# Add parameter to script and write to file +function AddParameter { param ( - [string]$filePath, - [string]$expectedVersion = $null, - [switch]$optionalFile - ) - - if (-not (Test-Path $filePath)) { - if (-not $optionalFile) { - Write-Error "File not found: $filePath" - } - return $null - } - - try { - $jsonContent = Get-Content -Path $filePath -Raw | ConvertFrom-Json - - # Validate version if specified - if ($expectedVersion -and $jsonContent.Version -and $jsonContent.Version -ne $expectedVersion) { - Write-Error "$(Split-Path $filePath -Leaf) version mismatch (expected $expectedVersion, found $($jsonContent.Version))" - return $null - } - - return $jsonContent - } - catch { - Write-Error "Failed to parse JSON file: $filePath" - return $null - } -} - - -# Loads settings from a JSON file and adds them to script params -# Used by command-line modes (ShowDefaultModeOptions, LoadAndShowLastUsedSettings) -function LoadSettingsToParams { - param ( - [string]$filePath, - [string]$expectedVersion = "1.0" - ) - - $settingsJson = LoadJsonFile -filePath $filePath -expectedVersion $expectedVersion - - if (-not $settingsJson -or -not $settingsJson.Settings) { - throw "Failed to load settings from $(Split-Path $filePath -Leaf)" - } - - # Get current Windows build version - $WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' CurrentBuild - - foreach ($setting in $settingsJson.Settings) { - if ($setting.Value -eq $false) { - continue - } - - $feature = $script:Features[$setting.Name] - - # Check version and feature compatibility using Features.json - if (($feature.MinVersion -and $WinVersion -lt $feature.MinVersion) -or ($feature.MaxVersion -and $WinVersion -gt $feature.MaxVersion) -or ($feature.FeatureId -eq 'DisableModernStandbyNetworking' -and (-not $script:ModernStandbySupported))) { - continue - } - - AddParameter $setting.Name $setting.Value - } -} - - -# Applies settings from a JSON object to UI controls (checkboxes and comboboxes) -# Used by LoadDefaultsBtn and LoadLastUsedBtn in the UI -function ApplySettingsToUiControls { - param ( - $window, - $settingsJson, - $uiControlMappings - ) - - if (-not $settingsJson -or -not $settingsJson.Settings) { - return $false - } - - # First, reset all tweaks to "No Change" (index 0) or unchecked - if ($uiControlMappings) { - foreach ($comboName in $uiControlMappings.Keys) { - $control = $window.FindName($comboName) - if ($control -is [System.Windows.Controls.CheckBox]) { - $control.IsChecked = $false - } - elseif ($control -is [System.Windows.Controls.ComboBox]) { - $control.SelectedIndex = 0 - } - } - } - - # Also uncheck RestorePointCheckBox - $restorePointCheckBox = $window.FindName('RestorePointCheckBox') - if ($restorePointCheckBox) { - $restorePointCheckBox.IsChecked = $false - } - - # Apply settings from JSON - foreach ($setting in $settingsJson.Settings) { - if ($setting.Value -ne $true) { continue } - $paramName = $setting.Name - - # Handle RestorePointCheckBox separately - if ($paramName -eq 'CreateRestorePoint') { - if ($restorePointCheckBox) { $restorePointCheckBox.IsChecked = $true } - continue - } - - if ($uiControlMappings) { - foreach ($comboName in $uiControlMappings.Keys) { - $mapping = $uiControlMappings[$comboName] - if ($mapping.Type -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]) { - $control.SelectedIndex = $i - } - } - break - } - $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]) { - $control.IsChecked = $true - } - elseif ($control -is [System.Windows.Controls.ComboBox]) { - $control.SelectedIndex = 1 - } - } - } - } - } - } - } - - return $true -} - - -# Attaches shift-click selection behavior to a checkbox in an apps panel -# Parameters: -# - $checkbox: The checkbox to attach the behavior to -# - $appsPanel: The StackPanel containing checkbox items -# - $lastSelectedCheckboxRef: A reference to a variable storing the last clicked checkbox -# - $updateStatusCallback: Optional callback to update selection status -function AttachShiftClickBehavior { - param ( - [System.Windows.Controls.CheckBox]$checkbox, - [System.Windows.Controls.StackPanel]$appsPanel, - [ref]$lastSelectedCheckboxRef, - [scriptblock]$updateStatusCallback = $null + $parameterName, + $value = $true ) - # Use a closure to capture the parameters - $checkbox.Add_PreviewMouseLeftButtonDown({ - param( - $sender, - $e - ) - - $isShiftPressed = [System.Windows.Input.Keyboard]::IsKeyDown([System.Windows.Input.Key]::LeftShift) -or - [System.Windows.Input.Keyboard]::IsKeyDown([System.Windows.Input.Key]::RightShift) - - if ($isShiftPressed -and $null -ne $lastSelectedCheckboxRef.Value) { - # Get all visible checkboxes in the panel - $visibleCheckboxes = @() - foreach ($child in $appsPanel.Children) { - if ($child -is [System.Windows.Controls.CheckBox] -and $child.Visibility -eq 'Visible') { - $visibleCheckboxes += $child - } - } - - # Find indices of the last selected and current checkbox - $lastIndex = -1 - $currentIndex = -1 - - for ($i = 0; $i -lt $visibleCheckboxes.Count; $i++) { - if ($visibleCheckboxes[$i] -eq $lastSelectedCheckboxRef.Value) { - $lastIndex = $i - } - if ($visibleCheckboxes[$i] -eq $sender) { - $currentIndex = $i - } - } - - if ($lastIndex -ge 0 -and $currentIndex -ge 0 -and $lastIndex -ne $currentIndex) { - $startIndex = [Math]::Min($lastIndex, $currentIndex) - $endIndex = [Math]::Max($lastIndex, $currentIndex) - - $shouldDeselect = $sender.IsChecked - - # Set all checkboxes in the range to the appropriate state - for ($i = $startIndex; $i -le $endIndex; $i++) { - $visibleCheckboxes[$i].IsChecked = -not $shouldDeselect - } - - if ($updateStatusCallback) { - & $updateStatusCallback - } - - # Mark the event as handled to prevent the default toggle behavior - $e.Handled = $true - return - } - } - - # Update the last selected checkbox reference for next time - $lastSelectedCheckboxRef.Value = $sender - }.GetNewClosure()) -} - - -# Sets resource colors for a WPF window based on dark mode preference -function SetWindowThemeResources { - param ( - $window, - [bool]$usesDarkMode - ) - - if ($usesDarkMode) { - $window.Resources.Add("BgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#202020"))) - $window.Resources.Add("FgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFFFFF"))) - $window.Resources.Add("CardBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#2b2b2b"))) - $window.Resources.Add("BorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#404040"))) - $window.Resources.Add("ButtonBorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#404040"))) - $window.Resources.Add("CheckBoxBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#272727"))) - $window.Resources.Add("CheckBoxBorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#808080"))) - $window.Resources.Add("CheckBoxHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#343434"))) - $window.Resources.Add("ComboBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#373737"))) - $window.Resources.Add("ComboHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#434343"))) - $window.Resources.Add("ComboItemBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#2c2c2c"))) - $window.Resources.Add("ComboItemHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#383838"))) - $window.Resources.Add("ComboItemSelectedColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#343434"))) - $window.Resources.Add("AccentColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFD700"))) - $window.Resources.Add("ButtonDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#434343"))) - $window.Resources.Add("ButtonTextDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#989898"))) - $window.Resources.Add("SecondaryButtonBg", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#393939"))) - $window.Resources.Add("SecondaryButtonHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#2a2a2a"))) - $window.Resources.Add("SecondaryButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#1e1e1e"))) - $window.Resources.Add("SecondaryButtonDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#3b3b3b"))) - $window.Resources.Add("SecondaryButtonTextDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#787878"))) - $window.Resources.Add("InputFocusColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#1f1f1f"))) - $window.Resources.Add("ScrollBarThumbColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#3d3d3d"))) - $window.Resources.Add("ScrollBarThumbHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#4b4b4b"))) + # Add parameter or update its value if key already exists + if (-not $script:Params.ContainsKey($parameterName)) { + $script:Params.Add($parameterName, $value) } else { - $window.Resources.Add("BgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f3f3f3"))) - $window.Resources.Add("FgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#000000"))) - $window.Resources.Add("CardBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#fbfbfb"))) - $window.Resources.Add("BorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#ededed"))) - $window.Resources.Add("ButtonBorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#d3d3d3"))) - $window.Resources.Add("CheckBoxBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f5f5f5"))) - $window.Resources.Add("CheckBoxBorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#898989"))) - $window.Resources.Add("CheckBoxHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#ececec"))) - $window.Resources.Add("ComboBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFFFFF"))) - $window.Resources.Add("ComboHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f8f8f8"))) - $window.Resources.Add("ComboItemBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f9f9f9"))) - $window.Resources.Add("ComboItemHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f0f0f0"))) - $window.Resources.Add("ComboItemSelectedColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f3f3f3"))) - $window.Resources.Add("AccentColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#ffae00"))) - $window.Resources.Add("ButtonDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#bfbfbf"))) - $window.Resources.Add("ButtonTextDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#ffffff"))) - $window.Resources.Add("SecondaryButtonBg", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#fbfbfb"))) - $window.Resources.Add("SecondaryButtonHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f6f6f6"))) - $window.Resources.Add("SecondaryButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f0f0f0"))) - $window.Resources.Add("SecondaryButtonDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f7f7f7"))) - $window.Resources.Add("SecondaryButtonTextDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#b7b7b7"))) - $window.Resources.Add("InputFocusColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#fbfbfb"))) - $window.Resources.Add("ScrollBarThumbColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#b9b9b9"))) - $window.Resources.Add("ScrollBarThumbHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#8b8b8b"))) - } - - $window.Resources.Add("ButtonBg", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0067c0"))) - $window.Resources.Add("ButtonHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#1E88E5"))) - $window.Resources.Add("ButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#3284cc"))) - $window.Resources.Add("CloseHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#c42b1c"))) - $window.Resources.Add("InformationIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0078D4"))) - $window.Resources.Add("WarningIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFB900"))) - $window.Resources.Add("ErrorIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#E81123"))) - $window.Resources.Add("QuestionIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0078D4"))) -} - - -# Checks if the system is set to use dark mode for apps -function GetSystemUsesDarkMode { - try { - return (Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize' -Name 'AppsUseLightTheme').AppsUseLightTheme -eq 0 - } - catch { - return $false + $script:Params[$parameterName] = $value } } -# Shows a Windows 11 styled custom message box -function Show-ModernMessageBox { - param ( - [Parameter(Mandatory=$true)] - [string]$Message, - - [Parameter(Mandatory=$false)] - [string]$Title = "Win11Debloat", - - [Parameter(Mandatory=$false)] - [ValidateSet('OK', 'OKCancel', 'YesNo', 'YesNoCancel')] - [string]$Button = 'OK', - - [Parameter(Mandatory=$false)] - [ValidateSet('None', 'Information', 'Warning', 'Error', 'Question')] - [string]$Icon = 'None', - - [Parameter(Mandatory=$false)] - [System.Windows.Window]$Owner = $null - ) - - Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null - - $usesDarkMode = GetSystemUsesDarkMode - - # Determine owner window - use provided Owner, or fall back to main GUI window - $ownerWindow = if ($Owner) { $Owner } else { $script:GuiWindow } - - # Show overlay if owner window exists - $overlay = $null - if ($ownerWindow) { - try { - $overlay = $ownerWindow.FindName('ModalOverlay') - if ($overlay) { - $ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Visible' }) - } - } - catch { } - } - - # Load XAML from file - $xaml = Get-Content -Path $script:MessageBoxSchema -Raw - $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml)) - try { - $msgWindow = [System.Windows.Markup.XamlReader]::Load($reader) - } - finally { - $reader.Close() - } - - # Set owner to owner window if it exists - if ($ownerWindow) { - try { - $msgWindow.Owner = $ownerWindow - } - catch { } - } - - # Apply theme resources - SetWindowThemeResources -window $msgWindow -usesDarkMode $usesDarkMode - - # Get UI elements - $titleText = $msgWindow.FindName('TitleText') - $messageText = $msgWindow.FindName('MessageText') - $iconText = $msgWindow.FindName('IconText') - $button1 = $msgWindow.FindName('Button1') - $button2 = $msgWindow.FindName('Button2') - $titleBar = $msgWindow.FindName('TitleBar') - - # Set title and message - $titleText.Text = $Title - $messageText.Text = $Message - - # Configure icon - switch ($Icon) { - 'Information' { - $iconText.Text = [char]0xE946 - $iconText.Foreground = $msgWindow.FindResource('InformationIconColor') - $iconText.Visibility = 'Visible' - } - 'Warning' { - $iconText.Text = [char]0xE7BA - $iconText.Foreground = $msgWindow.FindResource('WarningIconColor') - $iconText.Visibility = 'Visible' - } - 'Error' { - $iconText.Text = [char]0xEA39 - $iconText.Foreground = $msgWindow.FindResource('ErrorIconColor') - $iconText.Visibility = 'Visible' - } - 'Question' { - $iconText.Text = [char]0xE897 - $iconText.Foreground = $msgWindow.FindResource('QuestionIconColor') - $iconText.Visibility = 'Visible' - } - default { - $iconText.Visibility = 'Collapsed' - } - } - - # Configure buttons - store result in window's Tag property - switch ($Button) { - 'OK' { - $button1.Content = 'OK' - $button1.Add_Click({ $msgWindow.Tag = 'OK'; $msgWindow.Close() }) - $button2.Visibility = 'Collapsed' - } - 'OKCancel' { - $button1.Content = 'OK' - $button2.Content = 'Cancel' - $button1.Add_Click({ $msgWindow.Tag = 'OK'; $msgWindow.Close() }) - $button2.Add_Click({ $msgWindow.Tag = 'Cancel'; $msgWindow.Close() }) - $button2.Visibility = 'Visible' - } - 'YesNo' { - $button1.Content = 'Yes' - $button2.Content = 'No' - $button1.Add_Click({ $msgWindow.Tag = 'Yes'; $msgWindow.Close() }) - $button2.Add_Click({ $msgWindow.Tag = 'No'; $msgWindow.Close() }) - $button2.Visibility = 'Visible' - } - 'YesNoCancel' { - $button1.Content = 'Yes' - $button2.Content = 'No' - $button1.Add_Click({ $msgWindow.Tag = 'Yes'; $msgWindow.Close() }) - $button2.Add_Click({ $msgWindow.Tag = 'No'; $msgWindow.Close() }) - $button2.Visibility = 'Visible' - } - } - - # Title bar drag to move window - $titleBar.Add_MouseLeftButtonDown({ - $msgWindow.DragMove() - }) - - # Handle Escape key to close - $msgWindow.Add_KeyDown({ - param($sender, $e) - if ($e.Key -eq 'Escape') { - if ($Button -eq 'OK') { - $msgWindow.Tag = 'OK' - } else { - $msgWindow.Tag = 'Cancel' - } - $msgWindow.Close() - } - }) - - # Show dialog and return result from Tag - $msgWindow.ShowDialog() | Out-Null - - # Hide overlay after dialog closes - if ($overlay) { - try { - $ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' }) - } - catch { } - } - - return $msgWindow.Tag -} - -function Show-AboutDialog { - param ( - [Parameter(Mandatory=$false)] - [System.Windows.Window]$Owner = $null - ) - - Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null - - $usesDarkMode = GetSystemUsesDarkMode - - # Determine owner window - $ownerWindow = if ($Owner) { $Owner } else { $script:GuiWindow } - - # Show overlay if owner window exists - $overlay = $null - if ($ownerWindow) { - try { - $overlay = $ownerWindow.FindName('ModalOverlay') - if ($overlay) { - $ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Visible' }) - } - } - catch { } - } - - # Load XAML from file - $xaml = Get-Content -Path $script:AboutWindowSchema -Raw - $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml)) - try { - $aboutWindow = [System.Windows.Markup.XamlReader]::Load($reader) - } - finally { - $reader.Close() - } - - # Set owner to owner window if it exists - if ($ownerWindow) { - try { - $aboutWindow.Owner = $ownerWindow - } - catch { } - } - - # Apply theme resources - SetWindowThemeResources -window $aboutWindow -usesDarkMode $usesDarkMode - - # Get UI elements - $titleBar = $aboutWindow.FindName('TitleBar') - $versionText = $aboutWindow.FindName('VersionText') - $projectLink = $aboutWindow.FindName('ProjectLink') - $kofiLink = $aboutWindow.FindName('KofiLink') - $closeButton = $aboutWindow.FindName('CloseButton') - - # Set version - $versionText.Text = $script:Version - - # Title bar drag to move window - $titleBar.Add_MouseLeftButtonDown({ - $aboutWindow.DragMove() - }) - - # Project link click handler - $projectLink.Add_MouseLeftButtonDown({ - Start-Process "https://github.com/Raphire/Win11Debloat" - }) - - # Ko-fi link click handler - $kofiLink.Add_MouseLeftButtonDown({ - Start-Process "https://ko-fi.com/raphire" - }) - - # Close button handler - $closeButton.Add_Click({ - $aboutWindow.Close() - }) - - # Handle Escape key to close - $aboutWindow.Add_KeyDown({ - param($sender, $e) - if ($e.Key -eq 'Escape') { - $aboutWindow.Close() - } - }) - - # Show dialog - $aboutWindow.ShowDialog() | Out-Null - - # Hide overlay after dialog closes - if ($overlay) { - try { - $ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' }) - } - catch { } - } -} - - - -# Initializes and opens the main GUI window -function OpenGUI { - Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase,System.Windows.Forms | Out-Null - - # Get current Windows build version - $WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' CurrentBuild - - $usesDarkMode = GetSystemUsesDarkMode - - # Load XAML from file - $xaml = Get-Content -Path $script:MainWindowSchema -Raw - $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml)) - try { - $window = [System.Windows.Markup.XamlReader]::Load($reader) - } - finally { - $reader.Close() - } - - SetWindowThemeResources -window $window -usesDarkMode $usesDarkMode - - # Get named elements - $titleBar = $window.FindName('TitleBar') - $kofiBtn = $window.FindName('KofiBtn') - $menuBtn = $window.FindName('MenuBtn') - $closeBtn = $window.FindName('CloseBtn') - $menuDocumentation = $window.FindName('MenuDocumentation') - $menuReportBug = $window.FindName('MenuReportBug') - $menuLogs = $window.FindName('MenuLogs') - $menuAbout = $window.FindName('MenuAbout') - - # Title bar event handlers - $titleBar.Add_MouseLeftButtonDown({ - if ($_.OriginalSource -is [System.Windows.Controls.Grid] -or $_.OriginalSource -is [System.Windows.Controls.Border] -or $_.OriginalSource -is [System.Windows.Controls.TextBlock]) { - $window.DragMove() - } - }) - - $kofiBtn.Add_Click({ - Start-Process "https://ko-fi.com/raphire" - }) - - $menuBtn.Add_Click({ - $menuBtn.ContextMenu.PlacementTarget = $menuBtn - $menuBtn.ContextMenu.Placement = [System.Windows.Controls.Primitives.PlacementMode]::Bottom - $menuBtn.ContextMenu.IsOpen = $true - }) - - $menuDocumentation.Add_Click({ - Start-Process "https://github.com/Raphire/Win11Debloat/wiki" - }) - - $menuReportBug.Add_Click({ - Start-Process "https://github.com/Raphire/Win11Debloat/issues" - }) - - $menuLogs.Add_Click({ - $logPath = Join-Path $PSScriptRoot "Win11Debloat.log" - if (Test-Path $logPath) { - Start-Process "notepad.exe" -ArgumentList $logPath - } - else { - Show-ModernMessageBox -Message "No log file found at: $logPath" -Title "Logs" -Button 'OK' -Icon 'Information' - } - }) - - $menuAbout.Add_Click({ - Show-AboutDialog -Owner $window - }) - - $closeBtn.Add_Click({ - $window.Close() - }) - - # Ensure closing the main window stops all execution - $window.Add_Closing({ - $script:CancelRequested = $true - }) - - # Implement window resize functionality - $resizeLeft = $window.FindName('ResizeLeft') - $resizeRight = $window.FindName('ResizeRight') - $resizeTop = $window.FindName('ResizeTop') - $resizeBottom = $window.FindName('ResizeBottom') - $resizeTopLeft = $window.FindName('ResizeTopLeft') - $resizeTopRight = $window.FindName('ResizeTopRight') - $resizeBottomLeft = $window.FindName('ResizeBottomLeft') - $resizeBottomRight = $window.FindName('ResizeBottomRight') - - $script:resizing = $false - $script:resizeEdges = $null - $script:resizeStart = $null - $script:windowStart = $null - $script:resizeElement = $null - - $resizeHandler = { - param($sender, $e) - - $script:resizing = $true - $script:resizeElement = $sender - $script:resizeStart = [System.Windows.Forms.Cursor]::Position - $script:windowStart = @{ - Left = $window.Left - Top = $window.Top - Width = $window.ActualWidth - Height = $window.ActualHeight - } - - # Parse direction tag into edge flags for cleaner resize logic - $direction = $sender.Tag - $script:resizeEdges = @{ - Left = $direction -match 'Left' - Right = $direction -match 'Right' - Top = $direction -match 'Top' - Bottom = $direction -match 'Bottom' - } - - $sender.CaptureMouse() - $e.Handled = $true - } - - $moveHandler = { - param($sender, $e) - if (-not $script:resizing) { return } - - $current = [System.Windows.Forms.Cursor]::Position - $deltaX = $current.X - $script:resizeStart.X - $deltaY = $current.Y - $script:resizeStart.Y - - # Handle horizontal resize - if ($script:resizeEdges.Left) { - $newWidth = [Math]::Max($window.MinWidth, $script:windowStart.Width - $deltaX) - if ($newWidth -ne $window.Width) { - $window.Left = $script:windowStart.Left + ($script:windowStart.Width - $newWidth) - $window.Width = $newWidth - } - } - elseif ($script:resizeEdges.Right) { - $window.Width = [Math]::Max($window.MinWidth, $script:windowStart.Width + $deltaX) - } - - # Handle vertical resize - if ($script:resizeEdges.Top) { - $newHeight = [Math]::Max($window.MinHeight, $script:windowStart.Height - $deltaY) - if ($newHeight -ne $window.Height) { - $window.Top = $script:windowStart.Top + ($script:windowStart.Height - $newHeight) - $window.Height = $newHeight - } - } - elseif ($script:resizeEdges.Bottom) { - $window.Height = [Math]::Max($window.MinHeight, $script:windowStart.Height + $deltaY) - } - - $e.Handled = $true - } - - $releaseHandler = { - param($sender, $e) - if ($script:resizing -and $script:resizeElement) { - $script:resizing = $false - $script:resizeEdges = $null - $script:resizeElement.ReleaseMouseCapture() - $script:resizeElement = $null - $e.Handled = $true - } - } - - # Set tags and add event handlers for resize borders - $resizeLeft.Tag = 'Left' - $resizeLeft.Add_PreviewMouseLeftButtonDown($resizeHandler) - $resizeLeft.Add_MouseMove($moveHandler) - $resizeLeft.Add_MouseLeftButtonUp($releaseHandler) - - $resizeRight.Tag = 'Right' - $resizeRight.Add_PreviewMouseLeftButtonDown($resizeHandler) - $resizeRight.Add_MouseMove($moveHandler) - $resizeRight.Add_MouseLeftButtonUp($releaseHandler) - - $resizeTop.Tag = 'Top' - $resizeTop.Add_PreviewMouseLeftButtonDown($resizeHandler) - $resizeTop.Add_MouseMove($moveHandler) - $resizeTop.Add_MouseLeftButtonUp($releaseHandler) - - $resizeBottom.Tag = 'Bottom' - $resizeBottom.Add_PreviewMouseLeftButtonDown($resizeHandler) - $resizeBottom.Add_MouseMove($moveHandler) - $resizeBottom.Add_MouseLeftButtonUp($releaseHandler) - - $resizeTopLeft.Tag = 'TopLeft' - $resizeTopLeft.Add_PreviewMouseLeftButtonDown($resizeHandler) - $resizeTopLeft.Add_MouseMove($moveHandler) - $resizeTopLeft.Add_MouseLeftButtonUp($releaseHandler) - - $resizeTopRight.Tag = 'TopRight' - $resizeTopRight.Add_PreviewMouseLeftButtonDown($resizeHandler) - $resizeTopRight.Add_MouseMove($moveHandler) - $resizeTopRight.Add_MouseLeftButtonUp($releaseHandler) - - $resizeBottomLeft.Tag = 'BottomLeft' - $resizeBottomLeft.Add_PreviewMouseLeftButtonDown($resizeHandler) - $resizeBottomLeft.Add_MouseMove($moveHandler) - $resizeBottomLeft.Add_MouseLeftButtonUp($releaseHandler) - - $resizeBottomRight.Tag = 'BottomRight' - $resizeBottomRight.Add_PreviewMouseLeftButtonDown($resizeHandler) - $resizeBottomRight.Add_MouseMove($moveHandler) - $resizeBottomRight.Add_MouseLeftButtonUp($releaseHandler) - - # Integrated App Selection UI - $appsPanel = $window.FindName('AppSelectionPanel') - $onlyInstalledAppsBox = $window.FindName('OnlyInstalledAppsBox') - $loadingAppsIndicator = $window.FindName('LoadingAppsIndicator') - $appSelectionStatus = $window.FindName('AppSelectionStatus') - $defaultAppsBtn = $window.FindName('DefaultAppsBtn') - $clearAppSelectionBtn = $window.FindName('ClearAppSelectionBtn') - - # Track the last selected checkbox for shift-click range selection - $script:MainWindowLastSelectedCheckbox = $null - - # Track current app loading operation to prevent race conditions - $script:CurrentAppLoadTimer = $null - $script:CurrentAppLoadJob = $null - $script:CurrentAppLoadJobStartTime = $null - - # Apply Tab UI Elements - $consoleOutput = $window.FindName('ConsoleOutput') - $consoleScrollViewer = $window.FindName('ConsoleScrollViewer') - $finishBtn = $window.FindName('FinishBtn') - $finishBtnText = $window.FindName('FinishBtnText') - - # Set script-level variables for Write-ToConsole function - $script:GuiConsoleOutput = $consoleOutput - $script:GuiConsoleScrollViewer = $consoleScrollViewer - $script:GuiWindow = $window - - # Updates app selection status text in the App Selection tab - function UpdateAppSelectionStatus { - $selectedCount = 0 - foreach ($child in $appsPanel.Children) { - if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) { - $selectedCount++ - } - } - $appSelectionStatus.Text = "$selectedCount app(s) selected for removal" - } - - # Dynamically builds Tweaks UI from Features.json - function BuildDynamicTweaks { - $featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0" - - if (-not $featuresJson) { - Show-ModernMessageBox -Message "Unable to load Features.json file!" -Title "Error" -Button 'OK' -Icon 'Error' | Out-Null - Exit - } - - # Column containers - $col0 = $window.FindName('Column0Panel') - $col1 = $window.FindName('Column1Panel') - $col2 = $window.FindName('Column2Panel') - $columns = @($col0, $col1, $col2) | Where-Object { $_ -ne $null } - - # Clear all columns for fully dynamic panel creation - foreach ($col in $columns) { - if ($col) { $col.Children.Clear() } - } - - $script:UiControlMappings = @{} - $script:CategoryCardMap = @{} - - function CreateLabeledCombo($parent, $labelText, $comboName, $items) { - # If only 2 items (No Change + one option), use a checkbox instead - if ($items.Count -eq 2) { - $checkbox = New-Object System.Windows.Controls.CheckBox - $checkbox.Content = $labelText - $checkbox.Name = $comboName - $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $labelText) - $checkbox.IsChecked = $false - $checkbox.Style = $window.Resources["FeatureCheckboxStyle"] - $parent.Children.Add($checkbox) | Out-Null - - # Register the checkbox with the window's name scope - try { - [System.Windows.NameScope]::SetNameScope($checkbox, [System.Windows.NameScope]::GetNameScope($window)) - $window.RegisterName($comboName, $checkbox) - } - catch { - # Name might already be registered, ignore - } - - return $checkbox - } - - # Otherwise use a combobox for multiple options - # Wrap label in a Border for search highlighting - $lblBorder = New-Object System.Windows.Controls.Border - $lblBorder.Style = $window.Resources['LabelBorderStyle'] - $lblBorderName = "$comboName`_LabelBorder" - $lblBorder.Name = $lblBorderName - - $lbl = New-Object System.Windows.Controls.TextBlock - $lbl.Text = $labelText - $lbl.Style = $window.Resources['LabelStyle'] - $labelName = "$comboName`_Label" - $lbl.Name = $labelName - - $lblBorder.Child = $lbl - $parent.Children.Add($lblBorder) | Out-Null - - # Register the label border with the window's name scope - try { - [System.Windows.NameScope]::SetNameScope($lblBorder, [System.Windows.NameScope]::GetNameScope($window)) - $window.RegisterName($lblBorderName, $lblBorder) - } - catch { - # Name might already be registered, ignore - } - - $combo = New-Object System.Windows.Controls.ComboBox - $combo.Name = $comboName - $combo.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $labelText) - foreach ($it in $items) { $cbItem = New-Object System.Windows.Controls.ComboBoxItem; $cbItem.Content = $it; $combo.Items.Add($cbItem) | Out-Null } - $combo.SelectedIndex = 0 - $parent.Children.Add($combo) | Out-Null - - # Register the combo box with the window's name scope - try { - [System.Windows.NameScope]::SetNameScope($combo, [System.Windows.NameScope]::GetNameScope($window)) - $window.RegisterName($comboName, $combo) - } - catch { - # Name might already be registered, ignore - } - - return $combo - } - - function GetWikiUrlForCategory($category) { - if (-not $category) { return 'https://github.com/Raphire/Win11Debloat/wiki/Features' } - - $slug = $category.ToLowerInvariant() - $slug = $slug -replace '&', '' - $slug = $slug -replace '[^a-z0-9\s-]', '' - $slug = $slug -replace '\s', '-' - - return "https://github.com/Raphire/Win11Debloat/wiki/Features#$slug" - } - - function GetOrCreateCategoryCard($categoryObj) { - $categoryName = $categoryObj.Name - $categoryIcon = $categoryObj.Icon - - if ($script:CategoryCardMap.ContainsKey($categoryName)) { return $script:CategoryCardMap[$categoryName] } - - # Create a new card Border + StackPanel and add to shortest column - $target = $columns | Sort-Object @{Expression={$_.Children.Count}; Ascending=$true}, @{Expression={$columns.IndexOf($_)}; Ascending=$true} | Select-Object -First 1 - - $border = New-Object System.Windows.Controls.Border - $border.Style = $window.Resources['CategoryCardBorderStyle'] - $border.Tag = 'DynamicCategory' - - $panel = New-Object System.Windows.Controls.StackPanel - $safe = ($categoryName -replace '[^a-zA-Z0-9_]','_') - $panel.Name = "Category_{0}_Panel" -f $safe - - $headerRow = New-Object System.Windows.Controls.StackPanel - $headerRow.Orientation = 'Horizontal' - - # Add category icon - $icon = New-Object System.Windows.Controls.TextBlock - # Convert HTML entity to character (e.g.,  -> actual character) - if ($categoryIcon -match '&#x([0-9A-Fa-f]+);') { - $hexValue = [Convert]::ToInt32($matches[1], 16) - $icon.Text = [char]$hexValue - } - $icon.Style = $window.Resources['CategoryHeaderIcon'] - $headerRow.Children.Add($icon) | Out-Null - - $header = New-Object System.Windows.Controls.TextBlock - $header.Text = $categoryName - $header.Style = $window.Resources['CategoryHeaderTextBlock'] - $headerRow.Children.Add($header) | Out-Null - - $helpIcon = New-Object System.Windows.Controls.TextBlock - $helpIcon.Text = '(?)' - $helpIcon.Style = $window.Resources['CategoryHelpLinkTextStyle'] - - $helpBtn = New-Object System.Windows.Controls.Button - $helpBtn.Content = $helpIcon - $helpBtn.ToolTip = "Open wiki for more info on '$categoryName' tweaks" - $helpBtn.Tag = (GetWikiUrlForCategory -category $categoryName) - $helpBtn.Style = $window.Resources['CategoryHelpLinkButtonStyle'] - $helpBtn.Add_Click({ - param($sender, $e) - if ($sender.Tag) { Start-Process $sender.Tag } - }) - $headerRow.Children.Add($helpBtn) | Out-Null - - $panel.Children.Add($headerRow) | Out-Null - - $border.Child = $panel - $target.Children.Add($border) | Out-Null - - $script:CategoryCardMap[$categoryName] = $panel - return $panel - } - - # Determine categories present (from lists and features) - $categoriesPresent = @{} - if ($featuresJson.UiGroups) { - foreach ($g in $featuresJson.UiGroups) { if ($g.Category) { $categoriesPresent[$g.Category] = $true } } - } - foreach ($f in $featuresJson.Features) { if ($f.Category) { $categoriesPresent[$f.Category] = $true } } - - # Create cards in the order defined in Features.json Categories (if present) - $orderedCategories = @() - if ($featuresJson.Categories) { - foreach ($c in $featuresJson.Categories) { - $categoryName = if ($c -is [string]) { $c } else { $c.Name } - if ($categoriesPresent.ContainsKey($categoryName)) { - # Store the full category object (or create one with default icon for string categories) - $categoryObj = if ($c -is [string]) { @{Name = $c; Icon = ''} } else { $c } - $orderedCategories += $categoryObj - } - } - } else { - # For backward compatibility, create category objects from keys - foreach ($catName in $categoriesPresent.Keys) { - $orderedCategories += @{Name = $catName; Icon = ''} - } - } - - foreach ($categoryObj in $orderedCategories) { - $categoryName = $categoryObj.Name - - # Create/get card for this category - $panel = GetOrCreateCategoryCard -categoryObj $categoryObj - if (-not $panel) { continue } - - # Collect groups and features for this category, then sort by priority - $categoryItems = @() - - # Add any groups for this category - if ($featuresJson.UiGroups) { - $groupIndex = 0 - foreach ($group in $featuresJson.UiGroups) { - if ($group.Category -ne $categoryName) { $groupIndex++; continue } - $categoryItems += [PSCustomObject]@{ - Type = 'group' - Data = $group - Priority = if ($null -ne $group.Priority) { $group.Priority } else { [int]::MaxValue } - OriginalIndex = $groupIndex - } - $groupIndex++ - } - } - - # Add individual features for this category - $featureIndex = 0 - foreach ($feature in $featuresJson.Features) { - if ($feature.Category -ne $categoryName) { $featureIndex++; continue } - - # Check version and feature compatibility using Features.json - if (($feature.MinVersion -and $WinVersion -lt $feature.MinVersion) -or ($feature.MaxVersion -and $WinVersion -gt $feature.MaxVersion) -or ($feature.FeatureId -eq 'DisableModernStandbyNetworking' -and (-not $script:ModernStandbySupported))) { - $featureIndex++; continue - } - - # Skip if feature part of a group - $inGroup = $false - if ($featuresJson.UiGroups) { - foreach ($g in $featuresJson.UiGroups) { foreach ($val in $g.Values) { if ($val.FeatureIds -contains $feature.FeatureId) { $inGroup = $true; break } }; if ($inGroup) { break } } - } - if ($inGroup) { $featureIndex++; continue } - - $categoryItems += [PSCustomObject]@{ - Type = 'feature' - Data = $feature - Priority = if ($null -ne $feature.Priority) { $feature.Priority } else { [int]::MaxValue } - OriginalIndex = $featureIndex - } - $featureIndex++ - } - - # Sort by priority first, then by original index for items with same/no priority - $sortedItems = $categoryItems | Sort-Object -Property Priority, OriginalIndex - - # Render sorted items - foreach ($item in $sortedItems) { - if ($item.Type -eq 'group') { - $group = $item.Data - $items = @('No Change') + ($group.Values | ForEach-Object { $_.Label }) - $comboName = 'Group_{0}Combo' -f $group.GroupId - $combo = CreateLabeledCombo -parent $panel -labelText $group.Label -comboName $comboName -items $items - $script:UiControlMappings[$comboName] = @{ Type='group'; Values = $group.Values; Label = $group.Label } - } - elseif ($item.Type -eq 'feature') { - $feature = $item.Data - $opt = 'Apply' - if ($feature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($feature.FeatureId -match '^Enable') { $opt = 'Enable' } - $items = @('No Change', $opt) - $comboName = ("Feature_{0}_Combo" -f $feature.FeatureId) -replace '[^a-zA-Z0-9_]','' - $combo = CreateLabeledCombo -parent $panel -labelText ($feature.Action + ' ' + $feature.Label) -comboName $comboName -items $items - $script:UiControlMappings[$comboName] = @{ Type='feature'; FeatureId = $feature.FeatureId; Action = $feature.Action } - } - } - } - } - - # Helper function to complete app loading with the WinGet list - function script:LoadAppsWithList($listOfApps) { - $appsToAdd = GetAppsFromJson -OnlyInstalled:$onlyInstalledAppsBox.IsChecked -InstalledList $listOfApps -InitialCheckedFromJson:$false - - # Reset the last selected checkbox when loading a new list - $script:MainWindowLastSelectedCheckbox = $null - - # Sort apps alphabetically and add to panel - $appsToAdd | Sort-Object -Property DisplayName | ForEach-Object { - $checkbox = New-Object System.Windows.Controls.CheckBox - $checkbox.Content = $_.DisplayName - $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $_.DisplayName) - $checkbox.Tag = $_.AppId - $checkbox.IsChecked = $_.IsChecked - $checkbox.ToolTip = $_.Description - $checkbox.Style = $window.Resources["AppsPanelCheckBoxStyle"] - - # Store metadata in checkbox for later use - Add-Member -InputObject $checkbox -MemberType NoteProperty -Name "SelectedByDefault" -Value $_.SelectedByDefault - - # Add event handler to update status - $checkbox.Add_Checked({ UpdateAppSelectionStatus }) - $checkbox.Add_Unchecked({ UpdateAppSelectionStatus }) - - # Attach shift-click behavior for range selection - AttachShiftClickBehavior -checkbox $checkbox -appsPanel $appsPanel -lastSelectedCheckboxRef ([ref]$script:MainWindowLastSelectedCheckbox) -updateStatusCallback { UpdateAppSelectionStatus } - - $appsPanel.Children.Add($checkbox) | Out-Null - } - - # Hide loading indicator and navigation blocker, update status - $loadingAppsIndicator.Visibility = 'Collapsed' - - UpdateAppSelectionStatus - } - - # Loads apps into the UI - function LoadAppsIntoMainUI { - # Cancel any existing load operation to prevent race conditions - if ($script:CurrentAppLoadTimer -and $script:CurrentAppLoadTimer.IsEnabled) { - $script:CurrentAppLoadTimer.Stop() - } - if ($script:CurrentAppLoadJob) { - Remove-Job -Job $script:CurrentAppLoadJob -Force -ErrorAction SilentlyContinue - } - $script:CurrentAppLoadTimer = $null - $script:CurrentAppLoadJob = $null - $script:CurrentAppLoadJobStartTime = $null - - # Show loading indicator and navigation blocker, clear existing apps immediately - $loadingAppsIndicator.Visibility = 'Visible' - $appsPanel.Children.Clear() - - # Update navigation buttons to disable Next/Previous - UpdateNavigationButtons - - # Force UI to update and render all changes (loading indicator, blocker, disabled buttons) - $window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Render, [action]{}) - - # Schedule the actual loading work to run after UI has updated - $window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ - $listOfApps = "" - - if ($onlyInstalledAppsBox.IsChecked -and ($script:WingetInstalled -eq $true)) { - # Start job to get list of installed apps via WinGet (async helper) - $asyncJob = GetInstalledAppsViaWinget -Async - $script:CurrentAppLoadJob = $asyncJob.Job - $script:CurrentAppLoadJobStartTime = $asyncJob.StartTime - - # Create timer to poll job status without blocking UI - $script:CurrentAppLoadTimer = New-Object System.Windows.Threading.DispatcherTimer - $script:CurrentAppLoadTimer.Interval = [TimeSpan]::FromMilliseconds(100) - - $script:CurrentAppLoadTimer.Add_Tick({ - # Check if this timer was cancelled (another load started) - if (-not $script:CurrentAppLoadJob -or -not $script:CurrentAppLoadTimer -or -not $script:CurrentAppLoadJobStartTime) { - if ($script:CurrentAppLoadTimer) { $script:CurrentAppLoadTimer.Stop() } - return - } - - $elapsed = (Get-Date) - $script:CurrentAppLoadJobStartTime - - # Check if job is complete or timed out (10 seconds) - if ($script:CurrentAppLoadJob.State -eq 'Completed') { - $script:CurrentAppLoadTimer.Stop() - $listOfApps = Receive-Job -Job $script:CurrentAppLoadJob - Remove-Job -Job $script:CurrentAppLoadJob -ErrorAction SilentlyContinue - $script:CurrentAppLoadJob = $null - $script:CurrentAppLoadTimer = $null - $script:CurrentAppLoadJobStartTime = $null - - # Continue with loading apps - LoadAppsWithList $listOfApps - } - elseif ($elapsed.TotalSeconds -gt 10 -or $script:CurrentAppLoadJob.State -eq 'Failed') { - $script:CurrentAppLoadTimer.Stop() - Remove-Job -Job $script:CurrentAppLoadJob -Force -ErrorAction SilentlyContinue - $script:CurrentAppLoadJob = $null - $script:CurrentAppLoadTimer = $null - $script:CurrentAppLoadJobStartTime = $null - - # Show error that the script was unable to get list of apps from WinGet - Show-ModernMessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null - $onlyInstalledAppsBox.IsChecked = $false - - # Continue with loading all apps (unchecked now) - LoadAppsWithList "" - } - }) - - $script:CurrentAppLoadTimer.Start() - return # Exit here, timer will continue the work - } - - # If checkbox is not checked or winget not installed, load all apps immediately - LoadAppsWithList $listOfApps - }) | Out-Null - } - - # Event handlers for app selection - $onlyInstalledAppsBox.Add_Checked({ - LoadAppsIntoMainUI - }) - $onlyInstalledAppsBox.Add_Unchecked({ - LoadAppsIntoMainUI - }) - - # Quick selection buttons - only select apps actually in those categories - $defaultAppsBtn.Add_Click({ - foreach ($child in $appsPanel.Children) { - if ($child -is [System.Windows.Controls.CheckBox]) { - if ($child.SelectedByDefault -eq $true) { - $child.IsChecked = $true - } else { - $child.IsChecked = $false - } - } - } - }) - - $clearAppSelectionBtn.Add_Click({ - foreach ($child in $appsPanel.Children) { - if ($child -is [System.Windows.Controls.CheckBox]) { - $child.IsChecked = $false - } - } - }) - - # Shared search highlighting configuration - $script:SearchHighlightColor = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFF4CE")) - $script:SearchHighlightColorDark = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#4A4A2A")) - - # Helper function to get the appropriate highlight brush based on theme - function GetSearchHighlightBrush { - if ($usesDarkMode) { return $script:SearchHighlightColorDark } - return $script:SearchHighlightColor - } - - # Helper function to scroll to an item if it's not visible, centering it in the viewport - function ScrollToItemIfNotVisible { - param ( - [System.Windows.Controls.ScrollViewer]$scrollViewer, - [System.Windows.UIElement]$item, - [System.Windows.UIElement]$container - ) - - if (-not $scrollViewer -or -not $item -or -not $container) { return } - - try { - $itemPosition = $item.TransformToAncestor($container).Transform([System.Windows.Point]::new(0, 0)).Y - $viewportHeight = $scrollViewer.ViewportHeight - $itemHeight = $item.ActualHeight - $currentOffset = $scrollViewer.VerticalOffset - - # Check if the item is currently visible in the viewport - $itemTop = $itemPosition - $currentOffset - $itemBottom = $itemTop + $itemHeight - - $isVisible = ($itemTop -ge 0) -and ($itemBottom -le $viewportHeight) - - # Only scroll if the item is not visible - if (-not $isVisible) { - # Center the item in the viewport - $targetOffset = $itemPosition - ($viewportHeight / 2) + ($itemHeight / 2) - $scrollViewer.ScrollToVerticalOffset([Math]::Max(0, $targetOffset)) - } - } - catch { - # Fallback to simple bring into view - $item.BringIntoView() - } - } - - # Helper function to find the parent ScrollViewer of an element - function FindParentScrollViewer { - param ([System.Windows.UIElement]$element) - - $parent = [System.Windows.Media.VisualTreeHelper]::GetParent($element) - while ($null -ne $parent) { - if ($parent -is [System.Windows.Controls.ScrollViewer]) { - return $parent - } - $parent = [System.Windows.Media.VisualTreeHelper]::GetParent($parent) - } - return $null - } - - # App Search Box functionality - $appSearchBox = $window.FindName('AppSearchBox') - $appSearchPlaceholder = $window.FindName('AppSearchPlaceholder') - - $appSearchBox.Add_TextChanged({ - $searchText = $appSearchBox.Text.ToLower().Trim() - - # Show/hide placeholder - $appSearchPlaceholder.Visibility = if ([string]::IsNullOrWhiteSpace($appSearchBox.Text)) { 'Visible' } else { 'Collapsed' } - - # Clear all highlights first - foreach ($child in $appsPanel.Children) { - if ($child -is [System.Windows.Controls.CheckBox]) { - $child.Background = [System.Windows.Media.Brushes]::Transparent - } - } - - if ([string]::IsNullOrWhiteSpace($searchText)) { return } - - # Find and highlight all matching apps - $firstMatch = $null - $highlightBrush = GetSearchHighlightBrush - - foreach ($child in $appsPanel.Children) { - if ($child -is [System.Windows.Controls.CheckBox] -and $child.Visibility -eq 'Visible') { - if ($child.Content.ToString().ToLower().Contains($searchText)) { - $child.Background = $highlightBrush - if ($null -eq $firstMatch) { $firstMatch = $child } - } - } - } - - # Scroll to first match if not visible - if ($firstMatch) { - $scrollViewer = FindParentScrollViewer -element $appsPanel - if ($scrollViewer) { - ScrollToItemIfNotVisible -scrollViewer $scrollViewer -item $firstMatch -container $appsPanel - } - } - }) - - # Tweak Search Box functionality - $tweakSearchBox = $window.FindName('TweakSearchBox') - $tweakSearchPlaceholder = $window.FindName('TweakSearchPlaceholder') - $tweakSearchBorder = $window.FindName('TweakSearchBorder') - $tweaksScrollViewer = $window.FindName('TweaksScrollViewer') - $tweaksGrid = $window.FindName('TweaksGrid') - $col0 = $window.FindName('Column0Panel') - $col1 = $window.FindName('Column1Panel') - $col2 = $window.FindName('Column2Panel') - - # Monitor scrollbar visibility and adjust searchbar margin - $tweaksScrollViewer.Add_ScrollChanged({ - if ($tweaksScrollViewer.ScrollableHeight -gt 0) { - # The 17px accounts for the scrollbar width + some padding - $tweakSearchBorder.Margin = [System.Windows.Thickness]::new(0, 0, 17, 0) - } else { - $tweakSearchBorder.Margin = [System.Windows.Thickness]::new(0, 0, 0, 0) - } - }) - - # Helper function to clear all tweak highlights - function ClearTweakHighlights { - $columns = @($col0, $col1, $col2) | Where-Object { $_ -ne $null } - foreach ($column in $columns) { - foreach ($card in $column.Children) { - if ($card -is [System.Windows.Controls.Border] -and $card.Child -is [System.Windows.Controls.StackPanel]) { - foreach ($control in $card.Child.Children) { - if ($control -is [System.Windows.Controls.CheckBox] -or - ($control -is [System.Windows.Controls.Border] -and $control.Name -like '*_LabelBorder')) { - $control.Background = [System.Windows.Media.Brushes]::Transparent - } - } - } - } - } - } - - # Helper function to check if a ComboBox contains matching items - function ComboBoxContainsMatch { - param ([System.Windows.Controls.ComboBox]$comboBox, [string]$searchText) - - foreach ($item in $comboBox.Items) { - $itemText = if ($item -is [System.Windows.Controls.ComboBoxItem]) { $item.Content.ToString().ToLower() } else { $item.ToString().ToLower() } - if ($itemText.Contains($searchText)) { return $true } - } - return $false - } - - $tweakSearchBox.Add_TextChanged({ - $searchText = $tweakSearchBox.Text.ToLower().Trim() - - # Show/hide placeholder - $tweakSearchPlaceholder.Visibility = if ([string]::IsNullOrWhiteSpace($tweakSearchBox.Text)) { 'Visible' } else { 'Collapsed' } - - # Clear all highlights - ClearTweakHighlights - - if ([string]::IsNullOrWhiteSpace($searchText)) { return } - - # Find and highlight all matching tweaks - $firstMatch = $null - $highlightBrush = GetSearchHighlightBrush - $columns = @($col0, $col1, $col2) | Where-Object { $_ -ne $null } - - foreach ($column in $columns) { - foreach ($card in $column.Children) { - if ($card -is [System.Windows.Controls.Border] -and $card.Child -is [System.Windows.Controls.StackPanel]) { - $controlsList = @($card.Child.Children) - for ($i = 0; $i -lt $controlsList.Count; $i++) { - $control = $controlsList[$i] - $matchFound = $false - $controlToHighlight = $null - - if ($control -is [System.Windows.Controls.CheckBox]) { - if ($control.Content.ToString().ToLower().Contains($searchText)) { - $matchFound = $true - $controlToHighlight = $control - } - } - elseif ($control -is [System.Windows.Controls.Border] -and $control.Name -like '*_LabelBorder') { - $labelText = if ($control.Child) { $control.Child.Text.ToLower() } else { "" } - $comboBox = if ($i + 1 -lt $controlsList.Count -and $controlsList[$i + 1] -is [System.Windows.Controls.ComboBox]) { $controlsList[$i + 1] } else { $null } - - # Check label text or combo box items - if ($labelText.Contains($searchText) -or ($comboBox -and (ComboBoxContainsMatch -comboBox $comboBox -searchText $searchText))) { - $matchFound = $true - $controlToHighlight = $control - } - } - - if ($matchFound -and $controlToHighlight) { - $controlToHighlight.Background = $highlightBrush - if ($null -eq $firstMatch) { $firstMatch = $controlToHighlight } - } - } - } - } - } - - # Scroll to first match if not visible - if ($firstMatch -and $tweaksScrollViewer) { - ScrollToItemIfNotVisible -scrollViewer $tweaksScrollViewer -item $firstMatch -container $tweaksGrid - } - }) - - # Add Ctrl+F keyboard shortcut to focus search box on current tab - $window.Add_KeyDown({ - param($sender, $e) - - # Check if Ctrl+F was pressed - if ($e.Key -eq [System.Windows.Input.Key]::F -and - ([System.Windows.Input.Keyboard]::Modifiers -band [System.Windows.Input.ModifierKeys]::Control)) { - - $currentTab = $tabControl.SelectedItem - - # Focus AppSearchBox if on App Removal tab - if ($currentTab.Header -eq "App Removal" -and $appSearchBox) { - $appSearchBox.Focus() - $e.Handled = $true - } - # Focus TweakSearchBox if on Tweaks tab - elseif ($currentTab.Header -eq "Tweaks" -and $tweakSearchBox) { - $tweakSearchBox.Focus() - $e.Handled = $true - } - } - }) - - # Wizard Navigation - $tabControl = $window.FindName('MainTabControl') - $previousBtn = $window.FindName('PreviousBtn') - $nextBtn = $window.FindName('NextBtn') - $userSelectionCombo = $window.FindName('UserSelectionCombo') - $userSelectionDescription = $window.FindName('UserSelectionDescription') - $otherUserPanel = $window.FindName('OtherUserPanel') - $otherUsernameTextBox = $window.FindName('OtherUsernameTextBox') - $usernameTextBoxPlaceholder = $window.FindName('UsernameTextBoxPlaceholder') - $usernameValidationMessage = $window.FindName('UsernameValidationMessage') - $appRemovalScopeCombo = $window.FindName('AppRemovalScopeCombo') - $appRemovalScopeDescription = $window.FindName('AppRemovalScopeDescription') - $appRemovalScopeSection = $window.FindName('AppRemovalScopeSection') - $appRemovalScopeCurrentUser = $window.FindName('AppRemovalScopeCurrentUser') - $appRemovalScopeTargetUser = $window.FindName('AppRemovalScopeTargetUser') - - # Navigation button handlers - function UpdateNavigationButtons { - $currentIndex = $tabControl.SelectedIndex - $totalTabs = $tabControl.Items.Count - - $homeIndex = 0 - $overviewIndex = $totalTabs - 2 - $applyIndex = $totalTabs - 1 - - # Navigation button visibility - if ($currentIndex -eq $homeIndex) { - $nextBtn.Visibility = 'Collapsed' - $previousBtn.Visibility = 'Collapsed' - } elseif ($currentIndex -eq $overviewIndex) { - $nextBtn.Visibility = 'Collapsed' - $previousBtn.Visibility = 'Visible' - } elseif ($currentIndex -eq $applyIndex) { - $nextBtn.Visibility = 'Collapsed' - $previousBtn.Visibility = 'Collapsed' - } else { - $nextBtn.Visibility = 'Visible' - $previousBtn.Visibility = 'Visible' - } - - # Update progress indicators - # Tab indices: 0=Home, 1=App Removal, 2=Tweaks, 3=Overview, 4=Apply - $blueColor = "#0067c0" - $greyColor = "#808080" - - $progressIndicator1 = $window.FindName('ProgressIndicator1') # App Removal - $progressIndicator2 = $window.FindName('ProgressIndicator2') # Tweaks - $progressIndicator3 = $window.FindName('ProgressIndicator3') # Overview - $bottomNavGrid = $window.FindName('BottomNavGrid') - - # Hide bottom navigation on home page and apply tab - if ($currentIndex -eq 0 -or $currentIndex -eq $applyIndex) { - $bottomNavGrid.Visibility = 'Collapsed' - } else { - $bottomNavGrid.Visibility = 'Visible' - } - - # Update indicator colors based on current tab - # Indicator 1 (App Removal) - tab index 1 - if ($currentIndex -ge 1) { - $progressIndicator1.Fill = $blueColor - } else { - $progressIndicator1.Fill = $greyColor - } - - # Indicator 2 (Tweaks) - tab index 2 - if ($currentIndex -ge 2) { - $progressIndicator2.Fill = $blueColor - } else { - $progressIndicator2.Fill = $greyColor - } - - # Indicator 3 (Overview) - tab index 3 - if ($currentIndex -ge 3) { - $progressIndicator3.Fill = $blueColor - } else { - $progressIndicator3.Fill = $greyColor - } - } - - # Update user selection description and show/hide other user panel - $userSelectionCombo.Add_SelectionChanged({ - switch ($userSelectionCombo.SelectedIndex) { - 0 { - $userSelectionDescription.Text = "Changes will be applied to the currently logged-in user profile." - $otherUserPanel.Visibility = 'Collapsed' - $usernameValidationMessage.Text = "" - # Show "Current user only" option, hide "Target user only" option - $appRemovalScopeCurrentUser.Visibility = 'Visible' - $appRemovalScopeTargetUser.Visibility = 'Collapsed' - # Enable app removal scope selection for current user - $appRemovalScopeCombo.IsEnabled = $true - $appRemovalScopeCombo.SelectedIndex = 0 - } - 1 { - $userSelectionDescription.Text = "Changes will be applied to a different user profile on this system." - $otherUserPanel.Visibility = 'Visible' - $usernameValidationMessage.Text = "" - # Hide "Current user only" option, show "Target user only" option - $appRemovalScopeCurrentUser.Visibility = 'Collapsed' - $appRemovalScopeTargetUser.Visibility = 'Visible' - # Enable app removal scope selection for other user - $appRemovalScopeCombo.IsEnabled = $true - $appRemovalScopeCombo.SelectedIndex = 0 - } - 2 { - $userSelectionDescription.Text = "Changes will be applied to the default user template, affecting all new users created after this point. Useful for Sysprep deployment." - $otherUserPanel.Visibility = 'Collapsed' - $usernameValidationMessage.Text = "" - # Hide other user options since they don't apply to default user template - $appRemovalScopeCurrentUser.Visibility = 'Collapsed' - $appRemovalScopeTargetUser.Visibility = 'Collapsed' - # Lock app removal scope to "All users" when applying to sysprep - $appRemovalScopeCombo.IsEnabled = $false - $appRemovalScopeCombo.SelectedIndex = 0 - } - } - }) - - # Helper function to update app removal scope description - function UpdateAppRemovalScopeDescription { - $selectedItem = $appRemovalScopeCombo.SelectedItem - if ($selectedItem) { - switch ($selectedItem.Content) { - "All users" { - $appRemovalScopeDescription.Text = "Apps will be removed for all users and from the Windows image to prevent reinstallation for new users." - } - "Current user only" { - $appRemovalScopeDescription.Text = "Apps will only be removed for the current user. Other users and new users will not be affected." - } - "Target user only" { - $appRemovalScopeDescription.Text = "Apps will only be removed for the specified target user. Other users and new users will not be affected." - } - } - } - } - - # Update app removal scope description - $appRemovalScopeCombo.Add_SelectionChanged({ - UpdateAppRemovalScopeDescription - }) - - $otherUsernameTextBox.Add_TextChanged({ - # Show/hide placeholder - if ([string]::IsNullOrWhiteSpace($otherUsernameTextBox.Text)) { - $usernameTextBoxPlaceholder.Visibility = 'Visible' - } else { - $usernameTextBoxPlaceholder.Visibility = 'Collapsed' - } - - ValidateOtherUsername - }) - - function ValidateOtherUsername { - # Only validate if "Other User" is selected - if ($userSelectionCombo.SelectedIndex -ne 1) { - return $true - } - - $username = $otherUsernameTextBox.Text.Trim() - - $errorBrush = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#c42b1c")) - $successBrush = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#28a745")) - - if ($username.Length -eq 0) { - $usernameValidationMessage.Text = "[X] Please enter a username" - $usernameValidationMessage.Foreground = $errorBrush - return $false - } - - if ($username -eq $env:USERNAME) { - $usernameValidationMessage.Text = "[X] Cannot enter your own username, use 'Current User' option instead" - $usernameValidationMessage.Foreground = $errorBrush - return $false - } - - $userExists = CheckIfUserExists -Username $username - - if ($userExists) { - $usernameValidationMessage.Text = "[OK] User found: $username" - $usernameValidationMessage.Foreground = $successBrush - return $true - } - - $usernameValidationMessage.Text = "[X] User not found, please enter a valid username" - $usernameValidationMessage.Foreground = $errorBrush - return $false - } - - function GenerateOverview { - # Load Features.json - $featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0" - $overviewChangesPanel = $window.FindName('OverviewChangesPanel') - $overviewChangesPanel.Children.Clear() - - $changesList = @() - - # Collect selected apps - $selectedAppsCount = 0 - foreach ($child in $appsPanel.Children) { - if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) { - $selectedAppsCount++ - } - } - if ($selectedAppsCount -gt 0) { - $changesList += "Remove $selectedAppsCount selected application(s)" - } - - # Update app removal scope section based on whether apps are selected - if ($selectedAppsCount -gt 0) { - # Enable app removal scope selection (unless locked by sysprep mode) - if ($userSelectionCombo.SelectedIndex -ne 2) { - $appRemovalScopeCombo.IsEnabled = $true - } - $appRemovalScopeSection.Opacity = 1.0 - UpdateAppRemovalScopeDescription - } - else { - # Disable app removal scope selection when no apps selected - $appRemovalScopeCombo.IsEnabled = $false - $appRemovalScopeSection.Opacity = 0.5 - $appRemovalScopeDescription.Text = "No apps selected for removal." - } - - # Collect all ComboBox/CheckBox selections from dynamically created controls - if ($script:UiControlMappings) { - foreach ($mappingKey in $script:UiControlMappings.Keys) { - $control = $window.FindName($mappingKey) - $isSelected = $false - - # Check if it's a checkbox or combobox - if ($control -is [System.Windows.Controls.CheckBox]) { - $isSelected = $control.IsChecked -eq $true - } - elseif ($control -is [System.Windows.Controls.ComboBox]) { - $isSelected = $control.SelectedIndex -gt 0 - } - - if ($control -and $isSelected) { - $mapping = $script:UiControlMappings[$mappingKey] - if ($mapping.Type -eq 'group') { - # For combobox: SelectedIndex 0 = No Change, so subtract 1 to index into Values - $selectedValue = $mapping.Values[$control.SelectedIndex - 1] - foreach ($fid in $selectedValue.FeatureIds) { - $feature = $featuresJson.Features | Where-Object { $_.FeatureId -eq $fid } - if ($feature) { $changesList += ($feature.Action + ' ' + $feature.Label) } - } - } - elseif ($mapping.Type -eq 'feature') { - $feature = $featuresJson.Features | Where-Object { $_.FeatureId -eq $mapping.FeatureId } - if ($feature) { $changesList += ($feature.Action + ' ' + $feature.Label) } - } - } - } - } - - if ($changesList.Count -eq 0) { - $textBlock = New-Object System.Windows.Controls.TextBlock - $textBlock.Text = "No changes selected" - $textBlock.Style = $window.Resources["OverviewNoChangesTextStyle"] - $overviewChangesPanel.Children.Add($textBlock) | Out-Null - } - else { - foreach ($change in $changesList) { - $bullet = New-Object System.Windows.Controls.TextBlock - $bullet.Text = "- $change" - $bullet.Style = $window.Resources["OverviewChangeBulletStyle"] - $overviewChangesPanel.Children.Add($bullet) | Out-Null - } - } - } - - $previousBtn.Add_Click({ - if ($tabControl.SelectedIndex -gt 0) { - $tabControl.SelectedIndex-- - UpdateNavigationButtons - } - }) - - $nextBtn.Add_Click({ - if ($tabControl.SelectedIndex -lt ($tabControl.Items.Count - 1)) { - $tabControl.SelectedIndex++ - - UpdateNavigationButtons - } - }) - - # Handle Home Start button - $homeStartBtn = $window.FindName('HomeStartBtn') - $homeStartBtn.Add_Click({ - # Navigate to first tab after home (App Removal) - $tabControl.SelectedIndex = 1 - UpdateNavigationButtons - }) - - # Handle Overview Apply Changes button - validates and immediately starts applying changes - $overviewApplyBtn = $window.FindName('OverviewApplyBtn') - $overviewApplyBtn.Add_Click({ - if (-not (ValidateOtherUsername)) { - Show-ModernMessageBox -Message "Please enter a valid username." -Title "Invalid Username" -Button 'OK' -Icon 'Warning' | Out-Null - return - } - - # App Removal - collect selected apps from integrated UI - $selectedApps = @() - foreach ($child in $appsPanel.Children) { - if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) { - $selectedApps += $child.Tag - } - } - - if ($selectedApps.Count -gt 0) { - # Check if Microsoft Store is selected - if ($selectedApps -contains "Microsoft.WindowsStore") { - $result = Show-ModernMessageBox -Message 'Are you sure you wish to uninstall the Microsoft Store? This app cannot easily be reinstalled.' -Title 'Are you sure?' -Button 'YesNo' -Icon 'Warning' - - if ($result -eq 'No') { - return - } - } - - AddParameter 'RemoveApps' - AddParameter 'Apps' ($selectedApps -join ',') - - # Add app removal target parameter based on selection - $selectedScopeItem = $appRemovalScopeCombo.SelectedItem - if ($selectedScopeItem) { - switch ($selectedScopeItem.Content) { - "All users" { - AddParameter 'AppRemovalTarget' 'AllUsers' - } - "Current user only" { - AddParameter 'AppRemovalTarget' 'CurrentUser' - } - "Target user only" { - # Use the target username from Other User panel - AddParameter 'AppRemovalTarget' ($otherUsernameTextBox.Text.Trim()) - } - } - } - } - - # Apply dynamic tweaks selections - if ($script:UiControlMappings) { - foreach ($mappingKey in $script:UiControlMappings.Keys) { - $control = $window.FindName($mappingKey) - $isSelected = $false - $selectedIndex = 0 - - # Check if it's a checkbox or combobox - if ($control -is [System.Windows.Controls.CheckBox]) { - $isSelected = $control.IsChecked -eq $true - $selectedIndex = if ($isSelected) { 1 } else { 0 } - } - elseif ($control -is [System.Windows.Controls.ComboBox]) { - $isSelected = $control.SelectedIndex -gt 0 - $selectedIndex = $control.SelectedIndex - } - - if ($control -and $isSelected) { - $mapping = $script:UiControlMappings[$mappingKey] - if ($mapping.Type -eq 'group') { - if ($selectedIndex -gt 0 -and $selectedIndex -le $mapping.Values.Count) { - $selectedValue = $mapping.Values[$selectedIndex - 1] - foreach ($fid in $selectedValue.FeatureIds) { - AddParameter $fid - } - } - } - elseif ($mapping.Type -eq 'feature') { - AddParameter $mapping.FeatureId - } - } - } - } - - $controlParamsCount = 0 - foreach ($Param in $script:ControlParams) { - if ($script:Params.ContainsKey($Param)) { - $controlParamsCount++ - } - } - - # Check if any changes were selected - $totalChanges = $script:Params.Count - $controlParamsCount - - # Apps parameter does not count as a change itself - if ($script:Params.ContainsKey('Apps')) { - $totalChanges = $totalChanges - 1 - } - - if ($totalChanges -eq 0) { - Show-ModernMessageBox -Message 'No changes have been selected, please select at least one option to proceed.' -Title 'No Changes Selected' -Button 'OK' -Icon 'Information' - return - } - - # Check RestorePointCheckBox - $restorePointCheckBox = $window.FindName('RestorePointCheckBox') - if ($restorePointCheckBox -and $restorePointCheckBox.IsChecked) { - AddParameter 'CreateRestorePoint' - } - - # Store selected user mode - switch ($userSelectionCombo.SelectedIndex) { - 1 { AddParameter User ($otherUsernameTextBox.Text.Trim()) } - 2 { AddParameter Sysprep } - } - - SaveSettings - - # Navigate to Apply tab (last tab) and start applying changes - $tabControl.SelectedIndex = $tabControl.Items.Count - 1 - - # Clear console and set initial status - $consoleOutput.Text = "" - - Write-ToConsole "Applying changes to $(if ($script:Params.ContainsKey("Sysprep")) { "default user template" } else { "user $(GetUserName)" })" - Write-ToConsole "Total changes to apply: $totalChanges" - Write-ToConsole "" - - # Run changes in background to keep UI responsive - $window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ - try { - ExecuteAllChanges - - # Check if user wants to restart explorer (from checkbox) - $restartExplorerCheckBox = $window.FindName('RestartExplorerCheckBox') - if ($restartExplorerCheckBox -and $restartExplorerCheckBox.IsChecked -and -not $script:CancelRequested) { - RestartExplorer - } - - Write-ToConsole "" - if ($script:CancelRequested) { - Write-ToConsole "Script execution was cancelled by the user. Some changes may not have been applied." - } else { - Write-ToConsole "All changes have been applied. Please check the output above for any errors." - } - - $finishBtn.Dispatcher.Invoke([action]{ - $finishBtn.IsEnabled = $true - $finishBtnText.Text = "Close Win11Debloat" - }) - } - catch { - Write-ToConsole "Error: $($_.Exception.Message)" - $finishBtn.Dispatcher.Invoke([action]{ - $finishBtn.IsEnabled = $true - $finishBtnText.Text = "Close Win11Debloat" - }) - } - }) - }) - - # Initialize UI elements on window load - $window.Add_Loaded({ - BuildDynamicTweaks - - LoadAppsIntoMainUI - - # Update Current User label with username - if ($userSelectionCombo -and $userSelectionCombo.Items.Count -gt 0) { - $currentUserItem = $userSelectionCombo.Items[0] - if ($currentUserItem -is [System.Windows.Controls.ComboBoxItem]) { - $currentUserItem.Content = "Current User ($(GetUserName))" - } - } - - # Disable Restart Explorer option if NoRestartExplorer parameter is set - $restartExplorerCheckBox = $window.FindName('RestartExplorerCheckBox') - if ($restartExplorerCheckBox -and $script:Params.ContainsKey("NoRestartExplorer")) { - $restartExplorerCheckBox.IsChecked = $false - $restartExplorerCheckBox.IsEnabled = $false - } - - # Force Apply Changes To setting if Sysprep or User parameters are set - if ($script:Params.ContainsKey("Sysprep")) { - $userSelectionCombo.SelectedIndex = 2 - $userSelectionCombo.IsEnabled = $false - } - elseif ($script:Params.ContainsKey("User")) { - $userSelectionCombo.SelectedIndex = 1 - $userSelectionCombo.IsEnabled = $false - $otherUsernameTextBox.Text = $script:Params.Item("User") - $otherUsernameTextBox.IsEnabled = $false - } - - UpdateNavigationButtons - }) - - # Add event handler for tab changes - $tabControl.Add_SelectionChanged({ - # Regenerate overview when switching to Overview tab - if ($tabControl.SelectedIndex -eq ($tabControl.Items.Count - 2)) { - GenerateOverview - } - UpdateNavigationButtons - }) - - # Handle Load Defaults button - $loadDefaultsBtn = $window.FindName('LoadDefaultsBtn') - $loadDefaultsBtn.Add_Click({ - $defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0" - - if (-not $defaultsJson) { - Show-ModernMessageBox -Message "Failed to load default settings file" -Title "Error" -Button 'OK' -Icon 'Error' - return - } - - ApplySettingsToUiControls -window $window -settingsJson $defaultsJson -uiControlMappings $script:UiControlMappings - }) - - # Handle Load Last Used settings and Load Last Used apps - $loadLastUsedBtn = $window.FindName('LoadLastUsedBtn') - $loadLastUsedAppsBtn = $window.FindName('LoadLastUsedAppsBtn') - - $lastUsedSettingsJson = LoadJsonFile -filePath $script:SavedSettingsFilePath -expectedVersion "1.0" -optionalFile - - $hasSettings = $false - $appsSetting = $null - if ($lastUsedSettingsJson -and $lastUsedSettingsJson.Settings) { - foreach ($s in $lastUsedSettingsJson.Settings) { - # Only count as hasSettings if a setting other than RemoveApps/Apps is present and true - if ($s.Value -eq $true -and $s.Name -ne 'RemoveApps' -and $s.Name -ne 'Apps') { $hasSettings = $true } - if ($s.Name -eq 'Apps' -and $s.Value) { $appsSetting = $s.Value } - } - } - - # Show option to load last used settings if they exist - if ($hasSettings) { - $loadLastUsedBtn.Add_Click({ - try { - ApplySettingsToUiControls -window $window -settingsJson $lastUsedSettingsJson -uiControlMappings $script:UiControlMappings - } - catch { - Show-ModernMessageBox -Message "Failed to load last used settings: $_" -Title "Error" -Button 'OK' -Icon 'Error' - } - }) - } - else { - $loadLastUsedBtn.Visibility = 'Collapsed' - } - - # Show option to load last used apps if they exist - if ($appsSetting -and $appsSetting.ToString().Trim().Length -gt 0) { - $loadLastUsedAppsBtn.Add_Click({ - try { - $savedApps = @() - if ($appsSetting -is [string]) { $savedApps = $appsSetting.Split(',') } - elseif ($appsSetting -is [array]) { $savedApps = $appsSetting } - $savedApps = $savedApps | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } - - foreach ($child in $appsPanel.Children) { - if ($child -is [System.Windows.Controls.CheckBox]) { - if ($savedApps -contains $child.Tag) { $child.IsChecked = $true } else { $child.IsChecked = $false } - } - } - } - catch { - Show-ModernMessageBox -Message "Failed to load last used app selection: $_" -Title "Error" -Button 'OK' -Icon 'Error' - } - }) - } - else { - $loadLastUsedAppsBtn.Visibility = 'Collapsed' - } - - # Clear All Tweaks button - $clearAllTweaksBtn = $window.FindName('ClearAllTweaksBtn') - $clearAllTweaksBtn.Add_Click({ - # Reset all ComboBoxes to index 0 (No Change) and uncheck all CheckBoxes - if ($script:UiControlMappings) { - foreach ($comboName in $script:UiControlMappings.Keys) { - $control = $window.FindName($comboName) - if ($control -is [System.Windows.Controls.CheckBox]) { - $control.IsChecked = $false - } - elseif ($control -is [System.Windows.Controls.ComboBox]) { - $control.SelectedIndex = 0 - } - } - } - - # Also uncheck RestorePointCheckBox - $restorePointCheckBox = $window.FindName('RestorePointCheckBox') - if ($restorePointCheckBox) { - $restorePointCheckBox.IsChecked = $false - } - }) - - # Finish (Close Win11Debloat) button handler - $finishBtn.Add_Click({ - $window.Close() - }) - - # Show the window - return $window.ShowDialog() -} - - -# Shows application selection window that allows the user to select what apps they want to remove or keep -function OpenAppSelectionWindow { - Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null - - $usesDarkMode = GetSystemUsesDarkMode - - # Load XAML from file - $xaml = Get-Content -Path $script:AppSelectionSchema -Raw - $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml)) - try { - $window = [System.Windows.Markup.XamlReader]::Load($reader) - } - finally { - $reader.Close() - } - - SetWindowThemeResources -window $window -usesDarkMode $usesDarkMode - - $appsPanel = $window.FindName('AppsPanel') - $checkAllBox = $window.FindName('CheckAllBox') - $onlyInstalledBox = $window.FindName('OnlyInstalledBox') - $confirmBtn = $window.FindName('ConfirmBtn') - $loadingIndicator = $window.FindName('LoadingAppsIndicator') - $titleBar = $window.FindName('TitleBar') - - # Track the last selected checkbox for shift-click range selection - $script:AppSelectionWindowLastSelectedCheckbox = $null - - # Loads apps into the apps UI - function LoadApps { - # Show loading indicator - $loadingIndicator.Visibility = 'Visible' - $window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{}) - - $appsPanel.Children.Clear() - $listOfApps = "" - - if ($onlyInstalledBox.IsChecked -and ($script:WingetInstalled -eq $true)) { - # Attempt to get a list of installed apps via WinGet, times out after 10 seconds - $listOfApps = GetInstalledAppsViaWinget -TimeOut 10 - if (-not $listOfApps) { - # Show error that the script was unable to get list of apps from WinGet - Show-ModernMessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' -Owner $window | Out-Null - $onlyInstalledBox.IsChecked = $false - } - } - - $appsToAdd = GetAppsFromJson -OnlyInstalled:$onlyInstalledBox.IsChecked -InstalledList $listOfApps -InitialCheckedFromJson - - # Reset the last selected checkbox when loading a new list - $script:AppSelectionWindowLastSelectedCheckbox = $null - - # Sort apps alphabetically and add to panel - $appsToAdd | Sort-Object -Property DisplayName | ForEach-Object { - $checkbox = New-Object System.Windows.Controls.CheckBox - $checkbox.Content = $_.DisplayName - $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $_.DisplayName) - $checkbox.Tag = $_.AppId - $checkbox.IsChecked = $_.IsChecked - $checkbox.ToolTip = $_.Description - $checkbox.Style = $window.Resources["AppsPanelCheckBoxStyle"] - - # Attach shift-click behavior for range selection - AttachShiftClickBehavior -checkbox $checkbox -appsPanel $appsPanel -lastSelectedCheckboxRef ([ref]$script:AppSelectionWindowLastSelectedCheckbox) - - $appsPanel.Children.Add($checkbox) | Out-Null - } - - # Hide loading indicator - $loadingIndicator.Visibility = 'Collapsed' - } - - # Event handlers - $titleBar.Add_MouseLeftButtonDown({ - $window.DragMove() - }) - - $checkAllBox.Add_Checked({ - foreach ($child in $appsPanel.Children) { - if ($child -is [System.Windows.Controls.CheckBox]) { - $child.IsChecked = $true - } - } - }) - - $checkAllBox.Add_Unchecked({ - foreach ($child in $appsPanel.Children) { - if ($child -is [System.Windows.Controls.CheckBox]) { - $child.IsChecked = $false - } - } - }) - - $onlyInstalledBox.Add_Checked({ LoadApps }) - $onlyInstalledBox.Add_Unchecked({ LoadApps }) - - $confirmBtn.Add_Click({ - $selectedApps = @() - foreach ($child in $appsPanel.Children) { - if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) { - $selectedApps += $child.Tag - } - } - - # Close form without saving if no apps were selected - if ($selectedApps.Count -eq 0) { - $window.Close() - return - } - - if ($selectedApps -contains "Microsoft.WindowsStore" -and -not $Silent) { - $result = Show-ModernMessageBox -Message 'Are you sure you wish to uninstall the Microsoft Store? This app cannot easily be reinstalled.' -Title 'Are you sure?' -Button 'YesNo' -Icon 'Warning' -Owner $window - - if ($result -eq 'No') { - return - } - } - - SaveCustomAppsListToFile -appsList $selectedApps - - $window.DialogResult = $true - }) - - # Load apps after window is shown (allows UI to render first) - $window.Add_ContentRendered({ - $window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ LoadApps }) - }) - - # Show the window and return dialog result - return $window.ShowDialog() -} - - -# Saves the provided appsList to the CustomAppsList file -function SaveCustomAppsListToFile { - param ( - $appsList - ) - - $script:SelectedApps = $appsList - - # Create file that stores selected apps if it doesn't exist - if (-not (Test-Path $script:CustomAppsListFilePath)) { - $null = New-Item $script:CustomAppsListFilePath -ItemType File - } - - Set-Content -Path $script:CustomAppsListFilePath -Value $script:SelectedApps -} - - -# Returns a validated list of apps based on the provided appsList and the supported apps from Apps.json -function ValidateAppslist { - param ( - $appsList - ) - - $supportedAppsList = (GetAppsFromJson | ForEach-Object { $_.AppId }) - $validatedAppsList = @() - - # Validate provided appsList against supportedAppsList - Foreach ($app in $appsList) { - $app = $app.Trim() - $appString = $app.Trim('*') - - if ($supportedAppsList -notcontains $appString) { - Write-Host "Removal of app '$appString' is not supported and will be skipped" -ForegroundColor Yellow - continue - } - - $validatedAppsList += $appString - } - - return $validatedAppsList -} - - -# Returns list of apps from the specified file, it trims the app names and removes any comments -function ReadAppslistFromFile { - param ( - $appsFilePath - ) - - $appsList = @() - - if (-not (Test-Path $appsFilePath)) { - return $appsList - } - - try { - # Check if file is JSON or text format - if ($appsFilePath -like "*.json") { - # JSON file format - $jsonContent = Get-Content -Path $appsFilePath -Raw | ConvertFrom-Json - Foreach ($appData in $jsonContent.Apps) { - $appId = $appData.AppId.Trim() - $selectedByDefault = $appData.SelectedByDefault - if ($selectedByDefault -and $appId.length -gt 0) { - $appsList += $appId - } - } - } - else { - # Legacy text file format - Foreach ($app in (Get-Content -Path $appsFilePath | Where-Object { $_ -notmatch '^#.*' -and $_ -notmatch '^\s*$' } )) { - if (-not ($app.IndexOf('#') -eq -1)) { - $app = $app.Substring(0, $app.IndexOf('#')) - } - - $app = $app.Trim() - $appString = $app.Trim('*') - $appsList += $appString - } - } - - return $appsList - } - catch { - Write-Error "Unable to read apps list from file: $appsFilePath" - AwaitKeyToExit - } -} - -# Read Apps.json and return list of app objects with optional filtering -function GetAppsFromJson { - param ( - [switch]$OnlyInstalled, - [string]$InstalledList = "", - [switch]$InitialCheckedFromJson - ) - - $apps = @() - try { - $jsonContent = Get-Content -Path $script:AppsListFilePath -Raw | ConvertFrom-Json - } - catch { - Write-Error "Failed to read Apps.json: $_" - return $apps - } - - foreach ($appData in $jsonContent.Apps) { - $appId = $appData.AppId.Trim() - if ($appId.length -eq 0) { continue } - - if ($OnlyInstalled) { - if (-not ($InstalledList -like ("*$appId*")) -and -not (Get-AppxPackage -Name $appId)) { - continue - } - if (($appId -eq "Microsoft.Edge") -and -not ($InstalledList -like "* Microsoft.Edge *")) { - continue - } - } - - $displayName = if ($appData.FriendlyName) { "$($appData.FriendlyName) ($appId)" } else { $appId } - $isChecked = if ($InitialCheckedFromJson) { $appData.SelectedByDefault } else { $false } - - $apps += [PSCustomObject]@{ - AppId = $appId - DisplayName = $displayName - IsChecked = $isChecked - Description = $appData.Description - SelectedByDefault = $appData.SelectedByDefault - } - } - - return $apps -} - # Run winget list and return installed apps (sync or async) function GetInstalledAppsViaWinget { param ( @@ -2608,6 +337,88 @@ function GetInstalledAppsViaWinget { } +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 { @@ -2630,6 +441,35 @@ function GetFriendlyTargetUserName { } +# 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 +} + + # Removes apps specified during function call based on the target scope. function RemoveApps { param ( @@ -2663,14 +503,14 @@ function RemoveApps { RegImport "Adding scheduled task to uninstall $app after for new users..." "Uninstall_$($appName).reg" } else { - # Uninstall app via WinGet, with any progress indicators removed from the output - StripProgress -ScriptBlock { winget uninstall --accept-source-agreements --disable-interactivity --id $app } | Tee-Object -Variable wingetOutput + # Uninstall app via WinGet + winget uninstall --accept-source-agreements --disable-interactivity --id $app If (($app -eq "Microsoft.Edge") -and (Select-String -InputObject $wingetOutput -Pattern "Uninstall failed with exit code")) { Write-ToConsole "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red if ($script:GuiConsoleOutput) { - $result = Show-ModernMessageBox -Message 'Unable to uninstall Microsoft Edge via WinGet. Would you like to forcefully uninstall it? NOT RECOMMENDED!' -Title 'Force Uninstall Microsoft Edge?' -Button 'YesNo' -Icon 'Warning' + $result = Show-MessageBox -Message 'Unable to uninstall Microsoft Edge via WinGet. Would you like to forcefully uninstall it? NOT RECOMMENDED!' -Title 'Force Uninstall Microsoft Edge?' -Button 'YesNo' -Icon 'Warning' if ($result -eq 'Yes') { Write-ToConsole "" @@ -2782,133 +622,6 @@ function ForceRemoveEdge { } -# Execute provided command and strips progress spinners/bars from console output -function StripProgress { - param( - [ScriptBlock]$ScriptBlock - ) - - # Regex pattern to match spinner characters and progress bar patterns - $progressPattern = 'Γû[Æê]|^\s+[-\\|/]\s+$' - - # Corrected regex pattern for size formatting, ensuring proper capture groups are utilized - $sizePattern = '(\d+(\.\d{1,2})?)\s+(B|KB|MB|GB|TB|PB) /\s+(\d+(\.\d{1,2})?)\s+(B|KB|MB|GB|TB|PB)' - - & $ScriptBlock 2>&1 | ForEach-Object { - if ($_ -is [System.Management.Automation.ErrorRecord]) { - "Error: $($_.Exception.Message)" - } - else { - $line = $_ -replace $progressPattern, '' -replace $sizePattern, '' - if (-not ([string]::IsNullOrWhiteSpace($line)) -and -not ($line.StartsWith(' '))) { - $line - } - } - } -} - - -# 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 -} - - -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 -} - - -# 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 -} - - # Import & execute regfile function RegImport { param ( @@ -2970,39 +683,6 @@ function RegImport { } -# Restart the Windows Explorer process -function RestartExplorer { - Write-ToConsole "> Attempting to restart the Windows Explorer process to apply all changes..." - - if ($script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User") -or $script:Params.ContainsKey("NoRestartExplorer")) { - Write-ToConsole "Explorer process restart was skipped, please manually reboot your PC to apply all changes" -ForegroundColor Yellow - return - } - - if ($script:Params.ContainsKey("DisableMouseAcceleration")) { - Write-ToConsole "Warning: Changes to the Enhance Pointer Precision setting will only take effect after a reboot" -ForegroundColor Yellow - } - - if ($script:Params.ContainsKey("DisableStickyKeys")) { - Write-ToConsole "Warning: Changes to the Sticky Keys setting will only take effect after a reboot" -ForegroundColor Yellow - } - - if ($script:Params.ContainsKey("DisableAnimations")) { - Write-ToConsole "Warning: Animations will only be disabled after a reboot" -ForegroundColor Yellow - } - - # Only restart if the powershell process matches the OS architecture. - # Restarting explorer from a 32bit PowerShell window will fail on a 64bit OS - if ([Environment]::Is64BitProcess -eq [Environment]::Is64BitOperatingSystem) { - Write-ToConsole "Restarting the Windows Explorer process... (This may cause your screen to flicker)" - Stop-Process -processName: Explorer -Force - } - else { - Write-ToConsole "Unable to restart Windows Explorer process, please manually reboot your PC to apply all changes" -ForegroundColor Yellow - } -} - - # Replace the startmenu for all users, when using the default startmenuTemplate this clears all pinned apps # Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/ function ReplaceStartMenuForAllUsers { @@ -3088,141 +768,6 @@ function ReplaceStartMenu { } -# 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 - } -} - - -# Saves the current settings, excluding control parameters, to a JSON file -function SaveSettings { - $settings = @{ - "Version" = "1.0" - "Settings" = @() - } - - foreach ($param in $script:Params.Keys) { - if ($script:ControlParams -notcontains $param) { - $value = $script:Params[$param] - - $settings.Settings += @{ - "Name" = $param - "Value" = $value - } - } - } - - try { - $settings | ConvertTo-Json -Depth 10 | Set-Content $script:SavedSettingsFilePath - } - catch { - Write-Output "" - Write-Host "Error: Failed to save settings to LastUsedSettings.json file" -ForegroundColor Red - } -} - - -# Prints the header for the script -function PrintHeader { - param ( - $title - ) - - $fullTitle = " Win11Debloat Script - $title" - - if ($script:Params.ContainsKey("Sysprep")) { - $fullTitle = "$fullTitle (Sysprep mode)" - } - else { - $fullTitle = "$fullTitle (User: $(GetUserName))" - } - - Clear-Host - Write-Host "-------------------------------------------------------------------------------------------" - Write-Host $fullTitle - Write-Host "-------------------------------------------------------------------------------------------" -} - - -# Prints all pending changes that will be made by the script -function PrintPendingChanges { - Write-Output "Win11Debloat will make the following changes:" - - if ($script:Params['CreateRestorePoint']) { - Write-Output "- $($script:Features['CreateRestorePoint'].Label)" - } - foreach ($parameterName in $script:Params.Keys) { - if ($script:ControlParams -contains $parameterName) { - continue - } - - # Print parameter description - switch ($parameterName) { - 'Apps' { - continue - } - 'CreateRestorePoint' { - continue - } - 'RemoveApps' { - $appsList = GenerateAppsList - - if ($appsList.Count -eq 0) { - Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow - Write-Output "" - continue - } - - Write-Output "- Remove $($appsList.Count) apps:" - Write-Host $appsList -ForegroundColor DarkGray - continue - } - 'RemoveAppsCustom' { - $appsList = ReadAppslistFromFile $script:CustomAppsListFilePath - - if ($appsList.Count -eq 0) { - Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow - Write-Output "" - continue - } - - Write-Output "- Remove $($appsList.Count) apps:" - Write-Host $appsList -ForegroundColor DarkGray - continue - } - default { - if ($script:Features -and $script:Features.ContainsKey($parameterName)) { - $action = $script:Features[$parameterName].Action - $message = $script:Features[$parameterName].Label - Write-Output "- $action $message" - } - else { - # Fallback: show the parameter name if no feature description is available - Write-Output "- $parameterName" - } - continue - } - } - } - - Write-Output "" - Write-Output "" - Write-Output "Press enter to execute the script or press CTRL+C to quit..." - Read-Host | Out-Null -} - - # 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])) { @@ -3233,7 +778,7 @@ function GenerateAppsList { switch ($appMode) { 'default' { - $appsList = ReadAppslistFromFile $script:AppsListFilePath + $appsList = LoadAppsFromFile $script:AppsListFilePath return $appsList } default { @@ -3244,29 +789,6 @@ function GenerateAppsList { } } - -function AwaitKeyToExit { - # Suppress prompt if Silent parameter was passed - if (-not $Silent) { - Write-Output "" - Write-Output "Press any key to exit..." - $null = [System.Console]::ReadKey() - } - - Stop-Transcript - Exit -} - - -function GetUserName { - if ($script:Params.ContainsKey("User")) { - return $script:Params.Item("User") - } - - return $env:USERNAME -} - - # Executes a single parameter/feature based on its key # Parameters: # $paramKey - The parameter name to execute @@ -3320,7 +842,7 @@ function ExecuteParameter { } 'RemoveAppsCustom' { Write-ToConsole "> Removing selected apps..." - $appsList = ReadAppslistFromFile $script:CustomAppsListFilePath + $appsList = LoadAppsFromFile $script:CustomAppsListFilePath if ($appsList.Count -eq 0) { Write-ToConsole "No valid apps were selected for removal" -ForegroundColor Yellow @@ -3471,7 +993,7 @@ function CreateSystemRestorePoint { } } - $createRestorePointJobDone = $createRestorePointJob | Wait-Job -TimeOut 20 + $createRestorePointJobDone = $createRestorePointJob | Wait-Job -TimeOut 1 if (-not $createRestorePointJobDone) { Remove-Job -Job $createRestorePointJob -Force -ErrorAction SilentlyContinue @@ -3494,14 +1016,9 @@ function CreateSystemRestorePoint { # Ensure that the user is aware if creating a restore point failed, and give them the option to continue without a restore point or cancel the script if ($failed) { if ($script:GuiConsoleOutput) { - $result = [System.Windows.MessageBox]::Show( - 'Failed to create a system restore point. Do you want to continue without a restore point?', - 'Restore Point Creation Failed', - [System.Windows.MessageBoxButton]::YesNo, - [System.Windows.MessageBoxImage]::Warning - ) + $result = Show-MessageBox "Failed to create a system restore point. Do you want to continue without a restore point?" "Restore Point Creation Failed" "YesNo" "Warning" - if ($result -ne [System.Windows.MessageBoxResult]::Yes) { + if ($result -ne "Yes") { $script:CancelRequested = $true return } @@ -3519,171 +1036,49 @@ function CreateSystemRestorePoint { } -function ShowScriptMenuOptions { - Do { - $ModeSelectionMessage = "Please select an option (1/2)" - - PrintHeader 'Menu' - - Write-Host "(1) Default mode: Quickly apply the recommended changes" - Write-Host "(2) App removal mode: Select & remove apps, without making other changes" - - # Only show this option if SavedSettings file exists - if (Test-Path $script:SavedSettingsFilePath) { - Write-Host "(3) Quickly apply your last used settings" - - $ModeSelectionMessage = "Please select an option (1/2/3)" - } - - Write-Host "" - Write-Host "" - - $Mode = Read-Host $ModeSelectionMessage - - if (($Mode -eq '3') -and -not (Test-Path $script:SavedSettingsFilePath)) { - $Mode = $null - } - } - while ($Mode -ne '1' -and $Mode -ne '2' -and $Mode -ne '3') - - return $Mode -} - - -function ShowDefaultModeOptions { - # Show options for removing apps, or set selection if RunDefaults or RunDefaultsLite parameter was passed - if ($RunDefaults) { - $RemoveAppsInput = '1' - } - elseif ($RunDefaultsLite) { - $RemoveAppsInput = '0' - } - else { - $RemoveAppsInput = ShowDefaultModeAppRemovalOptions - - if ($RemoveAppsInput -eq '2' -and ($script:SelectedApps.contains('Microsoft.XboxGameOverlay') -or $script:SelectedApps.contains('Microsoft.XboxGamingOverlay')) -and - $( Read-Host -Prompt "Disable Game Bar integration and game/screen recording? This also stops ms-gamingoverlay and ms-gamebar popups (y/n)" ) -eq 'y') { - $DisableGameBarIntegrationInput = $true; - } - } - - PrintHeader 'Default Mode' - - # Add default settings based on user input - try { - # Select app removal options based on user input - switch ($RemoveAppsInput) { - '1' { - AddParameter 'RemoveApps' - AddParameter 'Apps' 'Default' - } - '2' { - AddParameter 'RemoveAppsCustom' - - if ($DisableGameBarIntegrationInput) { - AddParameter 'DisableDVR' - AddParameter 'DisableGameBarIntegration' - } - } - } - - # Load settings from DefaultSettings.json and add to params - LoadSettingsToParams -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0" - } - catch { - Write-Error "Failed to load settings from DefaultSettings.json file: $_" - AwaitKeyToExit - } - - SaveSettings - - # Skip change summary if Silent parameter was passed - if ($Silent) { +# Restart the Windows Explorer process +function RestartExplorer { + Write-ToConsole "> Attempting to restart the Windows Explorer process to apply all changes..." + + if ($script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User") -or $script:Params.ContainsKey("NoRestartExplorer")) { + Write-ToConsole "Explorer process restart was skipped, please manually reboot your PC to apply all changes" -ForegroundColor Yellow return } - PrintPendingChanges - PrintHeader 'Default Mode' -} - - -function ShowDefaultModeAppRemovalOptions { - PrintHeader 'Default Mode' - - Write-Host "Please note: The default selection of apps includes Microsoft Teams, Spotify, Sticky Notes and more. Select option 2 to verify and change what apps are removed by the script" -ForegroundColor DarkGray - Write-Host "" - - Do { - Write-Host "Options:" -ForegroundColor Yellow - Write-Host " (n) Don't remove any apps" -ForegroundColor Yellow - Write-Host " (1) Only remove the default selection of apps" -ForegroundColor Yellow - Write-Host " (2) Manually select which apps to remove" -ForegroundColor Yellow - $RemoveAppsInput = Read-Host "Do you want to remove any apps? Apps will be removed for all users (n/1/2)" - - # Show app selection form if user entered option 3 - if ($RemoveAppsInput -eq '2') { - $result = OpenAppSelectionWindow - - if ($result -ne $true) { - # User cancelled or closed app selection, change RemoveAppsInput so the menu will be shown again - Write-Host "" - Write-Host "Cancelled application selection, please try again" -ForegroundColor Red - - $RemoveAppsInput = 'c' - } - - Write-Host "" - } + if ($script:Params.ContainsKey("DisableMouseAcceleration")) { + Write-ToConsole "Warning: Changes to the Enhance Pointer Precision setting will only take effect after a reboot" -ForegroundColor Yellow } - while ($RemoveAppsInput -ne 'n' -and $RemoveAppsInput -ne '0' -and $RemoveAppsInput -ne '1' -and $RemoveAppsInput -ne '2') - return $RemoveAppsInput -} + if ($script:Params.ContainsKey("DisableStickyKeys")) { + Write-ToConsole "Warning: Changes to the Sticky Keys setting will only take effect after a reboot" -ForegroundColor Yellow + } + if ($script:Params.ContainsKey("DisableAnimations")) { + Write-ToConsole "Warning: Animations will only be disabled after a reboot" -ForegroundColor Yellow + } -function ShowAppRemoval { - PrintHeader "App Removal" - - Write-Output "> Opening app selection form..." - - $result = OpenAppSelectionWindow - - if ($result -eq $true) { - Write-Output "You have selected $($script:SelectedApps.Count) apps for removal" - AddParameter 'RemoveAppsCustom' - - SaveSettings - - # Suppress prompt if Silent parameter was passed - if (-not $Silent) { - Write-Output "" - Write-Output "" - Write-Output "Press enter to remove the selected apps or press CTRL+C to quit..." - Read-Host | Out-Null - PrintHeader "App Removal" - } + # Only restart if the powershell process matches the OS architecture. + # Restarting explorer from a 32bit PowerShell window will fail on a 64bit OS + if ([Environment]::Is64BitProcess -eq [Environment]::Is64BitOperatingSystem) { + Write-ToConsole "Restarting the Windows Explorer process... (This may cause your screen to flicker)" + Stop-Process -processName: Explorer -Force } else { - Write-Host "Selection was cancelled, no apps have been removed" -ForegroundColor Red - Write-Output "" + Write-ToConsole "Unable to restart Windows Explorer process, please manually reboot your PC to apply all changes" -ForegroundColor Yellow } } -function LoadAndShowLastUsedSettings { - PrintHeader 'Custom Mode' - - try { - # Load settings from LastUsedSettings.json and add to params - LoadSettingsToParams -filePath $script:SavedSettingsFilePath -expectedVersion "1.0" - } - catch { - Write-Error "Failed to load settings from LastUsedSettings.json file: $_" - AwaitKeyToExit +function AwaitKeyToExit { + # Suppress prompt if Silent parameter was passed + if (-not $Silent) { + Write-Output "" + Write-Output "Press any key to exit..." + $null = [System.Console]::ReadKey() } - PrintPendingChanges - PrintHeader 'Custom Mode' + Stop-Transcript + Exit } @@ -3758,7 +1153,7 @@ if ((Test-Path $script:SavedSettingsFilePath) -and ([String]::IsNullOrWhiteSpace if ($RunAppsListGenerator) { PrintHeader "Custom Apps List Generator" - $result = OpenAppSelectionWindow + $result = Show-AppSelectionWindow # Show different message based on whether the app selection was saved or cancelled if ($result -ne $true) { @@ -3775,7 +1170,7 @@ if ($RunAppsListGenerator) { # 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 ($RunDefaults -or $RunDefaultsLite) { - ShowDefaultModeOptions + ShowCLIDefaultModeOptions } elseif ($RunSavedSettings) { if (-not (Test-Path $script:SavedSettingsFilePath)) { @@ -3784,15 +1179,15 @@ if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSa AwaitKeyToExit } - LoadAndShowLastUsedSettings + ShowCLILastUsedSettings } else { if ($CLI) { - $Mode = ShowScriptMenuOptions + $Mode = ShowCLIMenuOptions } else { try { - $result = OpenGUI + $result = Show-MainWindow Stop-Transcript Exit @@ -3805,7 +1200,7 @@ if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSa $null = [System.Console]::ReadKey() } - $Mode = ShowScriptMenuOptions + $Mode = ShowCLIMenuOptions } } } @@ -3814,17 +1209,17 @@ if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSa switch ($Mode) { # Default mode, loads defaults and app removal options '1' { - ShowDefaultModeOptions + ShowCLIDefaultModeOptions } # App removal, remove apps based on user selection '2' { - ShowAppRemoval + ShowCLIAppRemoval } # Load last used options from the "LastUsedSettings.json" file '3' { - LoadAndShowLastUsedSettings + ShowCLILastUsedSettings } } }