Add ability to export/import settings configuration (#522)

This commit is contained in:
Jeffrey
2026-03-27 20:33:24 +01:00
committed by GitHub
parent e05af92acc
commit 774c8ecd92
15 changed files with 726 additions and 31 deletions

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
}
}