Add registry backup & restore (#566)

Starting from this commit, Win11Debloat will automatically create a registry backup every time the script is run. This registry backup can be used to revert any registry changes made by the script.
This commit is contained in:
Jeffrey
2026-05-08 21:19:52 +02:00
committed by GitHub
parent 11a324365d
commit 2c360961e3
37 changed files with 3193 additions and 719 deletions

View File

@@ -0,0 +1,121 @@
function New-RestoreDialogState {
param(
[string]$Result = 'Cancel',
[string]$SelectedFile = $null,
$Backup = $null
)
return @{ Result = $Result; SelectedFile = $SelectedFile; Backup = $Backup }
}
function Get-RestoreDialogFeatureDefinition {
param(
[string]$FeatureId,
[hashtable]$Features
)
if ([string]::IsNullOrWhiteSpace($FeatureId) -or -not $Features) {
return $null
}
if ($Features.ContainsKey($FeatureId)) {
return $Features[$FeatureId]
}
return $null
}
function Test-RestoreDialogFeatureCanAutoRevert {
param(
[string]$FeatureId,
[hashtable]$Features
)
if ([string]::IsNullOrWhiteSpace($FeatureId)) {
return $false
}
$featureDefinition = Get-RestoreDialogFeatureDefinition -FeatureId $FeatureId -Features $Features
if ($featureDefinition) {
return -not [string]::IsNullOrWhiteSpace([string]$featureDefinition.RegistryKey)
}
return $false
}
function Get-RestoreDialogFeatureDisplayLabel {
param(
[string]$FeatureId,
[hashtable]$Features
)
if ([string]::IsNullOrWhiteSpace($FeatureId)) {
return 'Unknown feature'
}
$featureDefinition = Get-RestoreDialogFeatureDefinition -FeatureId $FeatureId -Features $Features
if ($featureDefinition -and -not [string]::IsNullOrWhiteSpace([string]$featureDefinition.Label)) {
return [string]$featureDefinition.Label
}
return $FeatureId
}
function Test-RestoreDialogFeatureVisibleInOverview {
param(
[string]$FeatureId,
[hashtable]$Features
)
if ([string]::IsNullOrWhiteSpace($FeatureId)) {
return $false
}
$featureDefinition = Get-RestoreDialogFeatureDefinition -FeatureId $FeatureId -Features $Features
if (-not $featureDefinition) {
return $false
}
return -not [string]::IsNullOrWhiteSpace([string]$featureDefinition.Category)
}
function Get-SelectedFeatureIdsFromBackup {
param($SelectedBackup)
return @(
foreach ($featureId in @($SelectedBackup.SelectedFeatures)) {
if (-not [string]::IsNullOrWhiteSpace([string]$featureId)) {
[string]$featureId
}
}
)
}
function Get-RestoreBackupFeatureLists {
param(
[string[]]$SelectedFeatureIds,
[hashtable]$Features
)
$revertibleFeaturesList = @()
$nonRevertibleFeaturesList = @()
foreach ($featureId in $SelectedFeatureIds) {
if (-not (Test-RestoreDialogFeatureVisibleInOverview -FeatureId $featureId -Features $Features)) {
continue
}
$displayItem = [PSCustomObject]@{ DisplayText = "- $(Get-RestoreDialogFeatureDisplayLabel -FeatureId $featureId -Features $Features)" }
if (Test-RestoreDialogFeatureCanAutoRevert -FeatureId $featureId -Features $Features) {
$revertibleFeaturesList += $displayItem
}
else {
$nonRevertibleFeaturesList += $displayItem
}
}
return [PSCustomObject]@{
Revertible = @($revertibleFeaturesList)
NonRevertible = @($nonRevertibleFeaturesList)
}
}

View File

@@ -75,6 +75,7 @@ function SetWindowThemeResources {
$window.Resources.Add("ButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#3284cc")))
$window.Resources.Add("CloseHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#c42b1c")))
$window.Resources.Add("InformationIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0078D4")))
$window.Resources.Add("SuccessIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#107C10")))
$window.Resources.Add("WarningIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFB900")))
$window.Resources.Add("ErrorIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#E81123")))
$window.Resources.Add("QuestionIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0078D4")))

View File

