12 Commits

37 changed files with 1302 additions and 310 deletions

View File

@@ -647,7 +647,7 @@
}, },
{ {
"FriendlyName": "Microsoft Edge", "FriendlyName": "Microsoft Edge",
"AppId": "Microsoft.Edge", "AppId": ["Microsoft.Edge", "XPFFTQ037JWMHS"],
"Description": "Windows' default browser, WARNING: Removing this app also removes the only browser from Windows Sandbox and could affect other apps", "Description": "Windows' default browser, WARNING: Removing this app also removes the only browser from Windows Sandbox and could affect other apps",
"SelectedByDefault": false, "SelectedByDefault": false,
"Recommendation": "unsafe" "Recommendation": "unsafe"

View File

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

View File

@@ -160,6 +160,7 @@ Below is an overview of the key features and functionality offered by Win11Deblo
- Add all common folders (Desktop, Downloads, etc.) back to 'This PC' in File Explorer. - Add all common folders (Desktop, Downloads, etc.) back to 'This PC' in File Explorer.
- Hide the 3D objects, music or OneDrive folder from the File Explorer navigation pane. - Hide the 3D objects, music or OneDrive folder from the File Explorer navigation pane.
- Hide the 'Include in library', 'Give access to' and 'Share' options from the context menu. - Hide the 'Include in library', 'Give access to' and 'Share' options from the context menu.
- Change drive letter position or visibility in File Explorer.
#### Multi-tasking #### Multi-tasking

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,23 +7,33 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Border Name="BubbleBorder" <Grid Grid.Row="0"
Grid.Row="0" Margin="10,10,10,8">
Background="{DynamicResource CardBgColor}" <Border Name="BubbleBorder"
BorderBrush="{DynamicResource ButtonBorderColor}" Background="{DynamicResource CardBgColor}"
BorderThickness="1" BorderBrush="{DynamicResource ButtonBorderColor}"
CornerRadius="8" BorderThickness="1"
Padding="10,7,10,7"> CornerRadius="8"
Padding="10,7,10,7">
<Border.Effect>
<DropShadowEffect BlurRadius="9"
Opacity="0.16"
ShadowDepth="2"
Direction="270"
Color="Black"/>
</Border.Effect>
<TextBlock Name="BubbleText" <TextBlock Name="BubbleText"
Text="View the selected changes here" Text="View the selected changes here"
TextWrapping="Wrap" TextWrapping="Wrap"
MaxWidth="260" MaxWidth="260"
Foreground="{DynamicResource FgColor}"/> Foreground="{DynamicResource FgColor}"/>
</Border> </Border>
</Grid>
<Grid Grid.Row="1" <Grid Grid.Row="1"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Margin="0,-1,0,0" Margin="0,-9,0,0"
Panel.ZIndex="1"
Width="12" Width="12"
Height="8"> Height="8">
<Polygon Name="BubblePointer" <Polygon Name="BubblePointer"

View File

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

View File

@@ -1,15 +1,22 @@
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
Title="Win11Debloat" Title="Win11Debloat"
MinWidth="1130" MinHeight="600" MinWidth="1130" MinHeight="600"
MaxWidth="1400"
ResizeMode="CanResize" ResizeMode="CanResize"
SnapsToDevicePixels="True" SnapsToDevicePixels="True"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
WindowStyle="None" WindowStyle="None"
AllowsTransparency="True" AllowsTransparency="False"
Background="Transparent" Background="{DynamicResource BgColor}"
Foreground="{DynamicResource FgColor}"> Foreground="{DynamicResource FgColor}">
<shell:WindowChrome.WindowChrome>
<shell:WindowChrome ResizeBorderThickness="5"
CaptionHeight="32"
CornerRadius="8"
GlassFrameThickness="0"
UseAeroCaptionButtons="False"/>
</shell:WindowChrome.WindowChrome>
<Window.Resources> <Window.Resources>
<!-- Sort column header hover style --> <!-- Sort column header hover style -->
<Style x:Key="SortHeaderBtnStyle" TargetType="StackPanel"> <Style x:Key="SortHeaderBtnStyle" TargetType="StackPanel">
@@ -601,18 +608,11 @@
</Style> </Style>
</Window.Resources> </Window.Resources>
<Border BorderBrush="{DynamicResource BorderColor}" <Border x:Name="MainBorder" BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1" BorderThickness="1"
CornerRadius="8" CornerRadius="8"
Background="{DynamicResource BgColor}" Background="{DynamicResource BgColor}"
Margin="25"> Margin="0">
<Border.Effect>
<DropShadowEffect Color="Black"
Opacity="0.15"
BlurRadius="20"
ShadowDepth="0"
Direction="0"/>
</Border.Effect>
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="32"/> <RowDefinition Height="32"/>
@@ -621,18 +621,18 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Resize Borders --> <!-- Resize Borders -->
<Rectangle x:Name="ResizeLeft" Width="5" HorizontalAlignment="Left" Fill="Transparent" Cursor="SizeWE" Grid.Row="0" Grid.RowSpan="3" Panel.ZIndex="100"/> <Rectangle x:Name="ResizeLeft" Width="5" HorizontalAlignment="Left" Fill="Transparent" Cursor="SizeWE" Grid.Row="0" Grid.RowSpan="3" Panel.ZIndex="100" Visibility="Collapsed" IsHitTestVisible="False"/>
<Rectangle x:Name="ResizeRight" Width="5" HorizontalAlignment="Right" Fill="Transparent" Cursor="SizeWE" Grid.Row="0" Grid.RowSpan="3" Panel.ZIndex="100"/> <Rectangle x:Name="ResizeRight" Width="5" HorizontalAlignment="Right" Fill="Transparent" Cursor="SizeWE" Grid.Row="0" Grid.RowSpan="3" Panel.ZIndex="100" Visibility="Collapsed" IsHitTestVisible="False"/>
<Rectangle x:Name="ResizeTop" Height="5" VerticalAlignment="Top" Fill="Transparent" Cursor="SizeNS" Grid.Row="0" Panel.ZIndex="100"/> <Rectangle x:Name="ResizeTop" Height="5" VerticalAlignment="Top" Fill="Transparent" Cursor="SizeNS" Grid.Row="0" Panel.ZIndex="100" Visibility="Collapsed" IsHitTestVisible="False"/>
<Rectangle x:Name="ResizeBottom" Height="5" VerticalAlignment="Bottom" Fill="Transparent" Cursor="SizeNS" Grid.Row="2" Panel.ZIndex="100"/> <Rectangle x:Name="ResizeBottom" Height="5" VerticalAlignment="Bottom" Fill="Transparent" Cursor="SizeNS" Grid.Row="2" Panel.ZIndex="100" Visibility="Collapsed" IsHitTestVisible="False"/>
<Rectangle x:Name="ResizeTopLeft" Width="8" Height="8" HorizontalAlignment="Left" VerticalAlignment="Top" Fill="Transparent" Cursor="SizeNWSE" Grid.Row="0" Panel.ZIndex="101"/> <Rectangle x:Name="ResizeTopLeft" Width="8" Height="8" HorizontalAlignment="Left" VerticalAlignment="Top" Fill="Transparent" Cursor="SizeNWSE" Grid.Row="0" Panel.ZIndex="101" Visibility="Collapsed" IsHitTestVisible="False"/>
<Rectangle x:Name="ResizeTopRight" Width="8" Height="8" HorizontalAlignment="Right" VerticalAlignment="Top" Fill="Transparent" Cursor="SizeNESW" Grid.Row="0" Panel.ZIndex="101"/> <Rectangle x:Name="ResizeTopRight" Width="8" Height="8" HorizontalAlignment="Right" VerticalAlignment="Top" Fill="Transparent" Cursor="SizeNESW" Grid.Row="0" Panel.ZIndex="101" Visibility="Collapsed" IsHitTestVisible="False"/>
<Rectangle x:Name="ResizeBottomLeft" Width="8" Height="8" HorizontalAlignment="Left" VerticalAlignment="Bottom" Fill="Transparent" Cursor="SizeNESW" Grid.Row="2" Panel.ZIndex="101"/> <Rectangle x:Name="ResizeBottomLeft" Width="8" Height="8" HorizontalAlignment="Left" VerticalAlignment="Bottom" Fill="Transparent" Cursor="SizeNESW" Grid.Row="2" Panel.ZIndex="101" Visibility="Collapsed" IsHitTestVisible="False"/>
<Rectangle x:Name="ResizeBottomRight" Width="8" Height="8" HorizontalAlignment="Right" VerticalAlignment="Bottom" Fill="Transparent" Cursor="SizeNWSE" Grid.Row="2" Panel.ZIndex="101"/> <Rectangle x:Name="ResizeBottomRight" Width="8" Height="8" HorizontalAlignment="Right" VerticalAlignment="Bottom" Fill="Transparent" Cursor="SizeNWSE" Grid.Row="2" Panel.ZIndex="101" Visibility="Collapsed" IsHitTestVisible="False"/>
<!-- Custom Title Bar --> <!-- Custom Title Bar -->
<Grid Grid.Row="0" x:Name="TitleBar"> <Grid Grid.Row="0" x:Name="TitleBar">
<Border Background="{DynamicResource BgColor}" CornerRadius="8,8,0,0"> <Border x:Name="TitleBarBackground" Background="{DynamicResource BgColor}" CornerRadius="8,8,0,0">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
@@ -645,10 +645,33 @@
Margin="12,0,0,0" Margin="12,0,0,0"
FontSize="12"/> FontSize="12"/>
<StackPanel Grid.Column="1" Orientation="Horizontal"> <StackPanel Grid.Column="1" Orientation="Horizontal">
<Button x:Name="KofiBtn" Content="&#xEB52;" FontFamily="Segoe Fluent Icons" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="Support the creator" AutomationProperties.Name="Support the creator"/> <Button x:Name="KofiBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="&#xEB52;" FontFamily="Segoe Fluent Icons" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="Support the creator" AutomationProperties.Name="Support the creator"/>
<Button x:Name="MenuBtn" Content="&#xE700;" FontFamily="Segoe Fluent Icons" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="Options" AutomationProperties.Name="Options"> <Button x:Name="MenuBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="&#xE700;" FontFamily="Segoe Fluent Icons" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="Options" AutomationProperties.Name="Options">
<Button.ContextMenu> <Button.ContextMenu>
<ContextMenu x:Name="MainMenu"> <ContextMenu x:Name="MainMenu">
<ContextMenu.Resources>
<Style x:Key="{x:Static MenuItem.SeparatorStyleKey}" TargetType="Separator">
<Setter Property="Margin" Value="4,6"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Separator">
<Border Height="1" Background="{DynamicResource BorderColor}" SnapsToDevicePixels="True" HorizontalAlignment="Stretch"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ContextMenu.Resources>
<MenuItem x:Name="ImportConfigBtn" Header="Import config" AutomationProperties.Name="Import configuration">
<MenuItem.Icon>
<TextBlock Text="&#xe838;" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="ExportConfigBtn" Header="Export config" AutomationProperties.Name="Export configuration">
<MenuItem.Icon>
<TextBlock Text="&#xe74e;" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem x:Name="MenuDocumentation" Header="Documentation" AutomationProperties.Name="Documentation"> <MenuItem x:Name="MenuDocumentation" Header="Documentation" AutomationProperties.Name="Documentation">
<MenuItem.Icon> <MenuItem.Icon>
<TextBlock Text="&#xe736;" FontFamily="Segoe MDL2 Assets" FontSize="16" Foreground="{DynamicResource FgColor}"/> <TextBlock Text="&#xe736;" FontFamily="Segoe MDL2 Assets" FontSize="16" Foreground="{DynamicResource FgColor}"/>
@@ -672,7 +695,7 @@
</ContextMenu> </ContextMenu>
</Button.ContextMenu> </Button.ContextMenu>
</Button> </Button>
<Button x:Name="CloseBtn" Content="&#xE8BB;" Style="{StaticResource CloseButton}" ToolTip="Close" AutomationProperties.Name="Close"/> <Button x:Name="CloseBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="&#xE8BB;" Style="{StaticResource CloseButton}" ToolTip="Close" AutomationProperties.Name="Close"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
</Border> </Border>

View File

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

View File

@@ -9,6 +9,9 @@ function RemoveApps {
$appIndex = 0 $appIndex = 0
$appCount = @($appsList).Count $appCount = @($appsList).Count
$edgeIds = @('Microsoft.Edge', 'XPFFTQ037JWMHS')
$edgeUninstallSucceeded = $false
$edgeScheduledTaskAdded = $false
Foreach ($app in $appsList) { Foreach ($app in $appsList) {
if ($script:CancelRequested) { if ($script:CancelRequested) {
@@ -25,20 +28,27 @@ function RemoveApps {
Write-Host "Attempting to remove $app..." Write-Host "Attempting to remove $app..."
# Use WinGet only to remove OneDrive and Edge # Use WinGet only to remove OneDrive and Edge
if (($app -eq "Microsoft.OneDrive") -or ($app -eq "Microsoft.Edge")) { if (($app -eq "Microsoft.OneDrive") -or ($edgeIds -contains $app)) {
if ($script:WingetInstalled -eq $false) { if ($script:WingetInstalled -eq $false) {
Write-Host "WinGet is either not installed or is outdated, $app could not be removed" -ForegroundColor Red Write-Host "WinGet is either not installed or is outdated, $app could not be removed" -ForegroundColor Red
continue continue
} }
$appName = $app -replace '\.', '_' $isEdgeId = $edgeIds -contains $app
$appName = if ($isEdgeId) { 'Microsoft_Edge' } else { $app -replace '\.', '_' }
# Uninstall app via WinGet, or create a scheduled task to uninstall it later # Uninstall app via WinGet, or create a scheduled task to uninstall it later
if ($script:Params.ContainsKey("User")) { if ($script:Params.ContainsKey("User")) {
ImportRegistryFile "Adding scheduled task to uninstall $app for user $(GetUserName)..." "Uninstall_$($appName).reg" if (-not ($isEdgeId -and $edgeScheduledTaskAdded)) {
ImportRegistryFile "Adding scheduled task to uninstall $app for user $(GetUserName)..." "Uninstall_$($appName).reg"
if ($isEdgeId) { $edgeScheduledTaskAdded = $true }
}
} }
elseif ($script:Params.ContainsKey("Sysprep")) { elseif ($script:Params.ContainsKey("Sysprep")) {
ImportRegistryFile "Adding scheduled task to uninstall $app after for new users..." "Uninstall_$($appName).reg" if (-not ($isEdgeId -and $edgeScheduledTaskAdded)) {
ImportRegistryFile "Adding scheduled task to uninstall $app after for new users..." "Uninstall_$($appName).reg"
if ($isEdgeId) { $edgeScheduledTaskAdded = $true }
}
} }
else { else {
# Uninstall app via WinGet # Uninstall app via WinGet
@@ -47,21 +57,35 @@ function RemoveApps {
winget uninstall --accept-source-agreements --disable-interactivity --id $appId winget uninstall --accept-source-agreements --disable-interactivity --id $appId
} -ArgumentList $app } -ArgumentList $app
If (($app -eq "Microsoft.Edge") -and (Select-String -InputObject $wingetOutput -Pattern "Uninstall failed with exit code")) { $wingetFailed = Select-String -InputObject $wingetOutput -Pattern "Uninstall failed with exit code|No installed package found matching input criteria|No package found matching input criteria" -SimpleMatch:$false
Write-Host "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red if ($isEdgeId) {
if (-not $wingetFailed) {
$edgeUninstallSucceeded = $true
}
if ($script:GuiWindow) { # Prompt immediately after the final selected Edge ID attempt (if all attempts failed)
$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' $hasRemainingEdgeIds = $false
if ($appIndex -lt $appCount) {
$remainingApps = @($appsList)[($appIndex)..($appCount - 1)]
$hasRemainingEdgeIds = @($remainingApps | Where-Object { $edgeIds -contains $_ }).Count -gt 0
}
if ($result -eq 'Yes') { if (-not $hasRemainingEdgeIds -and -not $edgeUninstallSucceeded) {
Write-Host "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red
if ($script:GuiWindow) {
$result = Show-MessageBox -Message 'Unable to uninstall Microsoft Edge via WinGet. Would you like to forcefully uninstall it? NOT RECOMMENDED!' -Title 'Force Uninstall Microsoft Edge?' -Button 'YesNo' -Icon 'Warning'
if ($result -eq 'Yes') {
Write-Host ""
ForceRemoveEdge
}
}
elseif ($( Read-Host -Prompt "Would you like to forcefully uninstall Microsoft Edge? NOT RECOMMENDED! (y/n)" ) -eq 'y') {
Write-Host "" Write-Host ""
ForceRemoveEdge ForceRemoveEdge
} }
} }
elseif ($( Read-Host -Prompt "Would you like to forcefully uninstall Microsoft Edge? NOT RECOMMENDED! (y/n)" ) -eq 'y') {
Write-Host ""
ForceRemoveEdge
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -40,7 +40,7 @@ function Show-AboutDialog {
} }
catch { } catch { }
} }
# Apply theme resources # Apply theme resources
SetWindowThemeResources -window $aboutWindow -usesDarkMode $usesDarkMode SetWindowThemeResources -window $aboutWindow -usesDarkMode $usesDarkMode
@@ -83,13 +83,16 @@ function Show-AboutDialog {
}) })
# Show dialog # Show dialog
$aboutWindow.ShowDialog() | Out-Null try {
$aboutWindow.ShowDialog() | Out-Null
# Hide overlay after dialog closes }
if ($overlay) { finally {
try { # Hide overlay after dialog closes
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' }) if ($overlay) {
try {
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' })
}
catch { }
} }
catch { }
} }
} }

View File

@@ -75,7 +75,8 @@ function Show-AppSelectionWindow {
$checkbox = New-Object System.Windows.Controls.CheckBox $checkbox = New-Object System.Windows.Controls.CheckBox
$checkbox.Content = $_.DisplayName $checkbox.Content = $_.DisplayName
$checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $_.DisplayName) $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $_.DisplayName)
$checkbox.Tag = $_.AppId $checkbox.Tag = $_.AppIdDisplay
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppIds' -Value @($_.AppId)
$checkbox.IsChecked = $_.IsChecked $checkbox.IsChecked = $_.IsChecked
$checkbox.ToolTip = $_.Description $checkbox.ToolTip = $_.Description
$checkbox.Style = $window.Resources["AppsPanelCheckBoxStyle"] $checkbox.Style = $window.Resources["AppsPanelCheckBoxStyle"]
@@ -118,9 +119,10 @@ function Show-AppSelectionWindow {
$selectedApps = @() $selectedApps = @()
foreach ($child in $appsPanel.Children) { foreach ($child in $appsPanel.Children) {
if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) { if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) {
$selectedApps += $child.Tag $selectedApps += @($child.AppIds)
} }
} }
$selectedApps = @($selectedApps | Where-Object { $_ } | Select-Object -Unique)
# Close form without saving if no apps were selected # Close form without saving if no apps were selected
if ($selectedApps.Count -eq 0) { if ($selectedApps.Count -eq 0) {

View File

@@ -242,13 +242,16 @@ function Show-ApplyModal {
}) })
# Show dialog # Show dialog
$applyWindow.ShowDialog() | Out-Null try {
$applyWindow.ShowDialog() | Out-Null
# Hide overlay after dialog closes }
if ($overlay) { finally {
try { # Hide overlay after dialog closes
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' }) if ($overlay) {
try {
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' })
}
catch { }
} }
catch { }
} }
} }

View File

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

View File

@@ -19,7 +19,8 @@ function Show-MainWindow {
SetWindowThemeResources -window $window -usesDarkMode $usesDarkMode SetWindowThemeResources -window $window -usesDarkMode $usesDarkMode
# Get named elements # Get named elements
$titleBar = $window.FindName('TitleBar') $mainBorder = $window.FindName('MainBorder')
$titleBarBackground = $window.FindName('TitleBarBackground')
$kofiBtn = $window.FindName('KofiBtn') $kofiBtn = $window.FindName('KofiBtn')
$menuBtn = $window.FindName('MenuBtn') $menuBtn = $window.FindName('MenuBtn')
$closeBtn = $window.FindName('CloseBtn') $closeBtn = $window.FindName('CloseBtn')
@@ -27,11 +28,89 @@ function Show-MainWindow {
$menuReportBug = $window.FindName('MenuReportBug') $menuReportBug = $window.FindName('MenuReportBug')
$menuLogs = $window.FindName('MenuLogs') $menuLogs = $window.FindName('MenuLogs')
$menuAbout = $window.FindName('MenuAbout') $menuAbout = $window.FindName('MenuAbout')
$importConfigBtn = $window.FindName('ImportConfigBtn')
$exportConfigBtn = $window.FindName('ExportConfigBtn')
# Title bar event handlers $windowStateNormal = [System.Windows.WindowState]::Normal
$titleBar.Add_MouseLeftButtonDown({ $windowStateMaximized = [System.Windows.WindowState]::Maximized
if ($_.OriginalSource -is [System.Windows.Controls.Grid] -or $_.OriginalSource -is [System.Windows.Controls.Border] -or $_.OriginalSource -is [System.Windows.Controls.TextBlock]) { $normalWindowShadow = $mainBorder.Effect
$window.DragMove() $windowResizeBorder = [System.Windows.SystemParameters]::WindowResizeBorderThickness
$maximizedChromeInset = [System.Windows.Thickness]::new($windowResizeBorder.Left, $windowResizeBorder.Top, $windowResizeBorder.Right, 0)
$initialNormalMaxWidth = 1400.0
$convertScreenPixelsToDip = {
param(
[double]$width,
[double]$height
)
$source = [System.Windows.PresentationSource]::FromVisual($window)
if ($null -eq $source -or $null -eq $source.CompositionTarget) {
return [System.Windows.Size]::new($width, $height)
}
$topLeft = $source.CompositionTarget.TransformFromDevice.Transform([System.Windows.Point]::new(0, 0))
$bottomRight = $source.CompositionTarget.TransformFromDevice.Transform([System.Windows.Point]::new($width, $height))
return [System.Windows.Size]::new($bottomRight.X - $topLeft.X, $bottomRight.Y - $topLeft.Y)
}
$getWindowScreen = {
$hwnd = (New-Object System.Windows.Interop.WindowInteropHelper($window)).Handle
if ($hwnd -eq [IntPtr]::Zero) {
return $null
}
return [System.Windows.Forms.Screen]::FromHandle($hwnd)
}
$updateWindowChrome = {
if ($window.WindowState -eq $windowStateMaximized) {
$mainBorder.Margin = $maximizedChromeInset
$mainBorder.BorderThickness = [System.Windows.Thickness]::new(1)
$mainBorder.CornerRadius = [System.Windows.CornerRadius]::new(0)
$mainBorder.Effect = $null
$titleBarBackground.CornerRadius = [System.Windows.CornerRadius]::new(0)
}
else {
$mainBorder.Margin = [System.Windows.Thickness]::new(0)
$mainBorder.BorderThickness = [System.Windows.Thickness]::new(1)
$mainBorder.CornerRadius = [System.Windows.CornerRadius]::new(8)
$mainBorder.Effect = $normalWindowShadow
$titleBarBackground.CornerRadius = [System.Windows.CornerRadius]::new(8, 8, 0, 0)
}
}
$updateMaximizedBounds = {
& $updateWindowChrome
}
$applyInitialWindowSize = {
if ($window.WindowState -ne $windowStateNormal) {
return
}
$screen = & $getWindowScreen
if ($null -eq $screen) {
return
}
$workingAreaDip = & $convertScreenPixelsToDip $screen.WorkingArea.Width $screen.WorkingArea.Height
$window.Width = [Math]::Min($initialNormalMaxWidth, $workingAreaDip.Width)
$window.Left = $screen.WorkingArea.Left + (($workingAreaDip.Width - $window.Width) / 2)
}
$window.Add_SourceInitialized({
& $applyInitialWindowSize
& $updateMaximizedBounds
})
$window.Add_StateChanged({
& $updateMaximizedBounds
})
$window.Add_LocationChanged({
if ($window.WindowState -eq $windowStateMaximized) {
& $updateMaximizedBounds
} }
}) })
@@ -67,6 +146,22 @@ function Show-MainWindow {
Show-AboutDialog -Owner $window Show-AboutDialog -Owner $window
}) })
# --- Import/Export Configuration ---
$exportConfigBtn.Add_Click({
Export-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox
})
$importConfigBtn.Add_Click({
Import-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -OnAppsImported { UpdateAppSelectionStatus; UpdatePresetStates } -OnImportCompleted {
$tabControl.SelectedIndex = 3
UpdateNavigationButtons
$window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Loaded, [action]{
Show-Bubble -TargetControl $reviewChangesBtn -Message 'View the selected changes here'
}) | Out-Null
}
})
$closeBtn.Add_Click({ $closeBtn.Add_Click({
$window.Close() $window.Close()
}) })
@@ -76,135 +171,6 @@ function Show-MainWindow {
$script:CancelRequested = $true $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 # Integrated App Selection UI
$appsPanel = $window.FindName('AppSelectionPanel') $appsPanel = $window.FindName('AppSelectionPanel')
$onlyInstalledAppsBox = $window.FindName('OnlyInstalledAppsBox') $onlyInstalledAppsBox = $window.FindName('OnlyInstalledAppsBox')
@@ -224,6 +190,32 @@ function Show-MainWindow {
$presetsArrow = $window.FindName('PresetsArrow') $presetsArrow = $window.FindName('PresetsArrow')
$clearAppSelectionBtn = $window.FindName('ClearAppSelectionBtn') $clearAppSelectionBtn = $window.FindName('ClearAppSelectionBtn')
function NormalizeCheckboxState {
param([System.Windows.Controls.CheckBox]$checkBox)
$isChecked = ($checkBox.IsChecked -eq $true)
if ($null -eq $checkBox.IsChecked) {
$checkBox.IsChecked = $false
return $false
}
return $isChecked
}
function AnimatePresetsArrow {
param([double]$angle)
$animation = New-Object System.Windows.Media.Animation.DoubleAnimation
$animation.To = $angle
$animation.Duration = [System.Windows.Duration]::new([System.TimeSpan]::FromMilliseconds(200))
$ease = New-Object System.Windows.Media.Animation.CubicEase
$ease.EasingMode = 'EaseOut'
$animation.EasingFunction = $ease
$presetsArrow.RenderTransform.BeginAnimation([System.Windows.Media.RotateTransform]::AngleProperty, $animation)
}
# Load JSON-defined presets and build dynamic preset checkboxes # Load JSON-defined presets and build dynamic preset checkboxes
$script:JsonPresetCheckboxes = @() $script:JsonPresetCheckboxes = @()
foreach ($preset in (LoadAppPresetsFromJson)) { foreach ($preset in (LoadAppPresetsFromJson)) {
@@ -238,10 +230,9 @@ function Show-MainWindow {
$checkbox.Add_Click({ $checkbox.Add_Click({
if ($script:UpdatingPresets) { return } if ($script:UpdatingPresets) { return }
$check = ($this.IsChecked -eq $true) $check = NormalizeCheckboxState -checkBox $this
if ($this.IsChecked -eq $null) { $this.IsChecked = $false; $check = $false }
$presetIds = $this.PresetAppIds $presetIds = $this.PresetAppIds
ApplyPresetToApps -MatchFilter { param($c) $presetIds -contains $c.Tag }.GetNewClosure() -Check $check ApplyPresetToApps -MatchFilter { param($c) (@($c.AppIds) | Where-Object { $presetIds -contains $_ }).Count -gt 0 }.GetNewClosure() -Check $check
}) })
} }
@@ -254,6 +245,11 @@ function Show-MainWindow {
$script:PendingDefaultMode = $false $script:PendingDefaultMode = $false
# Holds apps data preloaded before ShowDialog() so the first load skips the background job # Holds apps data preloaded before ShowDialog() so the first load skips the background job
$script:PreloadedAppData = $null $script:PreloadedAppData = $null
# Prevent app import until the apps list has finished initial population.
if ($importConfigBtn) {
$importConfigBtn.IsEnabled = $false
}
# Set script-level variable for GUI window reference # Set script-level variable for GUI window reference
$script:GuiWindow = $window $script:GuiWindow = $window
@@ -292,12 +288,32 @@ function Show-MainWindow {
} }
} }
# Rebuilds $script:AppSearchMatches by scanning appsPanel children in their current order,
# collecting any that are still highlighted. Preserves the active match across reorderings.
function RebuildAppSearchIndex {
param($activeMatch = $null)
$newMatches = @()
$newActiveIndex = -1
$i = 0
foreach ($child in $appsPanel.Children) {
if ($child -is [System.Windows.Controls.CheckBox] -and $child.Background -ne [System.Windows.Media.Brushes]::Transparent) {
$newMatches += $child
if ($null -ne $activeMatch -and [System.Object]::ReferenceEquals($child, $activeMatch)) {
$newActiveIndex = $i
}
$i++
}
}
$script:AppSearchMatches = $newMatches
$script:AppSearchMatchIndex = if ($newActiveIndex -ge 0) { $newActiveIndex } elseif ($newMatches.Count -gt 0) { 0 } else { -1 }
}
function SortApps { function SortApps {
$children = @($appsPanel.Children) $children = @($appsPanel.Children)
$key = switch ($script:SortColumn) { $key = switch ($script:SortColumn) {
'Name' { { $_.AppName } } 'Name' { { $_.AppName } }
'Description' { { $_.AppDescription } } 'Description' { { $_.AppDescription } }
'AppId' { { $_.Tag } } 'AppId' { { $_.AppIdDisplay } }
} }
$sorted = $children | Sort-Object $key -Descending:(-not $script:SortAscending) $sorted = $children | Sort-Object $key -Descending:(-not $script:SortAscending)
$appsPanel.Children.Clear() $appsPanel.Children.Clear()
@@ -305,6 +321,14 @@ function Show-MainWindow {
$appsPanel.Children.Add($checkbox) | Out-Null $appsPanel.Children.Add($checkbox) | Out-Null
} }
UpdateSortArrows UpdateSortArrows
# Rebuild search match list in new sorted order so keyboard navigation stays correct
if ($script:AppSearchMatches.Count -gt 0) {
$activeMatch = if ($script:AppSearchMatchIndex -ge 0 -and $script:AppSearchMatchIndex -lt $script:AppSearchMatches.Count) {
$script:AppSearchMatches[$script:AppSearchMatchIndex]
} else { $null }
RebuildAppSearchIndex -activeMatch $activeMatch
}
} }
function SetSortColumn($column) { function SetSortColumn($column) {
@@ -351,14 +375,6 @@ function Show-MainWindow {
function UpdatePresetStates { function UpdatePresetStates {
$script:UpdatingPresets = $true $script:UpdatingPresets = $true
try { try {
# Build a set of currently checked app tags for fast lookup
$checkedTags = @{}
foreach ($child in $appsPanel.Children) {
if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) {
$checkedTags[$child.Tag] = $true
}
}
# Helper: count matching and checked apps, set checkbox state # Helper: count matching and checked apps, set checkbox state
function SetPresetState($checkbox, [scriptblock]$MatchFilter) { function SetPresetState($checkbox, [scriptblock]$MatchFilter) {
$total = 0; $checked = 0 $total = 0; $checked = 0
@@ -366,7 +382,7 @@ function Show-MainWindow {
if ($child -is [System.Windows.Controls.CheckBox]) { if ($child -is [System.Windows.Controls.CheckBox]) {
if (& $MatchFilter $child) { if (& $MatchFilter $child) {
$total++ $total++
if ($checkedTags.ContainsKey($child.Tag)) { $checked++ } if ($child.IsChecked) { $checked++ }
} }
} }
} }
@@ -388,12 +404,12 @@ function Show-MainWindow {
SetPresetState $presetDefaultApps { param($c) $c.SelectedByDefault -eq $true } SetPresetState $presetDefaultApps { param($c) $c.SelectedByDefault -eq $true }
foreach ($jsonCb in $script:JsonPresetCheckboxes) { foreach ($jsonCb in $script:JsonPresetCheckboxes) {
$localIds = $jsonCb.PresetAppIds $localIds = $jsonCb.PresetAppIds
SetPresetState $jsonCb { param($c) $localIds -contains $c.Tag }.GetNewClosure() SetPresetState $jsonCb { param($c) (@($c.AppIds) | Where-Object { $localIds -contains $_ }).Count -gt 0 }.GetNewClosure()
} }
# Last used preset: only update if it's visible (has saved apps) # Last used preset: only update if it's visible (has saved apps)
if ($presetLastUsed.Visibility -ne 'Collapsed' -and $script:SavedAppIds) { if ($presetLastUsed.Visibility -ne 'Collapsed' -and $script:SavedAppIds) {
SetPresetState $presetLastUsed { param($c) $script:SavedAppIds -contains $c.Tag } SetPresetState $presetLastUsed { param($c) (@($c.AppIds) | Where-Object { $script:SavedAppIds -contains $_ }).Count -gt 0 }
} }
} }
finally { finally {
@@ -547,8 +563,8 @@ function Show-MainWindow {
$helpBtn.Tag = (GetWikiUrlForCategory -category $categoryName) $helpBtn.Tag = (GetWikiUrlForCategory -category $categoryName)
$helpBtn.Style = $window.Resources['CategoryHelpLinkButtonStyle'] $helpBtn.Style = $window.Resources['CategoryHelpLinkButtonStyle']
$helpBtn.Add_Click({ $helpBtn.Add_Click({
param($sender, $e) param($button, $e)
if ($sender.Tag) { Start-Process $sender.Tag } if ($button.Tag) { Start-Process $button.Tag }
}) })
$headerRow.Children.Add($helpBtn) | Out-Null $headerRow.Children.Add($helpBtn) | Out-Null
@@ -718,6 +734,9 @@ function Show-MainWindow {
if ($appsToAdd.Count -eq 0) { if ($appsToAdd.Count -eq 0) {
$window.FindName('DeploymentApplyBtn').IsEnabled = $true $window.FindName('DeploymentApplyBtn').IsEnabled = $true
if ($importConfigBtn) {
$importConfigBtn.IsEnabled = $true
}
return return
} }
@@ -732,9 +751,9 @@ function Show-MainWindow {
$app = $appsToAdd[$i] $app = $appsToAdd[$i]
$checkbox = New-Object System.Windows.Controls.CheckBox $checkbox = New-Object System.Windows.Controls.CheckBox
$automationName = if ($app.FriendlyName) { $app.FriendlyName } elseif ($app.AppId) { $app.AppId } else { $null } $automationName = if ($app.FriendlyName) { $app.FriendlyName } elseif ($app.AppIdDisplay) { $app.AppIdDisplay } else { $null }
if ($automationName) { $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $automationName) } if ($automationName) { $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $automationName) }
$checkbox.Tag = $app.AppId $checkbox.Tag = $app.AppIdDisplay
$checkbox.IsChecked = $app.IsChecked $checkbox.IsChecked = $app.IsChecked
$checkbox.Style = $window.Resources['AppsPanelCheckBoxStyle'] $checkbox.Style = $window.Resources['AppsPanelCheckBoxStyle']
@@ -770,9 +789,9 @@ function Show-MainWindow {
[System.Windows.Controls.Grid]::SetColumn($tbDesc, 2) [System.Windows.Controls.Grid]::SetColumn($tbDesc, 2)
$tbId = New-Object System.Windows.Controls.TextBlock $tbId = New-Object System.Windows.Controls.TextBlock
$tbId.Text = $app.AppId $tbId.Text = $app.AppIdDisplay
$tbId.Style = $window.Resources['AppIdTextStyle'] $tbId.Style = $window.Resources["AppIdTextStyle"]
$tbId.ToolTip = $app.AppId $tbId.ToolTip = $app.AppIdDisplay
[System.Windows.Controls.Grid]::SetColumn($tbId, 3) [System.Windows.Controls.Grid]::SetColumn($tbId, 3)
$row.Children.Add($dot) | Out-Null $row.Children.Add($dot) | Out-Null
@@ -784,6 +803,8 @@ function Show-MainWindow {
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppName' -Value $app.FriendlyName Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppName' -Value $app.FriendlyName
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppDescription' -Value $app.Description Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppDescription' -Value $app.Description
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'SelectedByDefault' -Value $app.SelectedByDefault Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'SelectedByDefault' -Value $app.SelectedByDefault
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppIds' -Value @($app.AppId)
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppIdDisplay' -Value $app.AppIdDisplay
$checkbox.Add_Checked({ UpdateAppSelectionStatus }) $checkbox.Add_Checked({ UpdateAppSelectionStatus })
$checkbox.Add_Unchecked({ UpdateAppSelectionStatus }) $checkbox.Add_Unchecked({ UpdateAppSelectionStatus })
@@ -808,6 +829,9 @@ function Show-MainWindow {
# Re-enable Apply button now that the full, correctly-checked app list is ready # Re-enable Apply button now that the full, correctly-checked app list is ready
$window.FindName('DeploymentApplyBtn').IsEnabled = $true $window.FindName('DeploymentApplyBtn').IsEnabled = $true
if ($importConfigBtn) {
$importConfigBtn.IsEnabled = $true
}
} }
# Loads apps into the UI # Loads apps into the UI
@@ -816,6 +840,10 @@ function Show-MainWindow {
if ($script:IsLoadingApps) { return } if ($script:IsLoadingApps) { return }
$script:IsLoadingApps = $true $script:IsLoadingApps = $true
if ($importConfigBtn) {
$importConfigBtn.IsEnabled = $false
}
# Show loading indicator and clear existing apps # Show loading indicator and clear existing apps
$loadingAppsIndicator.Visibility = 'Visible' $loadingAppsIndicator.Visibility = 'Visible'
$appsPanel.Children.Clear() $appsPanel.Children.Clear()
@@ -863,34 +891,29 @@ function Show-MainWindow {
# Animate arrow when popup opens/closes, and lazily update preset states # Animate arrow when popup opens/closes, and lazily update preset states
$presetsPopup.Add_Opened({ $presetsPopup.Add_Opened({
UpdatePresetStates UpdatePresetStates
$animation = New-Object System.Windows.Media.Animation.DoubleAnimation AnimatePresetsArrow -angle 180
$animation.To = 180
$animation.Duration = [System.Windows.Duration]::new([System.TimeSpan]::FromMilliseconds(200))
$animation.EasingFunction = New-Object System.Windows.Media.Animation.CubicEase
$animation.EasingFunction.EasingMode = 'EaseOut'
$presetsArrow.RenderTransform.BeginAnimation([System.Windows.Media.RotateTransform]::AngleProperty, $animation)
}) })
$presetsPopup.Add_Closed({ $presetsPopup.Add_Closed({
$animation = New-Object System.Windows.Media.Animation.DoubleAnimation AnimatePresetsArrow -angle 0
$animation.To = 0
$animation.Duration = [System.Windows.Duration]::new([System.TimeSpan]::FromMilliseconds(200))
$animation.EasingFunction = New-Object System.Windows.Media.Animation.CubicEase
$animation.EasingFunction.EasingMode = 'EaseOut'
$presetsArrow.RenderTransform.BeginAnimation([System.Windows.Media.RotateTransform]::AngleProperty, $animation)
$presetsBtn.IsChecked = $false $presetsBtn.IsChecked = $false
}) })
# Close popup when clicking anywhere outside the popup or the presets button. # Close popup when clicking anywhere outside the popup or the presets button.
$window.Add_PreviewMouseDown({ $window.Add_PreviewMouseDown({
if (-not $presetsPopup.IsOpen) { return } if (-not $presetsPopup.IsOpen) { return }
if ($presetsPopup.Child -ne $null -and $presetsPopup.Child.IsMouseOver) { return } if ($null -ne $presetsPopup.Child -and $presetsPopup.Child.IsMouseOver) { return }
$src = $_.OriginalSource -as [System.Windows.DependencyObject] $src = $_.OriginalSource -as [System.Windows.DependencyObject]
if ($src -ne $null) { if ($null -ne $src) {
$inBtn = $presetsBtn.IsAncestorOf($src) -or [System.Object]::ReferenceEquals($presetsBtn, $src) $inBtn = $presetsBtn.IsAncestorOf($src) -or [System.Object]::ReferenceEquals($presetsBtn, $src)
if (-not $inBtn) { $presetsPopup.IsOpen = $false } if (-not $inBtn) { $presetsPopup.IsOpen = $false }
} }
}) })
# Close the preset menu when the main window loses focus (e.g., user switches to another app).
$window.Add_Deactivated({
if ($presetsPopup.IsOpen) { $presetsPopup.IsOpen = $false }
})
# Toggle popup on button click # Toggle popup on button click
$presetsBtn.Add_Click({ $presetsBtn.Add_Click({
$presetsPopup.IsOpen = -not $presetsPopup.IsOpen $presetsPopup.IsOpen = -not $presetsPopup.IsOpen
@@ -900,8 +923,7 @@ function Show-MainWindow {
# Preset: Default selection # Preset: Default selection
$presetDefaultApps.Add_Click({ $presetDefaultApps.Add_Click({
if ($script:UpdatingPresets) { return } if ($script:UpdatingPresets) { return }
$check = ($this.IsChecked -eq $true) $check = NormalizeCheckboxState -checkBox $this
if ($this.IsChecked -eq $null) { $this.IsChecked = $false; $check = $false }
ApplyPresetToApps -MatchFilter { param($c) $c.SelectedByDefault -eq $true } -Check $check ApplyPresetToApps -MatchFilter { param($c) $c.SelectedByDefault -eq $true } -Check $check
}) })
@@ -1018,7 +1040,7 @@ function Show-MainWindow {
}) })
$appSearchBox.Add_KeyDown({ $appSearchBox.Add_KeyDown({
param($sender, $e) param($sourceControl, $e)
if ($e.Key -eq [System.Windows.Input.Key]::Enter -and $script:AppSearchMatches.Count -gt 0) { if ($e.Key -eq [System.Windows.Input.Key]::Enter -and $script:AppSearchMatches.Count -gt 0) {
# Reset background of current active match # Reset background of current active match
$script:AppSearchMatches[$script:AppSearchMatchIndex].Background = $window.Resources["SearchHighlightColor"] $script:AppSearchMatches[$script:AppSearchMatchIndex].Background = $window.Resources["SearchHighlightColor"]
@@ -1141,7 +1163,7 @@ function Show-MainWindow {
# Add Ctrl+F keyboard shortcut to focus search box on current tab # Add Ctrl+F keyboard shortcut to focus search box on current tab
$window.Add_KeyDown({ $window.Add_KeyDown({
param($sender, $e) param($sourceControl, $e)
# Check if Ctrl+F was pressed # Check if Ctrl+F was pressed
if ($e.Key -eq [System.Windows.Input.Key]::F -and if ($e.Key -eq [System.Windows.Input.Key]::F -and
@@ -1250,7 +1272,7 @@ function Show-MainWindow {
$appRemovalScopeCombo.SelectedIndex = 0 $appRemovalScopeCombo.SelectedIndex = 0
} }
1 { 1 {
$userSelectionDescription.Text = "Changes will be applied to a different user profile on this system. Note: changes may not apply correctly if the target user is currently logged in." $userSelectionDescription.Text = "Changes will be applied to a different user profile on this system."
$otherUserPanel.Visibility = 'Visible' $otherUserPanel.Visibility = 'Visible'
$usernameValidationMessage.Text = "" $usernameValidationMessage.Text = ""
# Hide "Current user only" option, show "Target user only" option # Hide "Current user only" option, show "Target user only" option
@@ -1320,13 +1342,13 @@ function Show-MainWindow {
$successBrush = $window.Resources['ValidationSuccessColor'] $successBrush = $window.Resources['ValidationSuccessColor']
if ($username.Length -eq 0) { if ($username.Length -eq 0) {
$usernameValidationMessage.Text = "[X] Please enter a username" $usernameValidationMessage.Text = "Please enter a username"
$usernameValidationMessage.Foreground = $errorBrush $usernameValidationMessage.Foreground = $errorBrush
return $false return $false
} }
if ($username -eq $env:USERNAME) { if ($username -eq $env:USERNAME) {
$usernameValidationMessage.Text = "[X] Cannot enter your own username, use 'Current User' option instead" $usernameValidationMessage.Text = "Cannot enter your own username, use 'Current User' option instead"
$usernameValidationMessage.Foreground = $errorBrush $usernameValidationMessage.Foreground = $errorBrush
return $false return $false
} }
@@ -1334,12 +1356,18 @@ function Show-MainWindow {
$userExists = CheckIfUserExists -Username $username $userExists = CheckIfUserExists -Username $username
if ($userExists) { if ($userExists) {
$usernameValidationMessage.Text = "[OK] User found: $username" if (TestIfUserIsLoggedIn -Username $username) {
$usernameValidationMessage.Text = "User '$username' is currently logged in. Please sign out that user first."
$usernameValidationMessage.Foreground = $errorBrush
return $false
}
$usernameValidationMessage.Text = "User found: $username"
$usernameValidationMessage.Foreground = $successBrush $usernameValidationMessage.Foreground = $successBrush
return $true return $true
} }
$usernameValidationMessage.Text = "[X] User not found, please enter a valid username" $usernameValidationMessage.Text = "User not found, please enter a valid username"
$usernameValidationMessage.Foreground = $errorBrush $usernameValidationMessage.Foreground = $errorBrush
return $false return $false
} }
@@ -1482,7 +1510,13 @@ function Show-MainWindow {
$deploymentApplyBtn = $window.FindName('DeploymentApplyBtn') $deploymentApplyBtn = $window.FindName('DeploymentApplyBtn')
$deploymentApplyBtn.Add_Click({ $deploymentApplyBtn.Add_Click({
if (-not (ValidateOtherUsername)) { if (-not (ValidateOtherUsername)) {
Show-MessageBox -Message "Please enter a valid username." -Title "Invalid Username" -Button 'OK' -Icon 'Warning' | Out-Null $validationMessage = if (-not [string]::IsNullOrWhiteSpace($usernameValidationMessage.Text)) {
$usernameValidationMessage.Text
}
else {
"Please enter a valid username."
}
Show-MessageBox -Message $validationMessage -Title "Invalid Username" -Button 'OK' -Icon 'Warning' | Out-Null
return return
} }
@@ -1492,9 +1526,10 @@ function Show-MainWindow {
$selectedApps = @() $selectedApps = @()
foreach ($child in $appsPanel.Children) { foreach ($child in $appsPanel.Children) {
if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) { if ($child -is [System.Windows.Controls.CheckBox] -and $child.IsChecked) {
$selectedApps += $child.Tag $selectedApps += @($child.AppIds)
} }
} }
$selectedApps = @($selectedApps | Where-Object { $_ } | Select-Object -Unique)
if ($selectedApps.Count -gt 0) { if ($selectedApps.Count -gt 0) {
# Check if Microsoft Store is selected # Check if Microsoft Store is selected
@@ -1713,9 +1748,8 @@ function Show-MainWindow {
$presetLastUsed.Add_Click({ $presetLastUsed.Add_Click({
if ($script:UpdatingPresets) { return } if ($script:UpdatingPresets) { return }
$check = ($this.IsChecked -eq $true) $check = NormalizeCheckboxState -checkBox $this
if ($this.IsChecked -eq $null) { $this.IsChecked = $false; $check = $false } ApplyPresetToApps -MatchFilter { param($c) (@($c.AppIds) | Where-Object { $script:SavedAppIds -contains $_ }).Count -gt 0 } -Check $check
ApplyPresetToApps -MatchFilter { param($c) $script:SavedAppIds -contains $c.Tag } -Check $check
}) })
} }
else { else {
@@ -1749,5 +1783,12 @@ function Show-MainWindow {
} }
# Show the window # Show the window
return $window.ShowDialog() $frame = [System.Windows.Threading.DispatcherFrame]::new()
$window.Add_Closed({
$frame.Continue = $false
})
$window.Show() | Out-Null
[System.Windows.Threading.Dispatcher]::PushFrame($frame)
return $null
} }

View File

@@ -152,14 +152,17 @@ function Show-MessageBox {
}) })
# Show dialog and return result from Tag # Show dialog and return result from Tag
$msgWindow.ShowDialog() | Out-Null try {
$msgWindow.ShowDialog() | Out-Null
# Hide overlay after dialog closes (only if this dialog was the one that showed it) }
if ($overlay -and -not $overlayWasAlreadyVisible) { finally {
try { # Hide overlay after dialog closes (only if this dialog was the one that showed it)
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' }) if ($overlay -and -not $overlayWasAlreadyVisible) {
try {
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' })
}
catch { }
} }
catch { }
} }
return $msgWindow.Tag return $msgWindow.Tag

View File

@@ -11,6 +11,7 @@ param (
[switch]$RunDefaults, [switch]$RunDefaults,
[switch]$RunDefaultsLite, [switch]$RunDefaultsLite,
[switch]$RunSavedSettings, [switch]$RunSavedSettings,
[string]$Config,
[string]$Apps, [string]$Apps,
[string]$AppRemovalTarget, [string]$AppRemovalTarget,
[switch]$RemoveApps, [switch]$RemoveApps,
@@ -95,7 +96,11 @@ param (
[switch]$HideMusic, [switch]$HideMusic,
[switch]$HideIncludeInLibrary, [switch]$HideIncludeInLibrary,
[switch]$HideGiveAccessTo, [switch]$HideGiveAccessTo,
[switch]$HideShare [switch]$HideShare,
[switch]$ShowDriveLettersFirst,
[switch]$ShowDriveLettersLast,
[switch]$ShowNetworkDriveLettersFirst,
[switch]$HideDriveLetters
) )
# Show error if current powershell environment does not have LanguageMode set to FullLanguage # Show error if current powershell environment does not have LanguageMode set to FullLanguage

View File

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

View File

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

View File

@@ -13,6 +13,7 @@ param (
[switch]$RunDefaults, [switch]$RunDefaults,
[switch]$RunDefaultsLite, [switch]$RunDefaultsLite,
[switch]$RunSavedSettings, [switch]$RunSavedSettings,
[string]$Config,
[string]$Apps, [string]$Apps,
[string]$AppRemovalTarget, [string]$AppRemovalTarget,
[switch]$RemoveApps, [switch]$RemoveApps,
@@ -97,7 +98,11 @@ param (
[switch]$HideMusic, [switch]$HideMusic,
[switch]$HideIncludeInLibrary, [switch]$HideIncludeInLibrary,
[switch]$HideGiveAccessTo, [switch]$HideGiveAccessTo,
[switch]$HideShare [switch]$HideShare,
[switch]$ShowDriveLettersFirst,
[switch]$ShowDriveLettersLast,
[switch]$ShowNetworkDriveLettersFirst,
[switch]$HideDriveLetters
) )
@@ -119,9 +124,10 @@ $script:AboutWindowSchema = "$PSScriptRoot/Schemas/AboutWindow.xaml"
$script:ApplyChangesWindowSchema = "$PSScriptRoot/Schemas/ApplyChangesWindow.xaml" $script:ApplyChangesWindowSchema = "$PSScriptRoot/Schemas/ApplyChangesWindow.xaml"
$script:SharedStylesSchema = "$PSScriptRoot/Schemas/SharedStyles.xaml" $script:SharedStylesSchema = "$PSScriptRoot/Schemas/SharedStyles.xaml"
$script:BubbleHintSchema = "$PSScriptRoot/Schemas/BubbleHint.xaml" $script:BubbleHintSchema = "$PSScriptRoot/Schemas/BubbleHint.xaml"
$script:ImportExportConfigSchema = "$PSScriptRoot/Schemas/ImportExportConfigWindow.xaml"
$script:LoadAppsDetailsScriptPath = "$PSScriptRoot/Scripts/FileIO/LoadAppsDetailsFromJson.ps1" $script:LoadAppsDetailsScriptPath = "$PSScriptRoot/Scripts/FileIO/LoadAppsDetailsFromJson.ps1"
$script:ControlParams = 'WhatIf', 'Confirm', 'Verbose', 'Debug', 'LogPath', 'Silent', 'Sysprep', 'User', 'NoRestartExplorer', 'RunDefaults', 'RunDefaultsLite', 'RunSavedSettings', 'RunAppsListGenerator', 'CLI', 'AppRemovalTarget' $script:ControlParams = 'WhatIf', 'Confirm', 'Verbose', 'Debug', 'LogPath', 'Silent', 'Sysprep', 'User', 'NoRestartExplorer', 'RunDefaults', 'RunDefaultsLite', 'RunSavedSettings', 'Config', 'RunAppsListGenerator', 'CLI', 'AppRemovalTarget'
# Script-level variables for GUI elements # Script-level variables for GUI elements
$script:GuiWindow = $null $script:GuiWindow = $null
@@ -249,6 +255,7 @@ if (-not $script:WingetInstalled -and -not $Silent) {
# File I/O functions # File I/O functions
. "$PSScriptRoot/Scripts/FileIO/LoadJsonFile.ps1" . "$PSScriptRoot/Scripts/FileIO/LoadJsonFile.ps1"
. "$PSScriptRoot/Scripts/FileIO/SaveToFile.ps1"
. "$PSScriptRoot/Scripts/FileIO/SaveSettings.ps1" . "$PSScriptRoot/Scripts/FileIO/SaveSettings.ps1"
. "$PSScriptRoot/Scripts/FileIO/LoadSettings.ps1" . "$PSScriptRoot/Scripts/FileIO/LoadSettings.ps1"
. "$PSScriptRoot/Scripts/FileIO/SaveCustomAppsListToFile.ps1" . "$PSScriptRoot/Scripts/FileIO/SaveCustomAppsListToFile.ps1"
@@ -263,6 +270,7 @@ if (-not $script:WingetInstalled -and -not $Silent) {
. "$PSScriptRoot/Scripts/GUI/AttachShiftClickBehavior.ps1" . "$PSScriptRoot/Scripts/GUI/AttachShiftClickBehavior.ps1"
. "$PSScriptRoot/Scripts/GUI/ApplySettingsToUiControls.ps1" . "$PSScriptRoot/Scripts/GUI/ApplySettingsToUiControls.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-MessageBox.ps1" . "$PSScriptRoot/Scripts/GUI/Show-MessageBox.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-ConfigWindow.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-ApplyModal.ps1" . "$PSScriptRoot/Scripts/GUI/Show-ApplyModal.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-AppSelectionWindow.ps1" . "$PSScriptRoot/Scripts/GUI/Show-AppSelectionWindow.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-MainWindow.ps1" . "$PSScriptRoot/Scripts/GUI/Show-MainWindow.ps1"
@@ -275,9 +283,11 @@ if (-not $script:WingetInstalled -and -not $Silent) {
. "$PSScriptRoot/Scripts/Helpers/CheckModernStandbySupport.ps1" . "$PSScriptRoot/Scripts/Helpers/CheckModernStandbySupport.ps1"
. "$PSScriptRoot/Scripts/Helpers/GenerateAppsList.ps1" . "$PSScriptRoot/Scripts/Helpers/GenerateAppsList.ps1"
. "$PSScriptRoot/Scripts/Helpers/GetFriendlyTargetUserName.ps1" . "$PSScriptRoot/Scripts/Helpers/GetFriendlyTargetUserName.ps1"
. "$PSScriptRoot/Scripts/Helpers/ImportConfigToParams.ps1"
. "$PSScriptRoot/Scripts/Helpers/GetTargetUserForAppRemoval.ps1" . "$PSScriptRoot/Scripts/Helpers/GetTargetUserForAppRemoval.ps1"
. "$PSScriptRoot/Scripts/Helpers/GetUserDirectory.ps1" . "$PSScriptRoot/Scripts/Helpers/GetUserDirectory.ps1"
. "$PSScriptRoot/Scripts/Helpers/GetUserName.ps1" . "$PSScriptRoot/Scripts/Helpers/GetUserName.ps1"
. "$PSScriptRoot/Scripts/Helpers/TestIfUserIsLoggedIn.ps1"
# Threading functions # Threading functions
. "$PSScriptRoot/Scripts/Threading/DoEvents.ps1" . "$PSScriptRoot/Scripts/Threading/DoEvents.ps1"
@@ -351,6 +361,9 @@ if ((Test-Path $script:SavedSettingsFilePath) -and ([String]::IsNullOrWhiteSpace
Remove-Item -Path $script:SavedSettingsFilePath -recurse Remove-Item -Path $script:SavedSettingsFilePath -recurse
} }
# Default to CLI mode for deployment-targeted parameters.
$launchInCLI = $CLI -or $script:Params.ContainsKey("User") -or $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("AppRemovalTarget")
# Only run the app selection form if the 'RunAppsListGenerator' parameter was passed to the script # Only run the app selection form if the 'RunAppsListGenerator' parameter was passed to the script
if ($RunAppsListGenerator) { if ($RunAppsListGenerator) {
PrintHeader "Custom Apps List Generator" PrintHeader "Custom Apps List Generator"
@@ -370,7 +383,7 @@ if ($RunAppsListGenerator) {
} }
# Change script execution based on provided parameters or user input # Change script execution based on provided parameters or user input
if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSavedSettings -or ($controlParamsCount -eq $script:Params.Count)) { if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSavedSettings -or $Config -or ($controlParamsCount -eq $script:Params.Count)) {
if ($RunDefaults -or $RunDefaultsLite) { if ($RunDefaults -or $RunDefaultsLite) {
ShowCLIDefaultModeOptions ShowCLIDefaultModeOptions
} }
@@ -383,8 +396,23 @@ if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSa
ShowCLILastUsedSettings ShowCLILastUsedSettings
} }
elseif ($Config) {
try {
ImportConfigToParams -ConfigPath $Config -CurrentBuild $WinVersion -ExpectedVersion '1.0'
}
catch {
Write-Error "$_"
AwaitKeyToExit
}
if (-not $Silent) {
PrintHeader 'Custom Mode'
PrintPendingChanges
PrintHeader 'Custom Mode'
}
}
else { else {
if ($CLI) { if ($launchInCLI) {
$Mode = ShowCLIMenuOptions $Mode = ShowCLIMenuOptions
} }
else { else {