@@ -162,7 +162,7 @@ function Show-ApplyModal {
foreach ($paramKey in $script:Params.Keys) {
if ($script:Features.ContainsKey($paramKey) -and $script:Features[$paramKey].RequiresReboot -eq $true) {
$feature = $script:Features[$paramKey]
$rebootFeatures += "$($feature.Action) $($feature.Label)"
$rebootFeatures += "$($feature.Label)"
}
}

View File

@@ -6,7 +6,8 @@ function Show-ImportExportConfigWindow {
[string]$Prompt,
[string[]]$Categories = @('Applications', 'System Tweaks', 'Deployment Settings'),
[string[]]$DisabledCategories = @(),
[hashtable]$CategoryDetails = @()
[hashtable]$CategoryDetails = @(),
[string]$ActionLabel = 'OK'
)
# Show overlay on owner window
@@ -105,6 +106,7 @@ function Show-ImportExportConfigWindow {
$okBtn = $dlg.FindName('OkButton')
$cancelBtn = $dlg.FindName('CancelButton')
$okBtn.Content = $ActionLabel
$okBtn.Add_Click({ $dlg.Tag = 'OK'; $dlg.Close() })
$cancelBtn.Add_Click({ $dlg.Tag = 'Cancel'; $dlg.Close() })
@@ -379,8 +381,11 @@ function Export-Configuration {
$deploymentSettings = Get-DeploymentSettings -Owner $Owner -UserSelectionCombo $UserSelectionCombo -OtherUsernameTextBox $OtherUsernameTextBox
$categoryDetails = Build-CategoryDetails -AppCount $selectedApps.Count -TweakCount $tweakSettings.Count -DeploymentSettings $deploymentSettings
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Export Configuration' -Prompt 'Select which settings to include in the export:' -DisabledCategories $disabledCategories -CategoryDetails $categoryDetails
if (-not $categories) { return }
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Export Configuration' -Prompt 'Select the settings you wish to include in your export.' -DisabledCategories $disabledCategories -CategoryDetails $categoryDetails -ActionLabel 'Export Settings'
if (-not $categories) {
Write-Host 'Export canceled.'
return
}
$config = @{ Version = '1.0' }
@@ -401,12 +406,19 @@ function Export-Configuration {
$saveDialog.DefaultExt = '.json'
$saveDialog.FileName = "Win11Debloat-Config-$(Get-Date -Format 'yyyyMMdd').json"
if ($saveDialog.ShowDialog($Owner) -ne $true) { return }
if ($saveDialog.ShowDialog($Owner) -ne $true) {
Write-Host 'Export save dialog canceled.'
return
}
Write-Host "Exporting configuration to '$($saveDialog.FileName)'... (Categories: $($categories -join ', '))"
if (SaveToFile -Config $config -FilePath $saveDialog.FileName) {
Write-Host "Configuration exported successfully: $($saveDialog.FileName)"
Show-MessageBox -Message "Configuration exported successfully." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null
}
else {
Write-Error "Failed to export configuration to '$($saveDialog.FileName)'"
Show-MessageBox -Message "Failed to export configuration" -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null
}
}
@@ -425,36 +437,49 @@ function Import-Configuration {
# Show native open-file dialog
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
$openDialog.Title = 'Import Configuration'
$openDialog.Title = 'Select Configuration File'
$openDialog.Filter = 'JSON files (*.json)|*.json|All files (*.*)|*.*'
$openDialog.DefaultExt = '.json'
if ($openDialog.ShowDialog($Owner) -ne $true) { return }
if ($openDialog.ShowDialog($Owner) -ne $true) {
Write-Host 'Import file dialog canceled.'
return
}
Write-Host "Importing configuration from '$($openDialog.FileName)'..."
$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
Write-Error "Failed to read configuration file '$($openDialog.FileName)'"
Show-MessageBox -Message "Failed to read configuration file" -Title 'Invalid Config' -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
Write-Error "Invalid configuration file format: '$($openDialog.FileName)'"
Show-MessageBox -Message "Invalid configuration file format." -Title 'Invalid Config' -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
Write-Warning "Configuration file '$($openDialog.FileName)' contains no importable data."
Show-MessageBox -Message "The selected file contains no importable data." -Title 'Invalid Config' -Button 'OK' -Icon 'Error' | Out-Null
return
}
Write-Host "Available categories in config: $($availableCategories -join ', ')"
$appCount = @($config.Apps | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) }).Count
$tweakCount = @($config.Tweaks | Where-Object { $_ -and $_.Name -and $_.Value -eq $true }).Count
$categoryDetails = Build-CategoryDetails -AppCount $appCount -TweakCount $tweakCount -DeploymentSettings @($config.Deployment)
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Import Configuration' -Prompt 'Select which settings to import:' -Categories $availableCategories -CategoryDetails $categoryDetails
if (-not $categories) { return }
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Import Configuration' -Prompt 'Select the settings you wish to import. You can review and modify them before they are applied.' -Categories $availableCategories -CategoryDetails $categoryDetails -ActionLabel 'Import Settings'
if (-not $categories) {
Write-Host 'Import canceled.'
return
}
if ($categories -contains 'Applications' -and $config.Apps) {
$appIds = @(
@@ -464,6 +489,7 @@ function Import-Configuration {
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
)
Write-Host "Importing $($appIds.Count) app selection(s)."
Apply-ImportedApplications -AppsPanel $AppsPanel -AppIds $appIds
if ($OnAppsImported) {
@@ -471,12 +497,16 @@ function Import-Configuration {
}
}
if ($categories -contains 'System Tweaks' -and $config.Tweaks) {
$tweakCount = @($config.Tweaks).Count
Write-Host "Importing $tweakCount tweak(s)."
Apply-ImportedTweakSettings -Owner $Owner -UiControlMappings $UiControlMappings -TweakSettings @($config.Tweaks)
}
if ($categories -contains 'Deployment Settings' -and $config.Deployment) {
Write-Host 'Importing deployment settings.'
Apply-ImportedDeploymentSettings -Owner $Owner -UserSelectionCombo $UserSelectionCombo -OtherUsernameTextBox $OtherUsernameTextBox -DeploymentSettings @($config.Deployment)
}
Write-Host 'Configuration imported successfully.'
Show-MessageBox -Message "Configuration imported successfully." -Title 'Import Configuration' -Button 'OK' -Icon 'Information' | Out-Null
if ($OnImportCompleted) {

View File

@@ -96,6 +96,7 @@ function Show-MainWindow {
$menuAbout = $window.FindName('MenuAbout')
$importConfigBtn = $window.FindName('ImportConfigBtn')
$exportConfigBtn = $window.FindName('ExportConfigBtn')
$restoreBackupBtn = $window.FindName('RestoreBackupBtn')
$windowStateNormal = [System.Windows.WindowState]::Normal
$windowStateMaximized = [System.Windows.WindowState]::Maximized
@@ -234,7 +235,7 @@ function Show-MainWindow {
})
$menuLogs.Add_Click({
$logsFolder = Join-Path $PSScriptRoot "../../Logs"
$logsFolder = Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) 'Logs'
if (Test-Path $logsFolder) {
Start-Process "explorer.exe" -ArgumentList $logsFolder
}
@@ -247,22 +248,45 @@ function Show-MainWindow {
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
try {
Export-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox
}
catch {
Write-Warning "Export configuration failed: $($_.Exception.Message)"
Show-MessageBox -Owner $window -Message "Unable to open export configuration dialog: $($_.Exception.Message)" -Title 'Export Configuration Failed' -Button 'OK' -Icon 'Error' | Out-Null
}
})
$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
try {
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
$window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Loaded, [action]{
Show-Bubble -TargetControl $reviewChangesBtn -Message 'View the selected changes here'
}) | Out-Null
}
}
catch {
Write-Warning "Import configuration failed: $($_.Exception.Message)"
Show-MessageBox -Owner $window -Message "Unable to open import configuration dialog: $($_.Exception.Message)" -Title 'Import Configuration Failed' -Button 'OK' -Icon 'Error' | Out-Null
}
})
if ($restoreBackupBtn) {
$restoreBackupBtn.Add_Click({
try {
Show-RestoreBackupWindow -Owner $window
}
catch {
Write-Warning "Restore backup action failed: $($_.Exception.Message)"
Show-MessageBox -Owner $window -Message "Unable to open restore backup dialog: $($_.Exception.Message)" -Title 'Restore Backup Failed' -Button 'OK' -Icon 'Error' | Out-Null
}
})
}
$closeBtn.Add_Click({
$window.Close()
})
@@ -352,6 +376,20 @@ function Show-MainWindow {
}
}
function ClearTweakSelections {
if (-not $script:UiControlMappings) { return }
foreach ($controlName in $script:UiControlMappings.Keys) {
$control = $window.FindName($controlName)
if ($control -is [System.Windows.Controls.CheckBox]) {
$control.IsChecked = $false
}
elseif ($control -is [System.Windows.Controls.ComboBox]) {
$control.SelectedIndex = 0
}
}
}
function AnimateDropdownArrow {
param(
[System.Windows.Controls.TextBlock]$arrow,
@@ -843,7 +881,7 @@ function Show-MainWindow {
if ($feature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($feature.FeatureId -match '^Enable') { $opt = 'Enable' }
$items = @('No Change', $opt)
$comboName = ("Feature_{0}_Combo" -f $feature.FeatureId) -replace '[^a-zA-Z0-9_]',''
$combo = CreateLabeledCombo -parent $panel -labelText ($feature.Action + ' ' + $feature.Label) -comboName $comboName -items $items
$combo = CreateLabeledCombo -parent $panel -labelText $feature.Label -comboName $comboName -items $items
# attach tooltip from Features.json if present
if ($feature.ToolTip) {
$tipBlock = New-Object System.Windows.Controls.TextBlock
@@ -855,7 +893,7 @@ function Show-MainWindow {
try { $lblBorderObj = $window.FindName("$comboName`_LabelBorder") } catch {}
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
}
$script:UiControlMappings[$comboName] = @{ Type='feature'; FeatureId = $feature.FeatureId; Action = $feature.Action; Label = $feature.Label; Category = $categoryName }
$script:UiControlMappings[$comboName] = @{ Type='feature'; FeatureId = $feature.FeatureId; Label = $feature.Label; Category = $categoryName }
}
}
}
@@ -863,7 +901,7 @@ function Show-MainWindow {
# Build a feature-label lookup so GenerateOverview can resolve feature IDs without reloading JSON
$script:FeatureLabelLookup = @{}
foreach ($f in $featuresJson.Features) {
$script:FeatureLabelLookup[$f.FeatureId] = $f.Action + ' ' + $f.Label
$script:FeatureLabelLookup[$f.FeatureId] = $f.Label
}
}
@@ -1508,8 +1546,6 @@ function Show-MainWindow {
# Show "Current user only" option, hide "Target user only" option
$appRemovalScopeCurrentUser.Visibility = 'Visible'
$appRemovalScopeTargetUser.Visibility = 'Collapsed'
# Enable app removal scope selection for current user
$appRemovalScopeCombo.IsEnabled = $true
$appRemovalScopeCombo.SelectedIndex = 0
}
1 {
@@ -1519,8 +1555,6 @@ function Show-MainWindow {
# Hide "Current user only" option, show "Target user only" option
$appRemovalScopeCurrentUser.Visibility = 'Collapsed'
$appRemovalScopeTargetUser.Visibility = 'Visible'
# Enable app removal scope selection for other user
$appRemovalScopeCombo.IsEnabled = $true
$appRemovalScopeCombo.SelectedIndex = 0
}
2 {
@@ -1531,10 +1565,12 @@ function Show-MainWindow {
$appRemovalScopeCurrentUser.Visibility = 'Collapsed'
$appRemovalScopeTargetUser.Visibility = 'Collapsed'
# Lock app removal scope to "All users" when applying to sysprep
$appRemovalScopeCombo.IsEnabled = $false
$appRemovalScopeCombo.SelectedIndex = 0
}
}
# Keep enabled/disabled state in sync with both app selection and user mode.
UpdateAppSelectionStatus
})
# Helper function to update app removal scope description
@@ -1577,38 +1613,16 @@ function Show-MainWindow {
return $true
}
$username = $otherUsernameTextBox.Text.Trim()
$errorBrush = $window.Resources['ValidationErrorColor']
$successBrush = $window.Resources['ValidationSuccessColor']
$validationResult = Test-TargetUserName -UserName $otherUsernameTextBox.Text
if ($username.Length -eq 0) {
$usernameValidationMessage.Text = "Please enter a username"
$usernameValidationMessage.Foreground = $errorBrush
return $false
}
if ($username -eq $env:USERNAME) {
$usernameValidationMessage.Text = "Cannot enter your own username, use 'Current User' option instead"
$usernameValidationMessage.Foreground = $errorBrush
return $false
}
$userExists = CheckIfUserExists -Username $username
if ($userExists) {
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.Text = $validationResult.Message
if ($validationResult.IsValid) {
$usernameValidationMessage.Foreground = $successBrush
return $true
}
$usernameValidationMessage.Text = "User not found, please enter a valid username"
$usernameValidationMessage.Foreground = $errorBrush
return $false
}
@@ -1655,7 +1669,7 @@ function Show-MainWindow {
}
elseif ($mapping.Type -eq 'feature') {
$label = $script:FeatureLabelLookup[$mapping.FeatureId]
if (-not $label) { $label = $mapping.Action + ' ' + $mapping.Label }
if (-not $label) { $label = $mapping.Label }
$changesList += $label
}
}
@@ -2210,18 +2224,7 @@ function Show-MainWindow {
# Clear All Tweaks button
$clearAllTweaksBtn = $window.FindName('ClearAllTweaksBtn')
$clearAllTweaksBtn.Add_Click({
# Reset all ComboBoxes to index 0 (No Change) and uncheck all CheckBoxes
if ($script:UiControlMappings) {
foreach ($comboName in $script:UiControlMappings.Keys) {
$control = $window.FindName($comboName)
if ($control -is [System.Windows.Controls.CheckBox]) {
$control.IsChecked = $false
}
elseif ($control -is [System.Windows.Controls.ComboBox]) {
$control.SelectedIndex = 0
}
}
}
ClearTweakSelections
UpdateTweakPresetStates
})

View File

@@ -12,7 +12,7 @@ function Show-MessageBox {
[string]$Button = 'OK',
[Parameter(Mandatory=$false)]
[ValidateSet('None', 'Information', 'Warning', 'Error', 'Question')]
[ValidateSet('None', 'Information', 'Success', 'Warning', 'Error', 'Question')]
[string]$Icon = 'None',
[Parameter(Mandatory=$false)]
@@ -90,6 +90,11 @@ function Show-MessageBox {
$iconText.Foreground = $msgWindow.FindResource('InformationIconColor')
$iconText.Visibility = 'Visible'
}
'Success' {
$iconText.Text = [char]0xE73E
$iconText.Foreground = $msgWindow.FindResource('SuccessIconColor')
$iconText.Visibility = 'Visible'
}
'Warning' {
$iconText.Text = [char]0xE7BA
$iconText.Foreground = $msgWindow.FindResource('WarningIconColor')

View File

@@ -0,0 +1,403 @@
function Show-RestoreBackupDialog {
param(
[Parameter(Mandatory = $false)]
[System.Windows.Window]$Owner = $null
)
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
$usesDarkMode = GetSystemUsesDarkMode
$ownerWindow = if ($Owner) { $Owner } else { $script:GuiWindow }
$overlay = $null
$overlayWasAlreadyVisible = $false
if ($ownerWindow) {
try {
$overlay = $ownerWindow.FindName('ModalOverlay')
if ($overlay) {
$overlayWasAlreadyVisible = ($overlay.Visibility -eq 'Visible')
if (-not $overlayWasAlreadyVisible) {
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Visible' })
}
}
}
catch { }
}
$schemaPath = $script:RestoreBackupWindowSchema
if (-not $schemaPath -or -not (Test-Path $schemaPath)) {
throw 'Restore backup window schema file could not be found.'
}
$xaml = Get-Content -Path $schemaPath -Raw
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
try {
$window = [System.Windows.Markup.XamlReader]::Load($reader)
}
finally {
$reader.Close()
}
if ($ownerWindow) {
try {
$window.Owner = $ownerWindow
}
catch { }
}
try {
SetWindowThemeResources -window $window -usesDarkMode $usesDarkMode
}
catch { }
$titleBar = $window.FindName('TitleBar')
$titleText = $window.FindName('TitleText')
$closeBtn = $window.FindName('CloseBtn')
$backBtn = $window.FindName('BackBtn')
$primaryActionBtn = $window.FindName('PrimaryActionBtn')
$chooseRegistryBtn = $window.FindName('ChooseRegistryBtn')
$chooseStartMenuBtn = $window.FindName('ChooseStartMenuBtn')
$restoreModeTabs = $window.FindName('RestoreModeTabs')
$startMenuIntroPanel = $window.FindName('StartMenuIntroPanel')
$startMenuScopeCombo = $window.FindName('StartMenuScopeCombo')
$startMenuAutoBackupCheck = $window.FindName('StartMenuAutoBackupCheck')
$introInfoPanel = $window.FindName('IntroInfoPanel')
$overviewPanel = $window.FindName('OverviewPanel')
$overviewFeaturesSection = $window.FindName('OverviewFeaturesSection')
$overviewSummaryText = $window.FindName('OverviewSummaryText')
$backupFileText = $window.FindName('BackupFileText')
$backupCreatedText = $window.FindName('BackupCreatedText')
$backupTargetText = $window.FindName('BackupTargetText')
$featuresItemsControl = $window.FindName('FeaturesItemsControl')
$nonRevertibleSeparator = $window.FindName('NonRevertibleSeparator')
$nonRevertiblePanel = $window.FindName('NonRevertiblePanel')
$nonRevertibleFeaturesItemsControl = $window.FindName('NonRevertibleFeaturesItemsControl')
$nonRevertibleWikiLink = $window.FindName('NonRevertibleWikiLink')
$titleBar.Add_MouseLeftButtonDown({ $window.DragMove() })
$window.Tag = New-RestoreDialogState
$chooseRegistryBtn.IsDefault = $true
$state = @{ WizardStep = 'SelectType'; SelectedRegistryBackup = $null; SelectedStartMenuBackupFilePath = $null }
$getStartMenuScopeInfo = {
$isAllUsersScope = ($startMenuScopeCombo.SelectedItem.Tag -eq 'AllUsers')
$scopeValue = if ($isAllUsersScope) { 'AllUsers' } else { 'CurrentUser' }
$summaryScopeText = if ($isAllUsersScope) { 'all users' } else { 'the current user' }
return [PSCustomObject]@{
Scope = $scopeValue
Target = $scopeValue
SummaryText = $summaryScopeText
}
}
$showStartMenuIntroState = {
$backupFileText.Text = 'Not selected'
$backupCreatedText.Text = 'N/A'
$overviewSummaryText.Visibility = 'Collapsed'
$overviewPanel.Visibility = 'Collapsed'
$startMenuIntroPanel.Visibility = 'Visible'
$restoreModeTabs.SelectedIndex = 2
}
$showStartMenuOverviewState = {
param([string]$BackupFilePath)
$scopeInfo = & $getStartMenuScopeInfo
$backupTargetText.Text = GetFriendlyRegistryBackupTarget -Target $scopeInfo.Target
$overviewSummaryText.Text = "This will replace the current Start Menu pinned apps layout for $($scopeInfo.SummaryText) with the selected backup."
$backupFileText.Text = Split-Path -Path $BackupFilePath -Leaf
$createdText = 'Unknown'
try {
$createdText = (Get-Item -LiteralPath $BackupFilePath -ErrorAction Stop).LastWriteTime.ToString('yyyy-MM-dd HH:mm')
}
catch { }
$backupCreatedText.Text = $createdText
$overviewFeaturesSection.Visibility = 'Collapsed'
$overviewSummaryText.Visibility = 'Visible'
$nonRevertibleSeparator.Visibility = 'Collapsed'
$nonRevertiblePanel.Visibility = 'Collapsed'
$introInfoPanel.Visibility = 'Collapsed'
$overviewPanel.Visibility = 'Visible'
$restoreModeTabs.SelectedIndex = 1
}
$updateStartMenuOverviewPanel = {
if ($state.WizardStep -ne 'StartMenu') {
return
}
if ([string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)) {
& $showStartMenuIntroState
return
}
& $showStartMenuOverviewState $state.SelectedStartMenuBackupFilePath
}
$updateStartMenuPrimaryActionText = {
if ($state.WizardStep -ne 'StartMenu') {
return
}
$isAutoBackupEnabled = ($startMenuAutoBackupCheck.IsChecked -eq $true)
$hasSelectedManualFile = -not [string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)
if ($isAutoBackupEnabled -or $hasSelectedManualFile) {
$primaryActionBtn.Content = 'Restore backup'
}
else {
$primaryActionBtn.Content = 'Select backup file'
}
}
$refreshStartMenuUi = {
& $updateStartMenuOverviewPanel
& $updateStartMenuPrimaryActionText
}
$enterSelectTypeStep = {
$titleText.Text = 'Restore Backup'
$restoreModeTabs.SelectedIndex = 0
$backBtn.Visibility = 'Visible'
$backBtn.Content = 'Cancel'
$primaryActionBtn.Visibility = 'Collapsed'
$chooseRegistryBtn.IsDefault = $true
$primaryActionBtn.IsDefault = $false
}
$enterRegistryStep = {
$titleText.Text = 'Restore Registry Backup'
$restoreModeTabs.SelectedIndex = 1
$introInfoPanel.Visibility = 'Visible'
$overviewPanel.Visibility = 'Collapsed'
$overviewFeaturesSection.Visibility = 'Visible'
$overviewSummaryText.Visibility = 'Collapsed'
$backBtn.Visibility = 'Visible'
$backBtn.Content = 'Back'
$primaryActionBtn.Visibility = 'Visible'
$primaryActionBtn.Content = 'Select backup file'
$primaryActionBtn.IsDefault = $true
$chooseRegistryBtn.IsDefault = $false
}
$enterStartMenuStep = {
$titleText.Text = 'Restore Start Menu Backup'
$restoreModeTabs.SelectedIndex = 2
$backBtn.Visibility = 'Visible'
$backBtn.Content = 'Back'
$primaryActionBtn.Visibility = 'Visible'
$primaryActionBtn.IsDefault = $true
$chooseRegistryBtn.IsDefault = $false
& $refreshStartMenuUi
}
$showRegistryOverview = {
param(
[Parameter(Mandatory = $true)]
$SelectedBackup,
[Parameter(Mandatory = $true)]
[string]$SelectedBackupFilePath
)
$createdText = if ([string]::IsNullOrWhiteSpace($SelectedBackup.CreatedAt)) {
'Unknown'
}
else {
try {
[DateTime]::Parse($SelectedBackup.CreatedAt).ToString('yyyy-MM-dd HH:mm')
}
catch {
$SelectedBackup.CreatedAt
}
}
$selectedFeatureIds = Get-SelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup
$featureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds $selectedFeatureIds -Features $script:Features
$revertibleFeaturesList = @($featureLists.Revertible)
$nonRevertibleFeaturesList = @($featureLists.NonRevertible)
Write-Host "Backup overview prepared. Revertible=$($revertibleFeaturesList.Count), NonRevertible=$($nonRevertibleFeaturesList.Count)"
if ($revertibleFeaturesList.Count -eq 0) {
throw 'The selected backup does not contain any changes that can be restored.'
}
$backupFileText.Text = Split-Path $SelectedBackupFilePath -Leaf
$backupCreatedText.Text = $createdText
$backupTargetText.Text = GetFriendlyRegistryBackupTarget -Target ([string]$SelectedBackup.Target)
$featuresItemsControl.ItemsSource = $revertibleFeaturesList
$overviewFeaturesSection.Visibility = 'Visible'
$overviewSummaryText.Visibility = 'Collapsed'
$nonRevertibleFeaturesItemsControl.ItemsSource = $nonRevertibleFeaturesList
$hasNonRevertibleItems = ($nonRevertibleFeaturesList.Count -gt 0)
if ($hasNonRevertibleItems) { $nonRevertiblePanel.Visibility = 'Visible' } else { $nonRevertiblePanel.Visibility = 'Collapsed' }
if ($hasNonRevertibleItems) { $nonRevertibleSeparator.Visibility = 'Visible' } else { $nonRevertibleSeparator.Visibility = 'Collapsed' }
$introInfoPanel.Visibility = 'Collapsed'
$overviewPanel.Visibility = 'Visible'
return $true
}
$handleRegistryPrimaryAction = {
if ($state.SelectedRegistryBackup) {
$window.Tag = @{
Result = 'RestoreRegistry'
Backup = $state.SelectedRegistryBackup
}
$window.DialogResult = $true
$window.Close()
return
}
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
$openDialog.Title = 'Select Registry Backup File'
$openDialog.Filter = 'Registry backup (*.json)|*.json|All files (*.*)|*.*'
$openDialog.DefaultExt = '.json'
$openDialog.InitialDirectory = $script:RegistryBackupsPath
if ($openDialog.ShowDialog($window) -ne $true) {
return
}
Write-Host "Backup file selected: $($openDialog.FileName)"
$selectedBackup = Load-RegistryBackupFromFile -FilePath $openDialog.FileName
if (-not (& $showRegistryOverview -SelectedBackup $selectedBackup -SelectedBackupFilePath $openDialog.FileName)) {
return
}
$state.SelectedRegistryBackup = $selectedBackup
$primaryActionBtn.Content = 'Restore from backup'
}
$handleStartMenuPrimaryAction = {
$scope = (& $getStartMenuScopeInfo).Scope
$useManualBackupFile = -not ($startMenuAutoBackupCheck.IsChecked -eq $true)
if ($useManualBackupFile -and [string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)) {
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
$openDialog.Title = 'Select Start Menu Backup File'
$openDialog.Filter = 'Start Menu backup (*.bak)|*.bak'
$openDialog.InitialDirectory = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$openDialog.DefaultExt = '.bak'
if ($openDialog.ShowDialog($window) -ne $true) {
return
}
$state.SelectedStartMenuBackupFilePath = $openDialog.FileName
Write-Host "Selected Start Menu backup file: $($state.SelectedStartMenuBackupFilePath)"
& $refreshStartMenuUi
return
}
$window.Tag = @{
Result = 'RestoreStartMenu'
StartMenuScope = $scope
UseManualBackupFile = $useManualBackupFile
BackupFilePath = $state.SelectedStartMenuBackupFilePath
}
$window.DialogResult = $true
$window.Close()
}
$setWizardStep = {
param([string]$step)
$state.WizardStep = $step
switch ($step) {
'SelectType' { & $enterSelectTypeStep }
'Registry' { & $enterRegistryStep }
'StartMenu' { & $enterStartMenuStep }
}
}
$startMenuAutoBackupCheck.Add_Checked({
$state.SelectedStartMenuBackupFilePath = $null
& $refreshStartMenuUi
})
$startMenuAutoBackupCheck.Add_Unchecked({
& $refreshStartMenuUi
})
$startMenuScopeCombo.Add_SelectionChanged({
& $refreshStartMenuUi
})
$nonRevertibleWikiLink.Add_MouseLeftButtonUp({
try {
Start-Process 'https://github.com/Raphire/Win11Debloat/wiki/Reverting-Changes' | Out-Null
}
catch { }
})
$closeBtn.Add_Click({
$window.Tag = New-RestoreDialogState
$window.DialogResult = $false
$window.Close()
})
$chooseRegistryBtn.Add_Click({ & $setWizardStep 'Registry' })
$chooseStartMenuBtn.Add_Click({ & $setWizardStep 'StartMenu' })
$backBtn.Add_Click({
if ($state.WizardStep -eq 'SelectType') {
$window.Tag = New-RestoreDialogState
$window.DialogResult = $false
$window.Close()
return
}
if ($state.WizardStep -eq 'Registry') {
$state.SelectedRegistryBackup = $null
}
if ($state.WizardStep -eq 'StartMenu') {
$state.SelectedStartMenuBackupFilePath = $null
$startMenuAutoBackupCheck.IsChecked = $true
}
& $setWizardStep 'SelectType'
})
$primaryActionBtn.Add_Click({
switch ($state.WizardStep) {
'Registry' { & $handleRegistryPrimaryAction }
'StartMenu' { & $handleStartMenuPrimaryAction }
}
})
$window.Add_KeyDown({
param($source, $e)
if ($e.Key -eq 'Escape') {
$window.Tag = New-RestoreDialogState
$window.DialogResult = $false
$window.Close()
}
})
& $setWizardStep 'SelectType'
try {
$null = $window.ShowDialog()
}
catch {
$innerMessage = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { 'None' }
throw "Failed to show restore backup dialog. Error: $($_.Exception.Message) Inner: $innerMessage"
}
finally {
if ($overlay -and -not $overlayWasAlreadyVisible) {
try {
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' })
}
catch { }
}
}
return $window.Tag
}

View File

@@ -0,0 +1,88 @@
function Show-RestoreBackupWindow {
param(
[Parameter(Mandatory = $false)]
[System.Windows.Window]$Owner = $null
)
try {
Write-Host 'Opening restore backup dialog.'
$dialogResult = Show-RestoreBackupDialog -Owner $Owner
if (-not $dialogResult -or $dialogResult.Result -eq 'Cancel') {
Write-Host 'Restore canceled by user.'
return
}
$successMessage = $null
$warningMessage = $null
if ($dialogResult.Result -eq 'RestoreRegistry') {
$backup = $dialogResult.Backup
if (-not $backup) {
throw 'Registry backup restore requested without a selected backup.'
}
Write-Host "User confirmed registry restore for $($backup.Target)."
Restore-RegistryBackupState -Backup $backup
$successMessage = 'Registry backup restored successfully.'
}
elseif ($dialogResult.Result -eq 'RestoreStartMenu') {
$scope = $dialogResult.StartMenuScope
$useManualBackupFile = ($dialogResult.UseManualBackupFile -eq $true)
$backupFilePath = $null
if ($dialogResult -is [hashtable] -and $dialogResult.ContainsKey('BackupFilePath')) {
$backupFilePath = $dialogResult['BackupFilePath']
}
elseif ($dialogResult.PSObject.Properties.Match('BackupFilePath').Count -gt 0) {
$backupFilePath = $dialogResult.BackupFilePath
}
if ($useManualBackupFile -and [string]::IsNullOrWhiteSpace($backupFilePath)) {
throw 'Start Menu restore canceled: no backup file selected.'
}
$result = if ($scope -eq 'AllUsers') {
RestoreStartMenuForAllUsers -BackupFilePath $backupFilePath
}
else {
RestoreStartMenu -BackupFilePath $backupFilePath
}
$resultEntries = @($result)
$successCount = @($resultEntries | Where-Object { $_.Result -eq $true }).Count
$failedEntries = @($resultEntries | Where-Object { $_.Result -ne $true })
if ($successCount -eq 0) {
$errorSummary = ($resultEntries | ForEach-Object { $_.Message }) -join [Environment]::NewLine
throw "Failed to restore the Start Menu backup.`n$errorSummary"
}
if ($failedEntries.Count -gt 0) {
$failureSummary = ($failedEntries | ForEach-Object { $_.Message }) -join [Environment]::NewLine
$warningMessage = "The Start Menu backup was successfully restored for $successCount user(s).`nSome users could not be restored:`n$failureSummary"
}
else {
if ($scope -eq 'AllUsers') {
$successMessage = "The Start Menu backup was successfully restored for all users. The changes will apply the next time users sign in."
}
else {
$successMessage = "The Start Menu backup was successfully restored for the current user. The changes will apply the next time you sign in."
}
}
}
if ($warningMessage) {
Write-Host "$warningMessage"
Show-MessageBox -Title 'Backup Restored' -Message $warningMessage -Icon Warning
}
elseif ($successMessage) {
Write-Host "$successMessage"
Show-MessageBox -Title 'Backup Restored' -Message $successMessage -Icon Success
}
}
catch {
$errorMessage = if ($_.Exception.Message) { $_.Exception.Message } else { 'An unexpected error occurred.' }
Write-Error "Restore operation failed: $errorMessage"
Show-MessageBox -Title 'Error' -Message "Restore failed: $errorMessage" -Icon Error
}
}