mirror of
https://github.com/Raphire/Win11Debloat.git
synced 2026-10-08 06:26:45 +00:00
Add localization framework for the GUI (en-US baseline) (#764)
Credit to @jmolinap2 for their work in #643, which was used as inspiration.
This commit is contained in:
@@ -50,7 +50,7 @@ function Remove-SelectedApps {
|
||||
$appIndex++
|
||||
|
||||
if ($script:ApplySubStepCallback -and $appCount -gt 1) {
|
||||
& $script:ApplySubStepCallback "Removing apps ($appIndex/$appCount)" $appIndex $appCount
|
||||
& $script:ApplySubStepCallback (Get-Translation -Key 'RemovingAppsSubStep' -FormatArgs @($appIndex, $appCount)) $appIndex $appCount
|
||||
}
|
||||
|
||||
Write-Host "Removing $app"
|
||||
@@ -329,7 +329,7 @@ function Get-AppRemovalMethod {
|
||||
#>
|
||||
function Request-EdgeForceRemove {
|
||||
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'
|
||||
$result = Show-MessageBox -Message (Get-Translation -Key 'ForceRemoveEdgeMessage') -Title (Get-Translation -Key 'ForceRemoveEdgeTitle') -Button 'YesNo' -Icon 'Warning'
|
||||
if ($result -eq 'Yes') {
|
||||
Write-Host ""
|
||||
return (Invoke-ForceRemoveEdge)
|
||||
|
||||
@@ -265,7 +265,7 @@ function Invoke-ApplyFeatures {
|
||||
|
||||
# Resolve display name for the progress indicator
|
||||
$f = $script:Features[$featureId]
|
||||
$displayName = $f.ApplyText
|
||||
$displayName = Get-Translation -Key $featureId -Field 'ApplyText' -Section 'Features'
|
||||
|
||||
if ($script:ApplyProgressCallback) {
|
||||
& $script:ApplyProgressCallback $step $TotalSteps $displayName
|
||||
@@ -309,8 +309,8 @@ function Invoke-UndoFeatures {
|
||||
if ($script:CancelRequested) { return }
|
||||
|
||||
$f = if ($script:Features.ContainsKey($featureId)) { $script:Features[$featureId] } else { $null }
|
||||
$undoLabel = if ($f -and $f.UndoLabel) { $f.UndoLabel } else { $featureId }
|
||||
$undoText = if ($f -and $f.ApplyUndoText) { $f.ApplyUndoText } else { $undoLabel }
|
||||
$undoLabel = if ($f -and $f.UndoLabel) { Get-Translation -Key $featureId -Field 'UndoLabel' -Section 'Features' } else { $featureId }
|
||||
$undoText = if ($f -and $f.ApplyUndoText) { Get-Translation -Key $featureId -Field 'ApplyUndoText' -Section 'Features' } else { $undoLabel }
|
||||
|
||||
if ($script:ApplyProgressCallback) {
|
||||
& $script:ApplyProgressCallback $step $TotalSteps $undoText
|
||||
@@ -391,7 +391,7 @@ function Invoke-AllChanges {
|
||||
if ($script:CancelRequested) { return }
|
||||
$step++
|
||||
if ($script:ApplyProgressCallback) {
|
||||
& $script:ApplyProgressCallback $step $totalSteps "Creating registry backup..."
|
||||
& $script:ApplyProgressCallback $step $totalSteps (Get-Translation -Key 'ApplyCreatingRegistryBackup')
|
||||
}
|
||||
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
@@ -421,7 +421,7 @@ function Invoke-AllChanges {
|
||||
if ($script:CancelRequested) { return }
|
||||
$step++
|
||||
if ($script:ApplyProgressCallback) {
|
||||
& $script:ApplyProgressCallback $step $totalSteps "Creating system restore point, this may take a moment..."
|
||||
& $script:ApplyProgressCallback $step $totalSteps (Get-Translation -Key 'ApplyCreatingRestorePoint')
|
||||
}
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Write-Host "[WhatIf] Create system restore point" -ForegroundColor Cyan
|
||||
|
||||
@@ -89,7 +89,7 @@ function Invoke-SystemRestorePoint {
|
||||
# Ensure that the user is aware if creating a restore point failed, and give them the option to continue without a restore point or cancel the script
|
||||
if ($failed) {
|
||||
if ($script:GuiWindow) {
|
||||
$result = Show-MessageBox "Failed to create a system restore point. Do you want to continue without a restore point?" "Restore Point Creation Failed" "YesNo" "Warning"
|
||||
$result = Show-MessageBox (Get-Translation -Key 'RestorePointFailedMessage') (Get-Translation -Key 'RestorePointFailedTitle') "YesNo" "Warning"
|
||||
|
||||
if ($result -ne "Yes") {
|
||||
$script:CancelRequested = $true
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Extracts and deduplicates the SelectedFeatures list from a backup, validating each entry.
|
||||
|
||||
.DESCRIPTION
|
||||
Returns an empty SelectedFeatures array with an error when the property is missing
|
||||
entirely, and flags (without stopping) any entry that isn't a non-empty string.
|
||||
#>
|
||||
function Get-NormalizedSelectedFeatureIdsFromBackup {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -10,7 +18,7 @@ function Get-NormalizedSelectedFeatureIdsFromBackup {
|
||||
$hasInvalidSelectedFeatureId = $false
|
||||
|
||||
if (-not $Backup.PSObject.Properties['SelectedFeatures']) {
|
||||
$errors.Add('Missing property: SelectedFeatures')
|
||||
$errors.Add((Get-Translation -Key 'BackupMissingProperty' -FormatArgs @('SelectedFeatures')))
|
||||
return [PSCustomObject]@{
|
||||
SelectedFeatures = $selectedFeatures.ToArray()
|
||||
Errors = $errors.ToArray()
|
||||
@@ -30,7 +38,7 @@ function Get-NormalizedSelectedFeatureIdsFromBackup {
|
||||
}
|
||||
|
||||
if ($hasInvalidSelectedFeatureId) {
|
||||
$errors.Add('SelectedFeatures must contain non-empty string feature IDs.')
|
||||
$errors.Add((Get-Translation -Key 'BackupSelectedFeaturesMustBeStrings'))
|
||||
}
|
||||
|
||||
return [PSCustomObject]@{
|
||||
@@ -39,6 +47,14 @@ function Get-NormalizedSelectedFeatureIdsFromBackup {
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Extracts and deduplicates the SelectedUndoFeatures list from a backup, validating each entry.
|
||||
|
||||
.DESCRIPTION
|
||||
Unlike SelectedFeatures, this property is optional: a missing property returns an
|
||||
empty array with no error, since not every backup has undone features to restore.
|
||||
#>
|
||||
function Get-NormalizedSelectedUndoFeatureIdsFromBackup {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -71,7 +87,7 @@ function Get-NormalizedSelectedUndoFeatureIdsFromBackup {
|
||||
}
|
||||
|
||||
if ($hasInvalidSelectedUndoFeatureId) {
|
||||
$errors.Add('SelectedUndoFeatures must contain non-empty string feature IDs.')
|
||||
$errors.Add((Get-Translation -Key 'BackupSelectedUndoFeaturesMustBeStrings'))
|
||||
}
|
||||
|
||||
return [PSCustomObject]@{
|
||||
@@ -80,6 +96,15 @@ function Get-NormalizedSelectedUndoFeatureIdsFromBackup {
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Normalizes a raw registry key snapshot into a consistent shape, recursing into sub-keys.
|
||||
|
||||
.DESCRIPTION
|
||||
Fills in defaults for optional properties (Exists, Values, SubKeys) so downstream
|
||||
validation can read a predictable shape regardless of what the backup file omitted.
|
||||
Throws if Path is missing, since every snapshot must be anchored to a real key.
|
||||
#>
|
||||
function Normalize-RegistryKeySnapshot {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -87,7 +112,7 @@ function Normalize-RegistryKeySnapshot {
|
||||
)
|
||||
|
||||
if (-not $Snapshot.PSObject.Properties['Path'] -or [string]::IsNullOrWhiteSpace([string]$Snapshot.Path)) {
|
||||
throw 'Backup validation failed: Registry key snapshot is missing Path.'
|
||||
throw (Get-Translation -Key 'BackupSnapshotMissingPath')
|
||||
}
|
||||
|
||||
$exists = $false
|
||||
@@ -127,6 +152,16 @@ function Normalize-RegistryKeySnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Validates that a backup's registry snapshots only contain paths and values the
|
||||
selected (and undo) features are actually allowed to touch.
|
||||
|
||||
.DESCRIPTION
|
||||
Builds an allow-list from the capture plans of the selected features, then checks
|
||||
every snapshot against it. Returns an array of translated error strings; an empty
|
||||
array means the backup is valid.
|
||||
#>
|
||||
function Test-RegistryBackupMatchesSelectedFeatures {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -145,7 +180,7 @@ function Test-RegistryBackupMatchesSelectedFeatures {
|
||||
$errors = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
if (-not $script:Features -or $script:Features.Count -eq 0) {
|
||||
$errors.Add('Unable to validate registry backup allowlist because feature definitions are not loaded.')
|
||||
$errors.Add((Get-Translation -Key 'BackupFeatureCatalogNotLoaded'))
|
||||
return $errors.ToArray()
|
||||
}
|
||||
|
||||
@@ -161,7 +196,7 @@ function Test-RegistryBackupMatchesSelectedFeatures {
|
||||
$planMap = New-RegistryBackupAllowListPlanMap -CapturePlans @($capturePlans)
|
||||
|
||||
if ($planMap.Count -eq 0 -and @($RegistryKeys).Count -gt 0) {
|
||||
$errors.Add('Backup contains registry snapshots but no allowed registry paths were derived from the selected features.')
|
||||
$errors.Add((Get-Translation -Key 'BackupNoAllowedRegistryPaths'))
|
||||
}
|
||||
|
||||
foreach ($rootSnapshot in @($RegistryKeys)) {
|
||||
@@ -171,6 +206,15 @@ function Test-RegistryBackupMatchesSelectedFeatures {
|
||||
return $errors.ToArray()
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resolves selected feature IDs to their catalog entries, keeping only the ones with a
|
||||
registry-backed apply or undo key relevant to the requested direction.
|
||||
|
||||
.DESCRIPTION
|
||||
Appends a translated error to the caller-supplied Errors list for any feature ID that
|
||||
isn't in the current catalog, rather than failing the whole validation outright.
|
||||
#>
|
||||
function Get-SelectedRegistryFeaturesForBackupValidation {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -186,11 +230,13 @@ function Get-SelectedRegistryFeaturesForBackupValidation {
|
||||
if ($null -eq $Errors -or -not ($Errors -is [System.Collections.IList])) {
|
||||
throw 'Get-SelectedRegistryFeaturesForBackupValidation requires Errors to be a mutable list collection.'
|
||||
}
|
||||
# Intentionally not localized: this throw signals a programming error (a caller passing the
|
||||
# wrong collection type), not a condition a user's backup file can trigger.
|
||||
|
||||
$selectedRegistryFeatures = New-Object System.Collections.Generic.List[object]
|
||||
foreach ($featureId in @($SelectedFeatureIds)) {
|
||||
if (-not $script:Features.ContainsKey($featureId)) {
|
||||
$Errors.Add("Selected feature '$featureId' was not found in the current feature catalog.")
|
||||
$Errors.Add((Get-Translation -Key 'BackupFeatureNotInCatalog' -FormatArgs @($featureId)))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -293,13 +339,13 @@ function Test-RegistrySnapshotAgainstAllowList {
|
||||
$snapshotPath = [string]$Snapshot.Path
|
||||
$normalizedPath = Get-NormalizedRegistryPathKey -Path $snapshotPath
|
||||
if ([string]::IsNullOrWhiteSpace($normalizedPath)) {
|
||||
$Errors.Add("Backup contains unsupported registry path '$snapshotPath'.")
|
||||
$Errors.Add((Get-Translation -Key 'BackupUnsupportedRegistryPath' -FormatArgs @($snapshotPath)))
|
||||
return
|
||||
}
|
||||
|
||||
$planMatch = Find-RegistryAllowListPlanMatch -NormalizedPath $normalizedPath -PlanMap $PlanMap
|
||||
if ($null -eq $planMatch) {
|
||||
$Errors.Add("Backup contains unexpected registry path '$snapshotPath' that is not allowed by SelectedFeatures.")
|
||||
$Errors.Add((Get-Translation -Key 'BackupUnexpectedRegistryPath' -FormatArgs @($snapshotPath)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -308,21 +354,21 @@ function Test-RegistrySnapshotAgainstAllowList {
|
||||
$valueExists = [bool]$valueSnapshot.Exists
|
||||
|
||||
if (-not (Test-RegistryValueAllowedByPlan -PlanMatch $planMatch -ValueName $valueName)) {
|
||||
$Errors.Add("Backup contains unexpected value '$valueName' under '$snapshotPath'.")
|
||||
$Errors.Add((Get-Translation -Key 'BackupUnexpectedValue' -FormatArgs @($valueName, $snapshotPath)))
|
||||
}
|
||||
|
||||
$kindName = if ($valueSnapshot.PSObject.Properties['Kind']) { [string]$valueSnapshot.Kind } else { '' }
|
||||
$valueReference = Get-RegistryValueReferenceForError -SnapshotPath $snapshotPath -ValueName $valueName
|
||||
if ($valueExists) {
|
||||
if (-not (Test-RegistryValueKindNameSupported -KindName $kindName)) {
|
||||
$Errors.Add("Backup contains unsupported registry value kind '$kindName' for '$valueReference'.")
|
||||
$Errors.Add((Get-Translation -Key 'BackupUnsupportedValueKind' -FormatArgs @($kindName, $valueReference)))
|
||||
}
|
||||
elseif (-not (Test-RegistryValueDataMatchesKind -KindName $kindName -Data $valueSnapshot.Data)) {
|
||||
$Errors.Add("Backup contains invalid registry data for kind '$kindName' at '$valueReference'.")
|
||||
$Errors.Add((Get-Translation -Key 'BackupInvalidValueData' -FormatArgs @($kindName, $valueReference)))
|
||||
}
|
||||
}
|
||||
elseif (-not [string]::IsNullOrWhiteSpace($kindName)) {
|
||||
$Errors.Add("Backup value '$valueReference' must not define Kind when Exists is false.")
|
||||
$Errors.Add((Get-Translation -Key 'BackupValueMustNotDefineKind' -FormatArgs @($valueReference)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -319,7 +319,7 @@ function Restore-StartMenuFromBackup {
|
||||
return [PSCustomObject]@{
|
||||
UserName = $userName
|
||||
Result = $false
|
||||
Message = "No start menu backup file found for user $userName."
|
||||
Message = Get-Translation -Key 'StartMenuNoBackupFoundForUser' -FormatArgs @($userName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +328,7 @@ function Restore-StartMenuFromBackup {
|
||||
return [PSCustomObject]@{
|
||||
UserName = $userName
|
||||
Result = $true
|
||||
Message = "[WhatIf] Restored start menu for user $userName."
|
||||
Message = Get-Translation -Key 'StartMenuWhatIfRestoredForUser' -FormatArgs @($userName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,7 +336,7 @@ function Restore-StartMenuFromBackup {
|
||||
return [PSCustomObject]@{
|
||||
UserName = $userName
|
||||
Result = $false
|
||||
Message = "No start menu backup file found for user $userName."
|
||||
Message = Get-Translation -Key 'StartMenuNoBackupFoundForUser' -FormatArgs @($userName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,14 +349,14 @@ function Restore-StartMenuFromBackup {
|
||||
return [PSCustomObject]@{
|
||||
UserName = $userName
|
||||
Result = $true
|
||||
Message = "Restored start menu for user $userName."
|
||||
Message = Get-Translation -Key 'StartMenuRestoredForUser' -FormatArgs @($userName)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
return [PSCustomObject]@{
|
||||
UserName = $userName
|
||||
Result = $false
|
||||
Message = "Failed to restore start menu for user $userName. $($_.Exception.Message)"
|
||||
Message = Get-Translation -Key 'StartMenuRestoreFailedForUser' -FormatArgs @($userName, $_.Exception.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -440,7 +440,7 @@ function Restore-StartMenuForAllUsers {
|
||||
$results += [PSCustomObject]@{
|
||||
UserName = 'Default'
|
||||
Result = $true
|
||||
Message = '[WhatIf] Removed start2.bin for the default user profile.'
|
||||
Message = Get-Translation -Key 'StartMenuWhatIfRemovedDefaultProfile'
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -449,14 +449,14 @@ function Restore-StartMenuForAllUsers {
|
||||
$results += [PSCustomObject]@{
|
||||
UserName = 'Default'
|
||||
Result = $true
|
||||
Message = 'Removed start2.bin for the default user profile.'
|
||||
Message = Get-Translation -Key 'StartMenuRemovedDefaultProfile'
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$results += [PSCustomObject]@{
|
||||
UserName = 'Default'
|
||||
Result = $false
|
||||
Message = "Failed to remove start2.bin for the default user profile. $($_.Exception.Message)"
|
||||
Message = Get-Translation -Key 'StartMenuRemoveFailedDefaultProfile' -FormatArgs @($_.Exception.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -467,7 +467,7 @@ function Restore-StartMenuForAllUsers {
|
||||
$results += [PSCustomObject]@{
|
||||
UserName = 'unknown'
|
||||
Result = $false
|
||||
Message = 'No user start menu locations were found.'
|
||||
Message = Get-Translation -Key 'StartMenuNoUserLocationsFound'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,14 +21,14 @@ function Import-RegistryBackup {
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $FilePath)) {
|
||||
throw "Backup file was not found: $FilePath"
|
||||
throw (Get-Translation -Key 'BackupFileNotFound' -FormatArgs @($FilePath))
|
||||
}
|
||||
|
||||
try {
|
||||
$rawBackup = Get-Content -LiteralPath $FilePath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
throw "Failed to read backup file '$FilePath'. The file is not valid JSON."
|
||||
throw (Get-Translation -Key 'BackupFileNotValidJson' -FormatArgs @($FilePath))
|
||||
}
|
||||
|
||||
return ConvertTo-NormalizedRegistryBackup -Backup $rawBackup
|
||||
@@ -61,22 +61,22 @@ function ConvertTo-NormalizedRegistryBackup {
|
||||
$errors = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
if (-not $Backup.PSObject.Properties['Version']) {
|
||||
$errors.Add('Missing property: Version')
|
||||
$errors.Add((Get-Translation -Key 'BackupMissingProperty' -FormatArgs @('Version')))
|
||||
}
|
||||
elseif ([string]$Backup.Version -ne '1.0') {
|
||||
$errors.Add("Unsupported backup version '$($Backup.Version)'.")
|
||||
$errors.Add((Get-Translation -Key 'BackupUnsupportedVersion' -FormatArgs @($Backup.Version)))
|
||||
}
|
||||
|
||||
if (-not $Backup.PSObject.Properties['BackupType']) {
|
||||
$errors.Add('Missing property: BackupType')
|
||||
$errors.Add((Get-Translation -Key 'BackupMissingProperty' -FormatArgs @('BackupType')))
|
||||
}
|
||||
elseif ([string]$Backup.BackupType -ne 'RegistryState') {
|
||||
$errors.Add("Unsupported BackupType '$($Backup.BackupType)'.")
|
||||
$errors.Add((Get-Translation -Key 'BackupUnsupportedType' -FormatArgs @($Backup.BackupType)))
|
||||
}
|
||||
|
||||
$normalizedTarget = ''
|
||||
if (-not $Backup.PSObject.Properties['Target'] -or [string]::IsNullOrWhiteSpace([string]$Backup.Target)) {
|
||||
$errors.Add('Missing property: Target')
|
||||
$errors.Add((Get-Translation -Key 'BackupMissingProperty' -FormatArgs @('Target')))
|
||||
}
|
||||
else {
|
||||
$normalizedTarget = [string]$Backup.Target
|
||||
@@ -88,27 +88,27 @@ function ConvertTo-NormalizedRegistryBackup {
|
||||
$targetUserName = $normalizedTarget.Substring(5)
|
||||
$targetValidation = Test-TargetUserName -UserName $targetUserName
|
||||
if (-not $targetValidation.IsValid) {
|
||||
$errors.Add("Invalid user '$normalizedTarget'")
|
||||
$errors.Add((Get-Translation -Key 'BackupInvalidUser' -FormatArgs @($normalizedTarget)))
|
||||
}
|
||||
}
|
||||
elseif ($normalizedTarget -like 'CurrentUser:*') {
|
||||
$targetCurrentUserName = $normalizedTarget.Substring(12)
|
||||
if (Test-RunningAsSystem) {
|
||||
$errors.Add("Backup was made for '$targetCurrentUserName' and is user-scoped. Re-run as that user; SYSTEM cannot restore a CurrentUser backup.")
|
||||
$errors.Add((Get-Translation -Key 'BackupUserScopedWrongContext' -FormatArgs @($targetCurrentUserName)))
|
||||
}
|
||||
elseif ([string]::IsNullOrWhiteSpace($targetCurrentUserName) -or
|
||||
-not (Test-UserNameMatch -UserNameA $targetCurrentUserName -UserNameB $env:USERNAME)) {
|
||||
$errors.Add("Backup was made for '$targetCurrentUserName', this does not match current user '$env:USERNAME'.")
|
||||
$errors.Add((Get-Translation -Key 'BackupUserMismatch' -FormatArgs @($targetCurrentUserName, $env:USERNAME)))
|
||||
}
|
||||
}
|
||||
else {
|
||||
$errors.Add("Unsupported Target '$normalizedTarget'.")
|
||||
$errors.Add((Get-Translation -Key 'BackupUnsupportedTarget' -FormatArgs @($normalizedTarget)))
|
||||
}
|
||||
}
|
||||
|
||||
$registryKeys = @()
|
||||
if (-not $Backup.PSObject.Properties['RegistryKeys']) {
|
||||
$errors.Add('Missing property: RegistryKeys')
|
||||
$errors.Add((Get-Translation -Key 'BackupMissingProperty' -FormatArgs @('RegistryKeys')))
|
||||
}
|
||||
else {
|
||||
$registryKeys = @($Backup.RegistryKeys)
|
||||
@@ -133,7 +133,7 @@ function ConvertTo-NormalizedRegistryBackup {
|
||||
|
||||
$allSelectedFeatures = @($selectedFeatures) + @($selectedUndoFeatures)
|
||||
if ($allSelectedFeatures.Count -eq 0) {
|
||||
$errors.Add('Backup must contain at least one feature ID in SelectedFeatures or SelectedUndoFeatures.')
|
||||
$errors.Add((Get-Translation -Key 'BackupMissingSelectedFeatures'))
|
||||
}
|
||||
else {
|
||||
try {
|
||||
@@ -143,17 +143,17 @@ function ConvertTo-NormalizedRegistryBackup {
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$errors.Add("Failed to validate backup: $($_.Exception.Message)")
|
||||
$errors.Add((Get-Translation -Key 'BackupAllowlistValidationFailed' -FormatArgs @($_.Exception.Message)))
|
||||
}
|
||||
}
|
||||
|
||||
if ($errors.Count -gt 0) {
|
||||
Write-Error "Backup validation failed: $($errors -join ' ')"
|
||||
if ($errors.Count -eq 1) {
|
||||
throw ("Validation failed: $($errors[0])")
|
||||
throw (Get-Translation -Key 'BackupValidationSingleError' -FormatArgs @($errors[0]))
|
||||
}
|
||||
else {
|
||||
throw ("Validation failed with $($errors.Count) errors. See console output for details.")
|
||||
throw (Get-Translation -Key 'BackupValidationMultipleErrors' -FormatArgs @($errors.Count))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ function Import-JsonFile {
|
||||
}
|
||||
|
||||
try {
|
||||
$jsonContent = Get-Content -Path $filePath -Raw | ConvertFrom-Json
|
||||
$jsonContent = Get-Content -Path $filePath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
|
||||
# Validate version if specified
|
||||
if ($expectedVersion -and $jsonContent.Version -and $jsonContent.Version -ne $expectedVersion) {
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resolves a language code to an available Config/Languages folder, or falls back to en-US.
|
||||
#>
|
||||
function Resolve-LanguageFolder {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$LanguageCode,
|
||||
[string]$LanguagesPath = $script:LanguagesPath
|
||||
)
|
||||
|
||||
$exactPath = Join-Path $LanguagesPath $LanguageCode
|
||||
if (Test-Path $exactPath -PathType Container) {
|
||||
return $LanguageCode
|
||||
}
|
||||
|
||||
$languagePrefix = ($LanguageCode -split '-')[0]
|
||||
$prefixMatch = Get-ChildItem -Path $LanguagesPath -Directory -Filter "$languagePrefix-*" -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if ($prefixMatch) {
|
||||
return $prefixMatch.Name
|
||||
}
|
||||
|
||||
return 'en-US'
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Loads Chrome/Features/Categories JSON for a language folder, or returns $null on failure.
|
||||
#>
|
||||
function Import-LanguageContent {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$LanguageFolder,
|
||||
[string]$LanguagesPath = $script:LanguagesPath
|
||||
)
|
||||
|
||||
$folderPath = Join-Path $LanguagesPath $LanguageFolder
|
||||
$chrome = Import-JsonFile -filePath (Join-Path $folderPath 'Chrome.json')
|
||||
$features = Import-JsonFile -filePath (Join-Path $folderPath 'Features.json')
|
||||
$categories = Import-JsonFile -filePath (Join-Path $folderPath 'Categories.json')
|
||||
|
||||
if (-not $chrome -or -not $features -or -not $categories) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return [PSCustomObject]@{
|
||||
LanguageCode = $LanguageFolder
|
||||
Chrome = $chrome
|
||||
Features = $features.Features
|
||||
UiGroups = $features.UiGroups
|
||||
Categories = $categories
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Loads the active language content, falling back to en-US when the requested language fails to load.
|
||||
#>
|
||||
function Import-LanguageFile {
|
||||
param(
|
||||
[string]$LanguageCode = ([System.Globalization.CultureInfo]::CurrentUICulture.Name),
|
||||
[string]$LanguagesPath = $script:LanguagesPath
|
||||
)
|
||||
|
||||
$resolvedFolder = Resolve-LanguageFolder -LanguageCode $LanguageCode -LanguagesPath $LanguagesPath
|
||||
$content = Import-LanguageContent -LanguageFolder $resolvedFolder -LanguagesPath $LanguagesPath
|
||||
|
||||
if (-not $content -and $resolvedFolder -ne 'en-US') {
|
||||
Write-Warning "Failed to load language '$resolvedFolder', falling back to en-US."
|
||||
$content = Import-LanguageContent -LanguageFolder 'en-US' -LanguagesPath $LanguagesPath
|
||||
}
|
||||
|
||||
if (-not $content) {
|
||||
Write-Error "Unable to load the en-US language files. The GUI cannot continue without them."
|
||||
return $null
|
||||
}
|
||||
|
||||
if ($content.LanguageCode -ne 'en-US') {
|
||||
$content | Add-Member -MemberType NoteProperty -Name 'Fallback' -Value (Import-LanguageContent -LanguageFolder 'en-US' -LanguagesPath $LanguagesPath)
|
||||
}
|
||||
|
||||
return $content
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Returns the CLDR plural category ('one', 'other', etc.) for a count in the given language.
|
||||
|
||||
.DESCRIPTION
|
||||
Each language's CLDR plural rule is its own case, keyed by language prefix ('en' matches
|
||||
both 'en-US' and 'en-GB'). A language without a case falls back to the English rule, which
|
||||
is also correct for Dutch and German but wrong for languages with 3+ plural categories or
|
||||
a different singular/plural split.
|
||||
#>
|
||||
function Get-PluralCategory {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$LanguageCode,
|
||||
[Parameter(Mandatory)]
|
||||
[int]$Count
|
||||
)
|
||||
|
||||
$languagePrefix = ($LanguageCode -split '-')[0].ToLowerInvariant()
|
||||
|
||||
switch ($languagePrefix) {
|
||||
# English CLDR rule: singular only for exactly 1, plural otherwise. Also correct for Dutch/German.
|
||||
default {
|
||||
if ($Count -eq 1) {
|
||||
return 'one'
|
||||
}
|
||||
|
||||
return 'other'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Returns a language and its Fallback (if any) as an ordered lookup chain, skipping nulls.
|
||||
#>
|
||||
function Get-LanguageFallbackChain {
|
||||
param(
|
||||
[object]$Lang
|
||||
)
|
||||
|
||||
return @($Lang, $Lang.Fallback) | Where-Object { $_ }
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Finds the named section object (Chrome, Features, UiGroups, or Categories) that owns a key.
|
||||
|
||||
.DESCRIPTION
|
||||
A FeatureId, GroupId, and CategoryId aren't guaranteed to be disjoint (Config/Features.json
|
||||
can and does reuse the same string as both a GroupId and a FeatureId), so the default search
|
||||
order below is only a fallback for callers who don't know which section their key belongs to.
|
||||
Pass -Section to look in exactly one section instead of guessing from the order.
|
||||
#>
|
||||
function Find-TranslationSection {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[object]$Lang,
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Key,
|
||||
[ValidateSet('', 'Chrome', 'Features', 'UiGroups', 'Categories')]
|
||||
[string]$Section = ''
|
||||
)
|
||||
|
||||
if ($Section) {
|
||||
$sectionObject = $Lang.$Section
|
||||
if ($sectionObject -and $sectionObject.PSObject.Properties[$Key]) {
|
||||
return $sectionObject
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
foreach ($sectionName in 'Chrome', 'Features', 'UiGroups', 'Categories') {
|
||||
$sectionObject = $Lang.$sectionName
|
||||
if ($sectionObject -and $sectionObject.PSObject.Properties[$Key]) {
|
||||
return $sectionObject
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Looks up a translated value from the active language, falling back to en-US, then to the key itself.
|
||||
|
||||
.DESCRIPTION
|
||||
Chrome.json keys are flat strings, so -Field is omitted for those. Features/UiGroups/Categories
|
||||
keys (FeatureId/GroupId/CategoryId) resolve to an object, so -Field picks the property on it
|
||||
(Label, ToolTip, ApplyText, UndoLabel, ApplyUndoText). One generic lookup covers every section
|
||||
instead of a separate function per section.
|
||||
|
||||
A FeatureId and a GroupId aren't guaranteed to be distinct strings, so pass -Section
|
||||
('Features', 'UiGroups', or 'Categories') whenever the caller knows which one it means,
|
||||
rather than relying on Find-TranslationSection's search order to guess correctly.
|
||||
|
||||
When -Count is supplied, tries the plural-suffixed key ("$Key`_$category") before the bare key,
|
||||
so callers don't need to add a plural variant for every string, only the ones that need one.
|
||||
.OUTPUTS
|
||||
System.String. The translated string, or the original Key if no language has it.
|
||||
#>
|
||||
function Get-Translation {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Key,
|
||||
[string]$Field = $null,
|
||||
[object]$Lang = $script:Lang,
|
||||
[Nullable[int]]$Count = $null,
|
||||
[object[]]$FormatArgs = $null,
|
||||
[ValidateSet('', 'Chrome', 'Features', 'UiGroups', 'Categories')]
|
||||
[string]$Section = ''
|
||||
)
|
||||
|
||||
if (-not $Lang) {
|
||||
return $Key
|
||||
}
|
||||
|
||||
$lookupKeys = @($Key)
|
||||
if ($null -ne $Count) {
|
||||
$category = Get-PluralCategory -LanguageCode $Lang.LanguageCode -Count $Count
|
||||
$lookupKeys = @("${Key}_$category", $Key)
|
||||
}
|
||||
|
||||
$resolved = $Key
|
||||
:langLoop foreach ($candidateLang in (Get-LanguageFallbackChain -Lang $Lang)) {
|
||||
foreach ($lookupKey in $lookupKeys) {
|
||||
$sectionObject = Find-TranslationSection -Lang $candidateLang -Key $lookupKey -Section $Section
|
||||
if (-not $sectionObject) { continue }
|
||||
|
||||
$entry = $sectionObject.$lookupKey
|
||||
if ($Field) {
|
||||
if ($entry.PSObject.Properties[$Field]) {
|
||||
$resolved = [string]$entry.$Field
|
||||
break langLoop
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
$resolved = [string]$entry
|
||||
break langLoop
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -ne $FormatArgs -and $FormatArgs.Count -gt 0) {
|
||||
try {
|
||||
return $resolved -f $FormatArgs
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Translation '$Key' has a placeholder mismatch with its format arguments: $($_.Exception.Message)"
|
||||
return $resolved
|
||||
}
|
||||
}
|
||||
|
||||
return $resolved
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Looks up a translated label for one value of a UiGroup, falling back to en-US, then to the raw label.
|
||||
|
||||
.DESCRIPTION
|
||||
UiGroups[GroupId].Values in the language files is a FeatureId -> Label map, one level deeper
|
||||
than Get-Translation's -Field lookup handles, so this has its own small fallback walk instead
|
||||
of overloading Get-Translation's flat-field shape.
|
||||
#>
|
||||
function Get-GroupValueTranslation {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$GroupId,
|
||||
[Parameter(Mandatory)]
|
||||
[string]$FeatureId,
|
||||
[Parameter(Mandatory)]
|
||||
[string]$FallbackLabel,
|
||||
[object]$Lang = $script:Lang
|
||||
)
|
||||
|
||||
if (-not $Lang) {
|
||||
return $FallbackLabel
|
||||
}
|
||||
|
||||
foreach ($candidateLang in (Get-LanguageFallbackChain -Lang $Lang)) {
|
||||
if (-not $candidateLang.UiGroups) { continue }
|
||||
|
||||
$group = $candidateLang.UiGroups.$GroupId
|
||||
if (-not $group -or -not $group.Values) { continue }
|
||||
|
||||
$valueEntry = $group.Values.PSObject.Properties[$FeatureId]
|
||||
if ($valueEntry) {
|
||||
return [string]$valueEntry.Value
|
||||
}
|
||||
}
|
||||
|
||||
return $FallbackLabel
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Flattens a loaded language object into a sorted set of dotted key paths.
|
||||
|
||||
.DESCRIPTION
|
||||
Chrome.json keys are already flat and are used as-is. Features/UiGroups/Categories
|
||||
keys are one level deeper (EntryId -> {Field: value}), so each field becomes its own
|
||||
"Section.EntryId.Field" path (e.g. "Features.DisableTelemetry.Label"). UiGroups' nested
|
||||
Values map becomes "UiGroups.GroupId.Values.FeatureId". Used by Test-LanguageKeyCoverage
|
||||
to diff one language's key set against another's.
|
||||
#>
|
||||
function Get-LanguageKeyPaths {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[object]$Lang
|
||||
)
|
||||
|
||||
$paths = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
foreach ($chromeKey in @($Lang.Chrome.PSObject.Properties.Name)) {
|
||||
if ([string]::IsNullOrEmpty($chromeKey)) { continue }
|
||||
$paths.Add($chromeKey)
|
||||
}
|
||||
|
||||
foreach ($sectionName in 'Features', 'Categories') {
|
||||
$section = $Lang.$sectionName
|
||||
if (-not $section) { continue }
|
||||
|
||||
foreach ($entryId in @($section.PSObject.Properties.Name)) {
|
||||
if ([string]::IsNullOrEmpty($entryId)) { continue }
|
||||
foreach ($field in @($section.$entryId.PSObject.Properties.Name)) {
|
||||
if ([string]::IsNullOrEmpty($field)) { continue }
|
||||
$paths.Add("$sectionName.$entryId.$field")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($groupId in @($Lang.UiGroups.PSObject.Properties.Name)) {
|
||||
if ([string]::IsNullOrEmpty($groupId)) { continue }
|
||||
$group = $Lang.UiGroups.$groupId
|
||||
foreach ($field in @($group.PSObject.Properties.Name)) {
|
||||
if ([string]::IsNullOrEmpty($field)) { continue }
|
||||
if ($field -eq 'Values') {
|
||||
foreach ($featureId in @($group.Values.PSObject.Properties.Name)) {
|
||||
if ([string]::IsNullOrEmpty($featureId)) { continue }
|
||||
$paths.Add("UiGroups.$groupId.Values.$featureId")
|
||||
}
|
||||
continue
|
||||
}
|
||||
$paths.Add("UiGroups.$groupId.$field")
|
||||
}
|
||||
}
|
||||
|
||||
return @($paths | Sort-Object -Unique)
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Diffs a language's key set against a baseline language, reporting missing and extra keys.
|
||||
|
||||
.DESCRIPTION
|
||||
Loads both languages fresh from disk (independent of any already-cached $script:Lang),
|
||||
flattens each with Get-LanguageKeyPaths, and compares the two sets. Intended to run
|
||||
against en-US as the baseline; useful today (catches a key present in one en-US file but
|
||||
not another) and becomes the real translation-completeness check once non-English
|
||||
language folders are contributed.
|
||||
|
||||
.OUTPUTS
|
||||
PSCustomObject with ResolvedLanguageCode (the folder actually compared, after the same
|
||||
exact/prefix/en-US resolution Import-LanguageFile applies), MissingKeys (present in
|
||||
Baseline, absent from LanguageCode), and ExtraKeys (present in LanguageCode, absent from
|
||||
Baseline) properties, the latter two sorted arrays.
|
||||
#>
|
||||
function Test-LanguageKeyCoverage {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$LanguageCode,
|
||||
[string]$BaselineLanguageCode = 'en-US',
|
||||
[string]$LanguagesPath = $script:LanguagesPath
|
||||
)
|
||||
|
||||
$resolvedLanguageCode = Resolve-LanguageFolder -LanguageCode $LanguageCode -LanguagesPath $LanguagesPath
|
||||
$baseline = Import-LanguageContent -LanguageFolder $BaselineLanguageCode -LanguagesPath $LanguagesPath
|
||||
$target = Import-LanguageContent -LanguageFolder $resolvedLanguageCode -LanguagesPath $LanguagesPath
|
||||
|
||||
if (-not $baseline -or -not $target) {
|
||||
Write-Error "Unable to load language content for coverage comparison ('$BaselineLanguageCode' vs '$resolvedLanguageCode')."
|
||||
return $null
|
||||
}
|
||||
|
||||
$baselineKeys = [System.Collections.Generic.HashSet[string]]::new([string[]](Get-LanguageKeyPaths -Lang $baseline))
|
||||
$targetKeys = [System.Collections.Generic.HashSet[string]]::new([string[]](Get-LanguageKeyPaths -Lang $target))
|
||||
|
||||
$missingKeys = @($baselineKeys | Where-Object { -not $targetKeys.Contains($_) } | Sort-Object)
|
||||
$extraKeys = @($targetKeys | Where-Object { -not $baselineKeys.Contains($_) } | Sort-Object)
|
||||
|
||||
return [PSCustomObject]@{
|
||||
ResolvedLanguageCode = $resolvedLanguageCode
|
||||
MissingKeys = $missingKeys
|
||||
ExtraKeys = $extraKeys
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Substitutes %LANG:Key% markers in XAML text with translated, XML-escaped values.
|
||||
|
||||
.DESCRIPTION
|
||||
Only resolves flat Chrome.json keys, since XAML markers never reference a Feature/Category/
|
||||
UiGroup field directly (those get their text from Get-Translation calls in the GUI scripts
|
||||
that build dynamic controls). Runs a single pass over every marker rather than sequential
|
||||
.Replace() calls, so an already-substituted value can't be re-matched by a later key.
|
||||
|
||||
After substitution, scans for any %LANG:...% text that survived unresolved and throws,
|
||||
since Get-Translation's key-as-fallback behavior means a missing key would otherwise render
|
||||
as plain, un-marked text (e.g. "TitleBarClose" instead of a visible error) rather than being
|
||||
caught here. Checks both the active language and its Fallback before flagging a key missing,
|
||||
so a partially-translated language degrades to en-US text instead of failing to load.
|
||||
#>
|
||||
function ConvertTo-LocalizedXaml {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Xaml,
|
||||
[object]$Lang = $script:Lang
|
||||
)
|
||||
|
||||
$missingKeys = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
$evaluator = [System.Text.RegularExpressions.MatchEvaluator]{
|
||||
param($match)
|
||||
$key = $match.Groups[1].Value
|
||||
$resolvesSomewhere = @(Get-LanguageFallbackChain -Lang $Lang) | Where-Object { $_.Chrome -and $_.Chrome.PSObject.Properties[$key] }
|
||||
if (-not $resolvesSomewhere) { $missingKeys.Add($key) }
|
||||
$value = if ($resolvesSomewhere) { Get-Translation -Key $key -Lang $Lang } else { $key }
|
||||
return [System.Security.SecurityElement]::Escape($value)
|
||||
}
|
||||
|
||||
$result = [System.Text.RegularExpressions.Regex]::Replace($Xaml, '%LANG:([A-Za-z0-9_]+)%', $evaluator)
|
||||
|
||||
if ($missingKeys.Count -gt 0) {
|
||||
throw "Unresolved localization marker(s) in XAML, key(s) not found in any language: $($missingKeys -join ', ')"
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
@@ -142,6 +142,15 @@ function Update-AppsPanelSort {
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Updates the "N apps selected" status text and shows or hides the app-removal scope section.
|
||||
|
||||
.DESCRIPTION
|
||||
The scope section only makes sense once at least one app is selected, so it's hidden
|
||||
entirely at zero and its description text is refreshed via Update-AppRemovalScopeDescription
|
||||
whenever the selection count changes.
|
||||
#>
|
||||
function Update-AppSelectionStatus {
|
||||
param(
|
||||
[System.Windows.Controls.Panel]$AppsPanel,
|
||||
@@ -158,7 +167,7 @@ function Update-AppSelectionStatus {
|
||||
$selectedCount++
|
||||
}
|
||||
}
|
||||
$AppSelectionStatus.Text = "$selectedCount app(s) selected for removal"
|
||||
$AppSelectionStatus.Text = Get-Translation -Key 'AppSelectionStatusSelected' -Count $selectedCount -FormatArgs @($selectedCount)
|
||||
|
||||
if ($AppRemovalScopeCombo -and $AppRemovalScopeSection -and $AppRemovalScopeDescription) {
|
||||
if ($selectedCount -gt 0) {
|
||||
@@ -189,13 +198,13 @@ function Update-AppRemovalScopeDescription {
|
||||
# Content is the display text and will change once translated; Name is stable.
|
||||
switch ($selectedItem.Name) {
|
||||
"AppRemovalScopeAllUsers" {
|
||||
$AppRemovalScopeDescription.Text = "Apps will be removed for all users and from the Windows image to prevent reinstallation for new users."
|
||||
$AppRemovalScopeDescription.Text = Get-Translation -Key 'AppRemovalScopeDescriptionAllUsers'
|
||||
}
|
||||
"AppRemovalScopeCurrentUser" {
|
||||
$AppRemovalScopeDescription.Text = "Apps will only be removed for the current user."
|
||||
$AppRemovalScopeDescription.Text = Get-Translation -Key 'AppRemovalScopeDescriptionCurrentUser'
|
||||
}
|
||||
"AppRemovalScopeTargetUser" {
|
||||
$AppRemovalScopeDescription.Text = "Apps will only be removed for the specified target user."
|
||||
$AppRemovalScopeDescription.Text = Get-Translation -Key 'AppRemovalScopeDescriptionTargetUser'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -448,9 +457,9 @@ function Add-AppsToMainWindow {
|
||||
$dot.Style = $Window.Resources['AppRecommendationDotStyle']
|
||||
$dot.Fill = switch ($app.Recommendation) { 'safe' { $brushSafe } 'unsafe' { $brushUnsafe } default { $brushDefault } }
|
||||
$dot.ToolTip = switch ($app.Recommendation) {
|
||||
'safe' { '[Recommended] Safe to remove for most users' }
|
||||
'unsafe' { '[Not Recommended] Only remove if you know what you are doing' }
|
||||
default { "[Optional] Can be safely removed if you don't need this app" }
|
||||
'safe' { Get-Translation -Key 'AppLegendRecommendedTooltip' }
|
||||
'unsafe' { Get-Translation -Key 'AppLegendNotRecommendedTooltip' }
|
||||
default { Get-Translation -Key 'AppLegendOptionalTooltip' }
|
||||
}
|
||||
[System.Windows.Controls.Grid]::SetColumn($dot, 0)
|
||||
|
||||
@@ -608,7 +617,7 @@ function Initialize-MainWindowApps {
|
||||
|
||||
if ($null -eq $listOfApps) {
|
||||
Write-Warning "WinGet returned no data (command timed out or failed)"
|
||||
Show-MessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null
|
||||
Show-MessageBox -Message (Get-Translation -Key 'AppSelectionWinGetLoadFailedMessage') -Title (Get-Translation -Key 'ErrorTitle') -Button 'OK' -Icon 'Error' | Out-Null
|
||||
$OnlyInstalledAppsBox.IsChecked = $false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,17 @@ function Get-PendingTweakActions {
|
||||
return @($actions.ToArray())
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Builds the plain-text list of pending changes: apps to remove and tweaks to apply or undo.
|
||||
|
||||
.PARAMETER ShowCurrentlyAppliedTweaksCheckBox
|
||||
When checked, pending tweak actions are computed against the system's live applied
|
||||
state instead of the controls' initial values.
|
||||
|
||||
.OUTPUTS
|
||||
System.String[]. One line per pending change, or an empty array if nothing is selected.
|
||||
#>
|
||||
function New-Overview {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
@@ -114,12 +125,12 @@ function New-Overview {
|
||||
}
|
||||
}
|
||||
if ($selectedAppsCount -gt 0) {
|
||||
$changesList += "Remove $selectedAppsCount application(s)"
|
||||
$changesList += Get-Translation -Key 'DeployChangeRemoveApps' -Count $selectedAppsCount -FormatArgs @($selectedAppsCount)
|
||||
}
|
||||
|
||||
foreach ($tweakAction in @(Get-PendingTweakActions -Window $Window -ShowAppliedTweaksMode:$showAppliedTweaksMode)) {
|
||||
if ($tweakAction.Action -eq 'Undo') {
|
||||
$changesList += "Undo: $($tweakAction.Label)"
|
||||
$changesList += Get-Translation -Key 'DeployChangeUndoPrefix' -FormatArgs @($tweakAction.Label)
|
||||
}
|
||||
else {
|
||||
$changesList += $tweakAction.Label
|
||||
@@ -129,6 +140,11 @@ function New-Overview {
|
||||
return $changesList
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Shows the pending-changes list from New-Overview in a message box, or a
|
||||
"no changes selected" message when there's nothing to show.
|
||||
#>
|
||||
function Invoke-ShowChangesOverview {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
@@ -139,12 +155,12 @@ function Invoke-ShowChangesOverview {
|
||||
$changesList = New-Overview -Window $Window -AppsPanel $AppsPanel -ShowCurrentlyAppliedTweaksCheckBox $ShowCurrentlyAppliedTweaksCheckBox
|
||||
|
||||
if ($changesList.Count -eq 0) {
|
||||
Show-MessageBox -Message 'No changes have been selected.' -Title 'Selected Changes' -Button 'OK' -Icon 'Information'
|
||||
Show-MessageBox -Message (Get-Translation -Key 'DeployNoChangesSelectedMessage') -Title (Get-Translation -Key 'DeploySelectedChangesTitle') -Button 'OK' -Icon 'Information'
|
||||
return
|
||||
}
|
||||
|
||||
$message = ($changesList | ForEach-Object { "$([char]0x2022) $_" }) -join "`n"
|
||||
Show-MessageBox -Message $message -Title 'Selected Changes' -Button 'OK' -Icon 'None' -Width 600
|
||||
Show-MessageBox -Message $message -Title (Get-Translation -Key 'DeploySelectedChangesTitle') -Button 'OK' -Icon 'None' -Width 600
|
||||
}
|
||||
|
||||
<#
|
||||
@@ -437,6 +453,14 @@ function Update-AppliedTweaksUserModeState {
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Updates the deployment-target description text to match the selected user option.
|
||||
|
||||
.DESCRIPTION
|
||||
Falls back to a generic description (current user, other user, or Sysprep default
|
||||
profile) when a username can't be resolved yet, and to a specific one once it can.
|
||||
#>
|
||||
function Update-UserSelectionDescription {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
@@ -449,23 +473,23 @@ function Update-UserSelectionDescription {
|
||||
0 {
|
||||
$currentUserName = Get-UserName
|
||||
if ([string]::IsNullOrWhiteSpace($currentUserName)) {
|
||||
$UserSelectionDescription.Text = "The currently logged-in user profile"
|
||||
$UserSelectionDescription.Text = Get-Translation -Key 'DeployUserDescriptionCurrentUser'
|
||||
}
|
||||
else {
|
||||
$UserSelectionDescription.Text = "The currently logged-in user profile: $currentUserName"
|
||||
$UserSelectionDescription.Text = Get-Translation -Key 'DeployUserDescriptionCurrentUserNamed' -FormatArgs @($currentUserName)
|
||||
}
|
||||
}
|
||||
1 {
|
||||
$targetUserName = $OtherUsernameTextBox.Text.Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($targetUserName)) {
|
||||
$UserSelectionDescription.Text = "A different user profile on this system"
|
||||
$UserSelectionDescription.Text = Get-Translation -Key 'DeployUserDescriptionOtherUser'
|
||||
}
|
||||
else {
|
||||
$UserSelectionDescription.Text = "A different user profile on this system: $targetUserName"
|
||||
$UserSelectionDescription.Text = Get-Translation -Key 'DeployUserDescriptionOtherUserNamed' -FormatArgs @($targetUserName)
|
||||
}
|
||||
}
|
||||
default {
|
||||
$UserSelectionDescription.Text = "The default user template, affecting all new users created after this point. Useful for Sysprep deployment."
|
||||
$UserSelectionDescription.Text = Get-Translation -Key 'DeployUserDescriptionSysprep'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -154,7 +154,8 @@ function New-DynamicTweakControls {
|
||||
Returns the existing category panel or creates and registers a new one.
|
||||
|
||||
.PARAMETER CategoryObj
|
||||
The category definition containing Name and Icon properties.
|
||||
The category definition, with Name and Icon for internal use (the map key, the
|
||||
wiki-link slug) and CategoryId for the translated header shown to the user.
|
||||
|
||||
.OUTPUTS
|
||||
System.Windows.Controls.StackPanel. The category's content panel.
|
||||
@@ -162,6 +163,7 @@ function New-DynamicTweakControls {
|
||||
function Get-OrCreateCategoryCard($categoryObj) {
|
||||
$categoryName = $categoryObj.Name
|
||||
$categoryIcon = $categoryObj.Icon
|
||||
$categoryLabel = Get-Translation -Key $categoryObj.CategoryId -Field 'Label' -Section 'Categories'
|
||||
|
||||
if ($script:CategoryCardMap.ContainsKey($categoryName)) { return $script:CategoryCardMap[$categoryName] }
|
||||
|
||||
@@ -193,7 +195,7 @@ function New-DynamicTweakControls {
|
||||
$headerRow.Children.Add($icon) | Out-Null
|
||||
|
||||
$header = New-Object System.Windows.Controls.TextBlock
|
||||
$header.Text = $categoryName
|
||||
$header.Text = $categoryLabel
|
||||
$header.Style = $Window.Resources['CategoryHeaderTextBlock']
|
||||
$headerRow.Children.Add($header) | Out-Null
|
||||
|
||||
@@ -203,7 +205,7 @@ function New-DynamicTweakControls {
|
||||
|
||||
$helpBtn = New-Object System.Windows.Controls.Button
|
||||
$helpBtn.Content = $helpIcon
|
||||
$helpBtn.ToolTip = "Open the wiki for more info on '$categoryName' tweaks"
|
||||
$helpBtn.ToolTip = Get-Translation -Key 'TweaksCategoryHelpTooltip' -FormatArgs @($categoryLabel)
|
||||
$helpBtn.Tag = (Get-WikiUrlForCategory -category $categoryName)
|
||||
$helpBtn.Style = $Window.Resources['CategoryHelpLinkButtonStyle']
|
||||
$helpBtn.Add_Click({
|
||||
@@ -347,17 +349,18 @@ function New-DynamicTweakControls {
|
||||
|
||||
if ($featureMap.ContainsKey($soleFid)) {
|
||||
$soleFeature = $featureMap[$soleFid]
|
||||
$opt = 'Apply'
|
||||
if ($soleFeature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($soleFeature.FeatureId -match '^Enable') { $opt = 'Enable' }
|
||||
$items = @('No Change', $opt)
|
||||
$soleFeatureLabel = Get-Translation -Key $soleFeature.FeatureId -Field 'Label' -Section 'Features'
|
||||
$opt = Get-Translation -Key 'TweaksOptionApply'
|
||||
if ($soleFeature.FeatureId -match '^Disable') { $opt = Get-Translation -Key 'TweaksOptionDisable' } elseif ($soleFeature.FeatureId -match '^Enable') { $opt = Get-Translation -Key 'TweaksOptionEnable' }
|
||||
$items = @((Get-Translation -Key 'TweaksOptionNoChange'), $opt)
|
||||
$comboName = ("Feature_{0}_Combo" -f $soleFeature.FeatureId) -replace '[^a-zA-Z0-9_]', ''
|
||||
if (-not $panel) { $panel = Get-OrCreateCategoryCard -categoryObj $categoryObj }
|
||||
$combo = New-LabeledCombo -parent $panel -labelText $soleFeature.Label -comboName $comboName -items $items
|
||||
$combo = New-LabeledCombo -parent $panel -labelText $soleFeatureLabel -comboName $comboName -items $items
|
||||
# attach tooltip from Features.json if present
|
||||
if ($soleFeature.ToolTip -or $soleFeature.DisableWhenApplied -eq $true) {
|
||||
$tooltipText = $soleFeature.ToolTip
|
||||
$tooltipText = Get-Translation -Key $soleFeature.FeatureId -Field 'ToolTip' -Section 'Features'
|
||||
if ($soleFeature.DisableWhenApplied -eq $true) {
|
||||
$tooltipText = "This tweak is already applied and cannot be undone automatically. Visit the Win11Debloat wiki for instructions on how to manually revert this change."
|
||||
$tooltipText = Get-Translation -Key 'TweaksAlreadyAppliedTooltip'
|
||||
}
|
||||
$tipBlock = New-Object System.Windows.Controls.TextBlock
|
||||
$tipBlock.Text = $tooltipText
|
||||
@@ -369,19 +372,20 @@ function New-DynamicTweakControls {
|
||||
try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {}
|
||||
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
|
||||
}
|
||||
$script:UiControlMappings[$comboName] = @{ Type = 'feature'; FeatureId = $soleFeature.FeatureId; Label = $soleFeature.Label; CategoryId = $categoryId }
|
||||
$script:UiControlMappings[$comboName] = @{ Type = 'feature'; FeatureId = $soleFeature.FeatureId; Label = $soleFeatureLabel; CategoryId = $categoryId }
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
$items = @('No Change') + ($filteredValues | ForEach-Object { $_.Label })
|
||||
$groupLabel = Get-Translation -Key $group.GroupId -Field 'Label' -Section 'UiGroups'
|
||||
$items = @((Get-Translation -Key 'TweaksOptionNoChange')) + ($filteredValues | ForEach-Object { Get-GroupValueTranslation -GroupId $group.GroupId -FeatureId $_.FeatureIds[0] -FallbackLabel $_.Label })
|
||||
$comboName = 'Group_{0}Combo' -f $group.GroupId
|
||||
if (-not $panel) { $panel = Get-OrCreateCategoryCard -categoryObj $categoryObj }
|
||||
$combo = New-LabeledCombo -parent $panel -labelText $group.Label -comboName $comboName -items $items
|
||||
$combo = New-LabeledCombo -parent $panel -labelText $groupLabel -comboName $comboName -items $items
|
||||
# attach tooltip from UiGroups if present
|
||||
if ($group.ToolTip) {
|
||||
$tipBlock = New-Object System.Windows.Controls.TextBlock
|
||||
$tipBlock.Text = $group.ToolTip
|
||||
$tipBlock.Text = Get-Translation -Key $group.GroupId -Field 'ToolTip' -Section 'UiGroups'
|
||||
$tipBlock.TextWrapping = 'Wrap'
|
||||
$tipBlock.MaxWidth = 420
|
||||
$combo.ToolTip = $tipBlock
|
||||
@@ -389,21 +393,22 @@ function New-DynamicTweakControls {
|
||||
try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {}
|
||||
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
|
||||
}
|
||||
$script:UiControlMappings[$comboName] = @{ Type = 'group'; Values = $filteredValues; Label = $group.Label; CategoryId = $categoryId }
|
||||
$script:UiControlMappings[$comboName] = @{ Type = 'group'; Values = $filteredValues; Label = $groupLabel; CategoryId = $categoryId }
|
||||
}
|
||||
elseif ($item.Type -eq 'feature') {
|
||||
$feature = $item.Data
|
||||
$opt = 'Apply'
|
||||
if ($feature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($feature.FeatureId -match '^Enable') { $opt = 'Enable' }
|
||||
$items = @('No Change', $opt)
|
||||
$featureLabel = Get-Translation -Key $feature.FeatureId -Field 'Label' -Section 'Features'
|
||||
$opt = Get-Translation -Key 'TweaksOptionApply'
|
||||
if ($feature.FeatureId -match '^Disable') { $opt = Get-Translation -Key 'TweaksOptionDisable' } elseif ($feature.FeatureId -match '^Enable') { $opt = Get-Translation -Key 'TweaksOptionEnable' }
|
||||
$items = @((Get-Translation -Key 'TweaksOptionNoChange'), $opt)
|
||||
$comboName = ("Feature_{0}_Combo" -f $feature.FeatureId) -replace '[^a-zA-Z0-9_]', ''
|
||||
if (-not $panel) { $panel = Get-OrCreateCategoryCard -categoryObj $categoryObj }
|
||||
$combo = New-LabeledCombo -parent $panel -labelText $feature.Label -comboName $comboName -items $items
|
||||
$combo = New-LabeledCombo -parent $panel -labelText $featureLabel -comboName $comboName -items $items
|
||||
# attach tooltip from Features.json if present, and include the disabled-state reason
|
||||
if ($feature.ToolTip -or $feature.DisableWhenApplied -eq $true) {
|
||||
$tooltipText = $feature.ToolTip
|
||||
$tooltipText = Get-Translation -Key $feature.FeatureId -Field 'ToolTip' -Section 'Features'
|
||||
if ($feature.DisableWhenApplied -eq $true) {
|
||||
$tooltipText = "This tweak is already applied and cannot be undone automatically. Visit the Win11Debloat wiki for instructions on how to manually revert this change."
|
||||
$tooltipText = Get-Translation -Key 'TweaksAlreadyAppliedTooltip'
|
||||
}
|
||||
|
||||
$tipBlock = New-Object System.Windows.Controls.TextBlock
|
||||
@@ -425,8 +430,10 @@ function New-DynamicTweakControls {
|
||||
$script:FeatureLabelLookup = @{}
|
||||
$script:UndoFeatureLabelLookup = @{}
|
||||
foreach ($f in $featuresJson.Features) {
|
||||
$script:FeatureLabelLookup[$f.FeatureId] = $f.Label
|
||||
$script:UndoFeatureLabelLookup[$f.FeatureId] = $f.UndoLabel
|
||||
$script:FeatureLabelLookup[$f.FeatureId] = Get-Translation -Key $f.FeatureId -Field 'Label' -Section 'Features'
|
||||
if ($f.UndoLabel) {
|
||||
$script:UndoFeatureLabelLookup[$f.FeatureId] = Get-Translation -Key $f.FeatureId -Field 'UndoLabel' -Section 'Features'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,8 +81,9 @@ function Test-RestoreDialogFeatureCanAutoRevert {
|
||||
Resolves a human-readable label for a feature shown in the restore dialog.
|
||||
|
||||
.DESCRIPTION
|
||||
Returns the feature's Label from Features.json when found, falling back
|
||||
to the raw FeatureId string. For null/empty FeatureIds returns 'Unknown feature'.
|
||||
Returns the feature's translated Label when found, falling back to the
|
||||
raw FeatureId string. For null/empty FeatureIds returns the translated
|
||||
"unknown feature" placeholder.
|
||||
|
||||
.PARAMETER FeatureId
|
||||
The feature identifier to resolve a label for.
|
||||
@@ -97,12 +98,12 @@ function Get-RestoreDialogFeatureDisplayLabel {
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($FeatureId)) {
|
||||
return 'Unknown feature'
|
||||
return Get-Translation -Key 'RestoreUnknownFeature'
|
||||
}
|
||||
|
||||
$featureDefinition = Get-RestoreDialogFeatureDefinition -FeatureId $FeatureId -Features $Features
|
||||
if ($featureDefinition) {
|
||||
return [string]$featureDefinition.Label
|
||||
return Get-Translation -Key $FeatureId -Field 'Label' -Section 'Features'
|
||||
}
|
||||
|
||||
return $FeatureId
|
||||
|
||||
@@ -32,6 +32,7 @@ function Show-AboutDialog {
|
||||
|
||||
# Load XAML from file
|
||||
$xaml = Get-Content -Path $script:AboutWindowSchema -Raw
|
||||
$xaml = ConvertTo-LocalizedXaml -Xaml $xaml
|
||||
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
|
||||
try {
|
||||
$aboutWindow = [System.Windows.Markup.XamlReader]::Load($reader)
|
||||
|
||||
@@ -24,6 +24,7 @@ function Show-AppSelectionWindow {
|
||||
|
||||
# Load XAML from file
|
||||
$xaml = Get-Content -Path $script:AppSelectionSchema -Raw
|
||||
$xaml = ConvertTo-LocalizedXaml -Xaml $xaml
|
||||
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
|
||||
try {
|
||||
$window = [System.Windows.Markup.XamlReader]::Load($reader)
|
||||
@@ -72,7 +73,7 @@ function Show-AppSelectionWindow {
|
||||
$listOfApps = Get-WingetInstalledApps -TimeOut 10 -NonBlocking
|
||||
if ($null -eq $listOfApps) {
|
||||
# Show error that the script was unable to get list of apps from WinGet
|
||||
Show-MessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' -Owner $window | Out-Null
|
||||
Show-MessageBox -Message (Get-Translation -Key 'AppSelectionWinGetLoadFailedMessage') -Title (Get-Translation -Key 'ErrorTitle') -Button 'OK' -Icon 'Error' -Owner $window | Out-Null
|
||||
$onlyInstalledBox.IsChecked = $false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ function Show-ApplyModal {
|
||||
|
||||
# Load XAML from file
|
||||
$xaml = Get-Content -Path $script:ApplyChangesWindowSchema -Raw
|
||||
$xaml = ConvertTo-LocalizedXaml -Xaml $xaml
|
||||
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
|
||||
try {
|
||||
$applyWindow = [System.Windows.Markup.XamlReader]::Load($reader)
|
||||
@@ -74,8 +75,8 @@ function Show-ApplyModal {
|
||||
# Initialize in-progress state
|
||||
$script:ApplyInProgressPanel.Visibility = 'Visible'
|
||||
$script:ApplyCompletionPanel.Visibility = 'Collapsed'
|
||||
$script:ApplyStepNameEl.Text = "Preparing..."
|
||||
$script:ApplyStepCounterEl.Text = "Preparing..."
|
||||
$script:ApplyStepNameEl.Text = Get-Translation -Key 'ApplyPreparing'
|
||||
$script:ApplyStepCounterEl.Text = Get-Translation -Key 'ApplyStepCounter' -FormatArgs @(0, 0)
|
||||
$script:ApplyProgressBarEl.Value = 0
|
||||
$script:ApplyModalInErrorState = $false
|
||||
|
||||
@@ -83,7 +84,7 @@ function Show-ApplyModal {
|
||||
$script:ApplyProgressCallback = {
|
||||
param($currentStep, $totalSteps, $stepName)
|
||||
$script:ApplyStepNameEl.Text = $stepName
|
||||
$script:ApplyStepCounterEl.Text = "Step $currentStep of $totalSteps"
|
||||
$script:ApplyStepCounterEl.Text = Get-Translation -Key 'ApplyStepCounter' -FormatArgs @($currentStep, $totalSteps)
|
||||
# Store current step/total in Tag properties for sub-step interpolation
|
||||
$script:ApplyStepCounterEl.Tag = $currentStep
|
||||
$script:ApplyProgressBarEl.Tag = $totalSteps
|
||||
@@ -140,8 +141,8 @@ function Show-ApplyModal {
|
||||
|
||||
$script:ApplyCompletionIconEl.Text = [char]0xE7BA
|
||||
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d"))
|
||||
$script:ApplyCompletionTitleEl.Text = "Cancelled"
|
||||
$script:ApplyCompletionMessageEl.Text = "Script execution was cancelled by the user."
|
||||
$script:ApplyCompletionTitleEl.Text = Get-Translation -Key 'ApplyCompletionTitleCancelled'
|
||||
$script:ApplyCompletionMessageEl.Text = Get-Translation -Key 'ApplyCompletionMessageCancelled'
|
||||
} elseif ($failureCount -gt 0 -or $appRemovalVerificationUnavailable) {
|
||||
if ($failureCount -gt 0) {
|
||||
Write-Host "Script completed with $failureCount error(s)."
|
||||
@@ -150,17 +151,17 @@ function Show-ApplyModal {
|
||||
$script:ApplyCompletionIconEl.Text = [char]0xE7BA
|
||||
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d"))
|
||||
if ($failureCount -eq 0 -and $appRemovalVerificationUnavailable) {
|
||||
$script:ApplyCompletionTitleEl.Text = "Changes Applied"
|
||||
$script:ApplyCompletionMessageEl.Text = "All changes were applied without errors, but Win11Debloat could not confirm that all selected apps were successfully uninstalled."
|
||||
$script:ApplyCompletionTitleEl.Text = Get-Translation -Key 'ApplyCompletionTitleSuccess'
|
||||
$script:ApplyCompletionMessageEl.Text = Get-Translation -Key 'ApplyCompletionMessageVerificationUnavailable'
|
||||
}
|
||||
else {
|
||||
$script:ApplyCompletionTitleEl.Text = "Changes Applied with Errors"
|
||||
$script:ApplyCompletionMessageEl.Text = "$failureCount change(s) failed. See console for details."
|
||||
$script:ApplyCompletionTitleEl.Text = Get-Translation -Key 'ApplyCompletionTitleErrors'
|
||||
$script:ApplyCompletionMessageEl.Text = Get-Translation -Key 'ApplyCompletionMessageFailures' -Count $failureCount -FormatArgs @($failureCount)
|
||||
}
|
||||
} else {
|
||||
Write-Host "All changes have been applied successfully!"
|
||||
|
||||
$script:ApplyCompletionTitleEl.Text = "Changes Applied"
|
||||
$script:ApplyCompletionTitleEl.Text = Get-Translation -Key 'ApplyCompletionTitleSuccess'
|
||||
|
||||
# Show completion message with reboot instructions if any applied features require reboot
|
||||
if ($InvokeRestartExplorer) {
|
||||
@@ -179,7 +180,7 @@ function Show-ApplyModal {
|
||||
$applyRebootPanel.Visibility = 'Visible'
|
||||
}
|
||||
else {
|
||||
$script:ApplyCompletionMessageEl.Text = "Your system is ready. Thanks for using Win11Debloat!"
|
||||
$script:ApplyCompletionMessageEl.Text = Get-Translation -Key 'ApplyCompletionMessageReady'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,24 +192,24 @@ function Show-ApplyModal {
|
||||
$script:ApplyCompletionPanel.Visibility = 'Visible'
|
||||
$script:ApplyCompletionIconEl.Text = [char]0xEA39
|
||||
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#c42b1c"))
|
||||
$script:ApplyCompletionTitleEl.Text = "Error"
|
||||
$script:ApplyCompletionMessageEl.Text = "An error occurred while applying changes: $($_.Exception.Message)"
|
||||
|
||||
$script:ApplyCompletionTitleEl.Text = Get-Translation -Key 'ApplyCompletionTitleError'
|
||||
$script:ApplyCompletionMessageEl.Text = Get-Translation -Key 'ApplyCompletionMessageError' -FormatArgs @($_.Exception.Message)
|
||||
|
||||
# Set error state to change Kofi button to report link
|
||||
$script:ApplyModalInErrorState = $true
|
||||
|
||||
# Update Kofi button to be a report issue button
|
||||
$applyKofiBtn.Content = $null
|
||||
|
||||
|
||||
$reportText = [System.Windows.Controls.TextBlock]::new()
|
||||
$reportText.Text = 'Report a bug'
|
||||
$reportText.Text = Get-Translation -Key 'MenuReportBug'
|
||||
$reportText.VerticalAlignment = 'Center'
|
||||
$reportText.FontSize = 14
|
||||
$reportText.Margin = [System.Windows.Thickness]::new(0, 0, 0, 1)
|
||||
|
||||
$applyKofiBtn.Content = $reportText
|
||||
|
||||
[System.Windows.Automation.AutomationProperties]::SetName($applyKofiBtn, 'Report a bug')
|
||||
|
||||
[System.Windows.Automation.AutomationProperties]::SetName($applyKofiBtn, (Get-Translation -Key 'MenuReportBug'))
|
||||
|
||||
$applyWindow.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Render, [action]{})
|
||||
}
|
||||
|
||||
@@ -72,8 +72,8 @@ function Hide-Bubble {
|
||||
parameter is mandatory.
|
||||
|
||||
.PARAMETER Message
|
||||
The text message to display inside the bubble. Defaults to
|
||||
'View the selected changes here'.
|
||||
The text message to display inside the bubble. Defaults to the
|
||||
translated "view the selected changes" prompt.
|
||||
|
||||
.PARAMETER DurationSeconds
|
||||
The number of seconds the bubble remains visible before auto-hiding.
|
||||
@@ -91,7 +91,7 @@ function Show-Bubble {
|
||||
[System.Windows.Controls.Control]$TargetControl,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Message = 'View the selected changes here',
|
||||
[string]$Message = (Get-Translation -Key 'BubbleViewChanges'),
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[int]$DurationSeconds = 5
|
||||
@@ -104,6 +104,7 @@ function Show-Bubble {
|
||||
Hide-Bubble -Immediate
|
||||
|
||||
$xaml = Get-Content -Path $script:BubbleHintSchema -Raw
|
||||
$xaml = ConvertTo-LocalizedXaml -Xaml $xaml
|
||||
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
|
||||
try {
|
||||
$bubblePanel = [System.Windows.Markup.XamlReader]::Load($reader)
|
||||
|
||||
@@ -1,3 +1,26 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Maps an internal import/export category ID ('Applications', 'System Tweaks',
|
||||
'Deployment Settings') to its Chrome.json translation key.
|
||||
|
||||
.DESCRIPTION
|
||||
The category strings stay as the stable internal IDs used for -contains checks and
|
||||
hashtable lookups throughout this file; only the label shown to the user is translated.
|
||||
#>
|
||||
function Get-ImportExportCategoryLabelKey {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Category
|
||||
)
|
||||
|
||||
switch ($Category) {
|
||||
'Applications' { return 'ImportExportCategoryApplications' }
|
||||
'System Tweaks' { return 'ImportExportCategorySystemTweaks' }
|
||||
'Deployment Settings' { return 'ImportExportCategoryDeploymentSettings' }
|
||||
default { return $Category }
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Shows a modal category-selection dialog for importing or exporting configuration.
|
||||
@@ -11,7 +34,7 @@ function Show-ImportExportConfigWindow {
|
||||
[string[]]$Categories = @('Applications', 'System Tweaks', 'Deployment Settings'),
|
||||
[string[]]$DisabledCategories = @(),
|
||||
[hashtable]$CategoryDetails = @(),
|
||||
[string]$ActionLabel = 'OK'
|
||||
[string]$ActionLabel = (Get-Translation -Key 'MessageBoxOk')
|
||||
)
|
||||
|
||||
# Show overlay on owner window
|
||||
@@ -32,7 +55,7 @@ function Show-ImportExportConfigWindow {
|
||||
$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
|
||||
Show-MessageBox -Message (Get-Translation -Key 'ImportExportSchemaMissingMessage') -Title (Get-Translation -Key 'ErrorTitle') -Button 'OK' -Icon 'Error' -Owner $Owner | Out-Null
|
||||
if ($overlay -and -not $overlayWasAlreadyVisible) {
|
||||
try { $Owner.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' }) } catch { }
|
||||
}
|
||||
@@ -40,6 +63,7 @@ function Show-ImportExportConfigWindow {
|
||||
}
|
||||
|
||||
$xaml = Get-Content -Path $schemaPath -Raw
|
||||
$xaml = ConvertTo-LocalizedXaml -Xaml $xaml
|
||||
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
|
||||
try {
|
||||
$dlg = [System.Windows.Markup.XamlReader]::Load($reader)
|
||||
@@ -79,7 +103,7 @@ function Show-ImportExportConfigWindow {
|
||||
|
||||
# Create checkbox
|
||||
$cb = New-Object System.Windows.Controls.CheckBox
|
||||
$cb.Content = $cat
|
||||
$cb.Content = Get-Translation -Key (Get-ImportExportCategoryLabelKey -Category $cat)
|
||||
$cb.IsChecked = $true
|
||||
$cb.Margin = [System.Windows.Thickness]::new(0,0,0,4)
|
||||
$cb.FontSize = 14
|
||||
@@ -89,7 +113,7 @@ function Show-ImportExportConfigWindow {
|
||||
$cb.IsChecked = $false
|
||||
$cb.IsEnabled = $false
|
||||
$cb.Opacity = 0.65
|
||||
$cb.ToolTip = 'No selected settings available in this category.'
|
||||
$cb.ToolTip = Get-Translation -Key 'ImportExportCategoryDisabledTooltip'
|
||||
}
|
||||
|
||||
$container.Children.Add($cb) | Out-Null
|
||||
@@ -245,6 +269,11 @@ function Get-AvailableImportExportCategories {
|
||||
return $availableCategories
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Summarizes a deployment settings array into the short description shown under the
|
||||
"Deployment Settings" checkbox in the import/export category picker.
|
||||
#>
|
||||
function Get-DeploymentCategoryDetailString {
|
||||
param (
|
||||
[array]$DeploymentSettings
|
||||
@@ -261,33 +290,38 @@ function Get-DeploymentCategoryDetailString {
|
||||
|
||||
if ($lookup.ContainsKey('UserSelectionIndex')) {
|
||||
switch ([int]$lookup['UserSelectionIndex']) {
|
||||
0 { $line1 += 'User: Current User' }
|
||||
1 { $line1 += "User: $(if ($lookup['OtherUsername']) { $lookup['OtherUsername'] } else { 'Other User' })" }
|
||||
2 { $line1 += 'User: Sysprep' }
|
||||
0 { $line1 += Get-Translation -Key 'ImportExportDeployUserCurrentUser' }
|
||||
1 { $line1 += Get-Translation -Key 'ImportExportDeployUserOther' -FormatArgs @($(if ($lookup['OtherUsername']) { $lookup['OtherUsername'] } else { Get-Translation -Key 'ImportExportDeployUserOtherFallback' })) }
|
||||
2 { $line1 += Get-Translation -Key 'ImportExportDeployUserSysprep' }
|
||||
}
|
||||
}
|
||||
|
||||
if ($lookup.ContainsKey('AppRemovalScopeIndex')) {
|
||||
switch ([int]$lookup['AppRemovalScopeIndex']) {
|
||||
0 { $line1 += 'App Removal: All Users' }
|
||||
1 { $line1 += 'App Removal: Current User' }
|
||||
2 { $line1 += "App Removal: $(if ($lookup['OtherUsername']) { $lookup['OtherUsername'] } else { 'Other User' })" }
|
||||
0 { $line1 += Get-Translation -Key 'ImportExportAppRemovalAllUsers' }
|
||||
1 { $line1 += Get-Translation -Key 'ImportExportAppRemovalCurrentUser' }
|
||||
2 { $line1 += Get-Translation -Key 'ImportExportAppRemovalOther' -FormatArgs @($(if ($lookup['OtherUsername']) { $lookup['OtherUsername'] } else { Get-Translation -Key 'ImportExportDeployUserOtherFallback' })) }
|
||||
}
|
||||
}
|
||||
|
||||
$options = @()
|
||||
if ($lookup.ContainsKey('CreateRestorePoint') -and [bool]$lookup['CreateRestorePoint']) { $options += 'Restore Point' }
|
||||
if (-not ($lookup.ContainsKey('SkipRegistryBackup') -and [bool]$lookup['SkipRegistryBackup'])) { $options += 'Registry Backup' }
|
||||
if ($lookup.ContainsKey('RestartExplorer') -and [bool]$lookup['RestartExplorer']) { $options += 'Restart Explorer' }
|
||||
if ($lookup.ContainsKey('CreateRestorePoint') -and [bool]$lookup['CreateRestorePoint']) { $options += Get-Translation -Key 'ImportExportOptionRestorePoint' }
|
||||
if (-not ($lookup.ContainsKey('SkipRegistryBackup') -and [bool]$lookup['SkipRegistryBackup'])) { $options += Get-Translation -Key 'ImportExportOptionRegistryBackup' }
|
||||
if ($lookup.ContainsKey('RestartExplorer') -and [bool]$lookup['RestartExplorer']) { $options += Get-Translation -Key 'ImportExportOptionRestartExplorer' }
|
||||
|
||||
$lines = @()
|
||||
if ($line1.Count -gt 0) { $lines += $line1 -join ', ' }
|
||||
if ($options.Count -gt 0) { $lines += "Options: $($options -join ', ')" }
|
||||
if ($options.Count -gt 0) { $lines += Get-Translation -Key 'ImportExportOptionsPrefix' -FormatArgs @($options -join ', ') }
|
||||
|
||||
if ($lines.Count -gt 0) { return $lines -join "`n" }
|
||||
return 'Default deployment settings'
|
||||
return Get-Translation -Key 'ImportExportDefaultDeploymentSettings'
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Builds the per-category description text shown under each checkbox in the
|
||||
import/export category picker.
|
||||
#>
|
||||
function Build-CategoryDetails {
|
||||
param (
|
||||
[int]$AppCount = 0,
|
||||
@@ -298,17 +332,17 @@ function Build-CategoryDetails {
|
||||
$details = @{}
|
||||
|
||||
if ($AppCount -gt 0) {
|
||||
$details['Applications'] = "$AppCount app$(if ($AppCount -ne 1) { 's' }) selected"
|
||||
$details['Applications'] = Get-Translation -Key 'ImportExportAppsSelected' -Count $AppCount -FormatArgs @($AppCount)
|
||||
}
|
||||
else {
|
||||
$details['Applications'] = 'No apps selected'
|
||||
$details['Applications'] = Get-Translation -Key 'ImportExportNoAppsSelected'
|
||||
}
|
||||
|
||||
if ($TweakCount -gt 0) {
|
||||
$details['System Tweaks'] = "$TweakCount tweak$(if ($TweakCount -ne 1) { 's' }) selected"
|
||||
$details['System Tweaks'] = Get-Translation -Key 'ImportExportTweaksSelected' -Count $TweakCount -FormatArgs @($TweakCount)
|
||||
}
|
||||
else {
|
||||
$details['System Tweaks'] = 'No tweaks selected'
|
||||
$details['System Tweaks'] = Get-Translation -Key 'ImportExportNoTweaksSelected'
|
||||
}
|
||||
|
||||
if ($DeploymentSettings) {
|
||||
@@ -422,7 +456,7 @@ 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 'Create a configuration file based on the currently selected settings. You can choose which settings categories you wish to include in the export.' -DisabledCategories $disabledCategories -CategoryDetails $categoryDetails -ActionLabel 'Export Settings'
|
||||
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title (Get-Translation -Key 'ImportExportExportTitle') -Prompt (Get-Translation -Key 'ImportExportExportPrompt') -DisabledCategories $disabledCategories -CategoryDetails $categoryDetails -ActionLabel (Get-Translation -Key 'ImportExportExportActionLabel')
|
||||
if (-not $categories) {
|
||||
Write-Host 'Export canceled.'
|
||||
return
|
||||
@@ -442,7 +476,7 @@ function Export-Configuration {
|
||||
|
||||
# Show native save-file dialog
|
||||
$saveDialog = New-Object Microsoft.Win32.SaveFileDialog
|
||||
$saveDialog.Title = 'Export Configuration'
|
||||
$saveDialog.Title = Get-Translation -Key 'ImportExportSelectExportFileDialogTitle'
|
||||
$saveDialog.Filter = 'JSON files (*.json)|*.json|All files (*.*)|*.*'
|
||||
$saveDialog.DefaultExt = '.json'
|
||||
$saveDialog.FileName = "Win11Debloat-Config-$(Get-Date -Format 'yyyyMMdd').json"
|
||||
@@ -456,17 +490,17 @@ function Export-Configuration {
|
||||
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Write-Host "[WhatIf] Export configuration to '$($saveDialog.FileName)'" -ForegroundColor Cyan
|
||||
Show-MessageBox -Message "[WhatIf] Configuration would be exported to this file (no file written)." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null
|
||||
Show-MessageBox -Message (Get-Translation -Key 'ImportExportWhatIfExportMessage') -Title (Get-Translation -Key 'ImportExportExportTitle') -Button 'OK' -Icon 'Information' | Out-Null
|
||||
return
|
||||
}
|
||||
|
||||
if (Save-ToFile -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
|
||||
Show-MessageBox -Message (Get-Translation -Key 'ImportExportExportSuccessMessage') -Title (Get-Translation -Key 'ImportExportExportTitle') -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
|
||||
Show-MessageBox -Message (Get-Translation -Key 'ImportExportExportFailedMessage') -Title (Get-Translation -Key 'ErrorTitle') -Button 'OK' -Icon 'Error' | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,7 +522,7 @@ function Import-Configuration {
|
||||
|
||||
# Show native open-file dialog
|
||||
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
|
||||
$openDialog.Title = 'Select Configuration File'
|
||||
$openDialog.Title = Get-Translation -Key 'ImportExportSelectImportFileDialogTitle'
|
||||
$openDialog.Filter = 'JSON files (*.json)|*.json|All files (*.*)|*.*'
|
||||
$openDialog.DefaultExt = '.json'
|
||||
|
||||
@@ -502,14 +536,14 @@ function Import-Configuration {
|
||||
$config = Import-JsonFile -filePath $openDialog.FileName -expectedVersion '1.0'
|
||||
if (-not $config) {
|
||||
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
|
||||
Show-MessageBox -Message (Get-Translation -Key 'ImportExportReadFailedMessage') -Title (Get-Translation -Key 'ImportExportInvalidConfigTitle') -Button 'OK' -Icon 'Error' | Out-Null
|
||||
return
|
||||
}
|
||||
|
||||
$consistencyError = Test-ConfigConsistency -Config $config
|
||||
if ($consistencyError) {
|
||||
Write-Error "Invalid configuration file '$($openDialog.FileName)': $consistencyError"
|
||||
Show-MessageBox -Message "Invalid configuration file: $consistencyError" -Title 'Invalid Config' -Button 'OK' -Icon 'Error' | Out-Null
|
||||
Show-MessageBox -Message (Get-Translation -Key 'ImportExportInvalidConfigMessage' -FormatArgs @($consistencyError)) -Title (Get-Translation -Key 'ImportExportInvalidConfigTitle') -Button 'OK' -Icon 'Error' | Out-Null
|
||||
return
|
||||
}
|
||||
|
||||
@@ -521,7 +555,7 @@ function Import-Configuration {
|
||||
$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 'Choose the settings categories that you wish to import. You can review and modify the imported settings before they are applied.' -Categories $availableCategories -CategoryDetails $categoryDetails -ActionLabel 'Import Settings'
|
||||
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title (Get-Translation -Key 'ImportExportImportTitle') -Prompt (Get-Translation -Key 'ImportExportImportPrompt') -Categories $availableCategories -CategoryDetails $categoryDetails -ActionLabel (Get-Translation -Key 'ImportExportImportActionLabel')
|
||||
if (-not $categories) {
|
||||
Write-Host 'Import canceled.'
|
||||
return
|
||||
@@ -553,7 +587,7 @@ function Import-Configuration {
|
||||
}
|
||||
|
||||
Write-Host 'Configuration imported successfully.'
|
||||
Show-MessageBox -Message "Configuration imported successfully." -Title 'Import Configuration' -Button 'OK' -Icon 'Information' | Out-Null
|
||||
Show-MessageBox -Message (Get-Translation -Key 'ImportExportImportSuccessMessage') -Title (Get-Translation -Key 'ImportExportImportTitle') -Button 'OK' -Icon 'Information' | Out-Null
|
||||
|
||||
if ($OnImportCompleted) {
|
||||
& $OnImportCompleted $categories
|
||||
|
||||
@@ -10,6 +10,7 @@ function Show-MainWindow {
|
||||
|
||||
# ---- Load XAML ----
|
||||
$xaml = Get-Content -Path $script:MainWindowSchema -Raw
|
||||
$xaml = ConvertTo-LocalizedXaml -Xaml $xaml
|
||||
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
|
||||
try {
|
||||
$window = [System.Windows.Markup.XamlReader]::Load($reader)
|
||||
@@ -97,7 +98,7 @@ function Show-MainWindow {
|
||||
Start-Process "explorer.exe" -ArgumentList $logsFolder
|
||||
}
|
||||
else {
|
||||
Show-MessageBox -Message "No logs folder found at: $logsFolder" -Title "Logs" -Button 'OK' -Icon 'Information'
|
||||
Show-MessageBox -Message (Get-Translation -Key 'LogsFolderNotFoundMessage' -FormatArgs @($logsFolder)) -Title (Get-Translation -Key 'LogsMenuTitle') -Button 'OK' -Icon 'Information'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -170,7 +171,7 @@ function Show-MainWindow {
|
||||
}
|
||||
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
|
||||
Show-MessageBox -Owner $window -Message (Get-Translation -Key 'ExportConfigFailedMessage' -FormatArgs @($_.Exception.Message)) -Title (Get-Translation -Key 'ExportConfigFailedTitle') -Button 'OK' -Icon 'Error' | Out-Null
|
||||
}
|
||||
})
|
||||
|
||||
@@ -180,13 +181,13 @@ function Show-MainWindow {
|
||||
$tabControl.SelectedIndex = 3
|
||||
Update-NavigationButtons -Window $window -TabControl $tabControl
|
||||
$window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Loaded, [action]{
|
||||
Show-Bubble -TargetControl $reviewChangesBtn -Message 'View the selected changes here'
|
||||
Show-Bubble -TargetControl $reviewChangesBtn -Message (Get-Translation -Key 'BubbleViewChanges')
|
||||
}) | 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
|
||||
Show-MessageBox -Owner $window -Message (Get-Translation -Key 'ImportConfigFailedMessage' -FormatArgs @($_.Exception.Message)) -Title (Get-Translation -Key 'ImportConfigFailedTitle') -Button 'OK' -Icon 'Error' | Out-Null
|
||||
}
|
||||
})
|
||||
|
||||
@@ -206,7 +207,7 @@ function Show-MainWindow {
|
||||
}
|
||||
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
|
||||
Show-MessageBox -Owner $window -Message (Get-Translation -Key 'RestoreBackupFailedMessage' -FormatArgs @($_.Exception.Message)) -Title (Get-Translation -Key 'RestoreBackupFailedTitle') -Button 'OK' -Icon 'Error' | Out-Null
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -232,7 +233,7 @@ function Show-MainWindow {
|
||||
$checkbox.Content = $preset.Name
|
||||
$checkbox.IsThreeState = $true
|
||||
$checkbox.Style = $window.Resources['PresetCheckBoxStyle']
|
||||
$checkbox.ToolTip = "Select $($preset.Name)"
|
||||
$checkbox.ToolTip = Get-Translation -Key 'AppPresetSelectTooltip' -FormatArgs @($preset.Name)
|
||||
$checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $preset.Name)
|
||||
Add-TriStateClickBehavior -CheckBox $checkbox
|
||||
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'PresetAppIds' -Value $preset.AppIds
|
||||
@@ -501,12 +502,11 @@ function Show-MainWindow {
|
||||
param($sourceControl, $e)
|
||||
if ($e.Key -eq [System.Windows.Input.Key]::F -and
|
||||
([System.Windows.Input.Keyboard]::Modifiers -band [System.Windows.Input.ModifierKeys]::Control)) {
|
||||
$currentTab = $tabControl.SelectedItem
|
||||
if ($currentTab.Header -eq "App Removal" -and $appSearchBox) {
|
||||
if ($tabControl.SelectedIndex -eq 1 -and $appSearchBox) {
|
||||
$appSearchBox.Focus()
|
||||
$e.Handled = $true
|
||||
}
|
||||
elseif ($currentTab.Header -eq "Tweaks" -and $tweakSearchBox) {
|
||||
elseif ($tabControl.SelectedIndex -eq 2 -and $tweakSearchBox) {
|
||||
$tweakSearchBox.Focus()
|
||||
$e.Handled = $true
|
||||
}
|
||||
@@ -599,9 +599,9 @@ function Show-MainWindow {
|
||||
$usernameValidationMessage.Text
|
||||
}
|
||||
else {
|
||||
"Please enter a valid username."
|
||||
Get-Translation -Key 'InvalidUsernameFallbackMessage'
|
||||
}
|
||||
Show-MessageBox -Message $validationMessage -Title "Invalid Username" -Button 'OK' -Icon 'Warning' | Out-Null
|
||||
Show-MessageBox -Message $validationMessage -Title (Get-Translation -Key 'InvalidUsernameTitle') -Button 'OK' -Icon 'Warning' | Out-Null
|
||||
return $false
|
||||
}
|
||||
return $true
|
||||
@@ -638,7 +638,7 @@ function Show-MainWindow {
|
||||
Invoke-NavigationUpdate
|
||||
|
||||
$window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Loaded, [action]{
|
||||
Show-Bubble -TargetControl $reviewChangesBtn -Message 'View the selected changes here'
|
||||
Show-Bubble -TargetControl $reviewChangesBtn -Message (Get-Translation -Key 'BubbleViewChanges')
|
||||
}) | Out-Null
|
||||
})
|
||||
|
||||
@@ -692,7 +692,7 @@ function Show-MainWindow {
|
||||
}
|
||||
|
||||
if (-not $hasAppSelection -and $selectedForwardFeatureIds.Count -eq 0 -and $script:UndoParams.Count -eq 0) {
|
||||
Show-MessageBox -Message 'No changes have been selected, please select at least one option to proceed.' -Title 'No Changes Selected' -Button 'OK' -Icon 'Information'
|
||||
Show-MessageBox -Message (Get-Translation -Key 'NoChangesSelectedMessage') -Title (Get-Translation -Key 'NoChangesSelectedTitle') -Button 'OK' -Icon 'Information'
|
||||
return
|
||||
}
|
||||
|
||||
@@ -764,7 +764,7 @@ function Show-MainWindow {
|
||||
if ($userSelectionCombo -and $userSelectionCombo.Items.Count -gt 0) {
|
||||
$currentUserItem = $userSelectionCombo.Items[0]
|
||||
if ($currentUserItem -is [System.Windows.Controls.ComboBoxItem]) {
|
||||
$currentUserItem.Content = "Current User ($(Get-UserName))"
|
||||
$currentUserItem.Content = Get-Translation -Key 'HomeCurrentUserWithName' -FormatArgs @(Get-UserName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -810,7 +810,7 @@ function Show-MainWindow {
|
||||
catch {
|
||||
Write-Warning "Error during GUI initialization: $($_.Exception.Message)"
|
||||
Write-Warning "Stack trace: $($_.Exception.StackTrace)"
|
||||
Show-MessageBox -Message "An error occurred during initialization: $($_.Exception.Message)" -Title "Initialization Error" -Button 'OK' -Icon 'Error' | Out-Null
|
||||
Show-MessageBox -Message (Get-Translation -Key 'InitializationErrorMessage' -FormatArgs @($_.Exception.Message)) -Title (Get-Translation -Key 'InitializationErrorTitle') -Button 'OK' -Icon 'Error' | Out-Null
|
||||
}
|
||||
})
|
||||
|
||||
@@ -900,7 +900,7 @@ function Show-MainWindow {
|
||||
# If WhatIf mode is enabled, notify the user that no changes will be made
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
$window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Loaded, [action]{
|
||||
Show-MessageBox -Message "WhatIf mode is enabled. The script will not make any changes to your system in this mode.`n`nYou can observe the actions that would be taken by the script in the console output." -Title 'WhatIf Mode' -Button 'OK' -Icon 'Information' -Owner $window
|
||||
Show-MessageBox -Message (Get-Translation -Key 'WhatIfModeMessage') -Title (Get-Translation -Key 'WhatIfModeTitle') -Button 'OK' -Icon 'Information' -Owner $window
|
||||
}) | Out-Null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Shows a themed Windows 11-style message box.
|
||||
|
||||
.DESCRIPTION
|
||||
Falls back to a plain native MessageBox if the themed dialog's own schema fails to
|
||||
localize (a bad %LANG:% marker in MessageBox.xaml, not a missing app-wide translation),
|
||||
so a broken schema can't take down every error dialog in the app at once.
|
||||
|
||||
.OUTPUTS
|
||||
System.String. One of 'OK', 'Cancel', 'Yes', or 'No', matching the pressed button
|
||||
in both the themed dialog and the native fallback.
|
||||
#>
|
||||
function Show-MessageBox {
|
||||
param (
|
||||
@@ -50,6 +59,41 @@ function Show-MessageBox {
|
||||
|
||||
# Load XAML from file
|
||||
$xaml = Get-Content -Path $script:MessageBoxSchema -Raw
|
||||
try {
|
||||
$xaml = ConvertTo-LocalizedXaml -Xaml $xaml
|
||||
}
|
||||
catch {
|
||||
# The themed dialog depends on the schema's own %LANG:% markers resolving. If that ever
|
||||
# breaks (a bad marker added to MessageBoxSchema, not a missing app-wide translation - that
|
||||
# never reaches this function), fall back to a plain native MessageBox rather than losing
|
||||
# the ability to show any error dialog at all.
|
||||
Write-Warning "Falling back to a native message box, themed dialog failed to localize: $($_.Exception.Message)"
|
||||
|
||||
# Hide overlay before showing the fallback (only if this dialog was the one that showed it)
|
||||
if ($overlay -and -not $overlayWasAlreadyVisible) {
|
||||
try {
|
||||
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' })
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
$nativeButton = switch ($Button) {
|
||||
'OKCancel' { [System.Windows.MessageBoxButton]::OKCancel }
|
||||
'YesNo' { [System.Windows.MessageBoxButton]::YesNo }
|
||||
default { [System.Windows.MessageBoxButton]::OK }
|
||||
}
|
||||
$nativeIcon = switch ($Icon) {
|
||||
'Warning' { [System.Windows.MessageBoxImage]::Warning }
|
||||
'Error' { [System.Windows.MessageBoxImage]::Error }
|
||||
'Question' { [System.Windows.MessageBoxImage]::Question }
|
||||
'Information' { [System.Windows.MessageBoxImage]::Information }
|
||||
'Success' { [System.Windows.MessageBoxImage]::Information }
|
||||
default { [System.Windows.MessageBoxImage]::None }
|
||||
}
|
||||
|
||||
$nativeResult = [System.Windows.MessageBox]::Show($Message, $Title, $nativeButton, $nativeIcon)
|
||||
return [string]$nativeResult
|
||||
}
|
||||
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
|
||||
try {
|
||||
$msgWindow = [System.Windows.Markup.XamlReader]::Load($reader)
|
||||
@@ -121,22 +165,22 @@ function Show-MessageBox {
|
||||
# Configure buttons - store result in window's Tag property
|
||||
switch ($Button) {
|
||||
'OK' {
|
||||
$button1.Content = 'OK'
|
||||
$button1.Content = Get-Translation -Key 'MessageBoxOk'
|
||||
$button1.Add_Click({ $msgWindow.Tag = 'OK'; $msgWindow.Close() })
|
||||
$button2.Visibility = 'Collapsed'
|
||||
# Right-align sole button by moving it to column 1
|
||||
[System.Windows.Controls.Grid]::SetColumn($button1, 1)
|
||||
}
|
||||
'OKCancel' {
|
||||
$button1.Content = 'OK'
|
||||
$button2.Content = 'Cancel'
|
||||
$button1.Content = Get-Translation -Key 'MessageBoxOk'
|
||||
$button2.Content = Get-Translation -Key 'MessageBoxCancel'
|
||||
$button1.Add_Click({ $msgWindow.Tag = 'OK'; $msgWindow.Close() })
|
||||
$button2.Add_Click({ $msgWindow.Tag = 'Cancel'; $msgWindow.Close() })
|
||||
$button2.Visibility = 'Visible'
|
||||
}
|
||||
'YesNo' {
|
||||
$button1.Content = 'Yes'
|
||||
$button2.Content = 'No'
|
||||
$button1.Content = Get-Translation -Key 'MessageBoxYes'
|
||||
$button2.Content = Get-Translation -Key 'MessageBoxNo'
|
||||
$button1.Add_Click({ $msgWindow.Tag = 'Yes'; $msgWindow.Close() })
|
||||
$button2.Add_Click({ $msgWindow.Tag = 'No'; $msgWindow.Close() })
|
||||
$button2.Visibility = 'Visible'
|
||||
|
||||
@@ -44,422 +44,429 @@ function Show-RestoreBackupDialog {
|
||||
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))
|
||||
# Everything from here through ShowDialog() sits inside one outer try/finally. The overlay
|
||||
# was already made visible above, and nothing hides it again until ShowDialog() returns, so
|
||||
# a failure anywhere in schema loading, control wiring, or event registration would otherwise
|
||||
# leave the owner window permanently dimmed.
|
||||
try {
|
||||
$window = [System.Windows.Markup.XamlReader]::Load($reader)
|
||||
}
|
||||
finally {
|
||||
$reader.Close()
|
||||
}
|
||||
$schemaPath = $script:RestoreBackupWindowSchema
|
||||
if (-not $schemaPath -or -not (Test-Path $schemaPath)) {
|
||||
throw 'Restore backup window schema file could not be found.'
|
||||
}
|
||||
|
||||
if ($ownerWindow) {
|
||||
$xaml = Get-Content -Path $schemaPath -Raw
|
||||
$xaml = ConvertTo-LocalizedXaml -Xaml $xaml
|
||||
|
||||
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
|
||||
try {
|
||||
$window.Owner = $ownerWindow
|
||||
$window = [System.Windows.Markup.XamlReader]::Load($reader)
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
try {
|
||||
Set-WindowThemeResources -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')
|
||||
$reappliedSeparator = $window.FindName('ReappliedSeparator')
|
||||
$reappliedPanel = $window.FindName('ReappliedPanel')
|
||||
$reappliedFeaturesItemsControl = $window.FindName('ReappliedFeaturesItemsControl')
|
||||
$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 = Get-FriendlyRegistryBackupTarget -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'
|
||||
$reappliedSeparator.Visibility = 'Collapsed'
|
||||
$reappliedPanel.Visibility = 'Collapsed'
|
||||
$nonRevertibleSeparator.Visibility = 'Collapsed'
|
||||
$nonRevertiblePanel.Visibility = 'Collapsed'
|
||||
$introInfoPanel.Visibility = 'Collapsed'
|
||||
$overviewPanel.Visibility = 'Visible'
|
||||
$restoreModeTabs.SelectedIndex = 1
|
||||
}
|
||||
|
||||
$updateStartMenuOverviewPanel = {
|
||||
if ($state.WizardStep -ne 'StartMenu') {
|
||||
return
|
||||
finally {
|
||||
$reader.Close()
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
# Show intro panel so user can configure scope & auto-detect
|
||||
$startMenuAutoBackupCheck.IsChecked = $true
|
||||
$state.SelectedStartMenuBackupFilePath = $null
|
||||
& $refreshStartMenuUi
|
||||
}
|
||||
|
||||
$showRegistryOverview = {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$SelectedBackup,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SelectedBackupFilePath
|
||||
)
|
||||
|
||||
$createdText = if ([string]::IsNullOrWhiteSpace($SelectedBackup.CreatedAt)) {
|
||||
'Unknown'
|
||||
}
|
||||
else {
|
||||
if ($ownerWindow) {
|
||||
try {
|
||||
[DateTime]::Parse($SelectedBackup.CreatedAt).ToString('yyyy-MM-dd HH:mm')
|
||||
}
|
||||
catch {
|
||||
$SelectedBackup.CreatedAt
|
||||
$window.Owner = $ownerWindow
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
$selectedForwardFeatureIds = @(Get-SelectedForwardFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
|
||||
$selectedUndoFeatureIds = @(Get-SelectedUndoFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
|
||||
|
||||
$seenForwardFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($featureId in $selectedForwardFeatureIds) {
|
||||
[void]$seenForwardFeatureIds.Add([string]$featureId)
|
||||
}
|
||||
|
||||
$filteredUndoFeatureIds = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($featureId in $selectedUndoFeatureIds) {
|
||||
if ($seenForwardFeatureIds.Contains([string]$featureId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$filteredUndoFeatureIds.Add([string]$featureId)
|
||||
}
|
||||
|
||||
$forwardFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds $selectedForwardFeatureIds -Features $script:Features
|
||||
$undoFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds @($filteredUndoFeatureIds.ToArray()) -Features $script:Features
|
||||
$combinedFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds (Get-SelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup) -Features $script:Features
|
||||
|
||||
$revertibleFeaturesList = @($forwardFeatureLists.Revertible)
|
||||
$reappliedFeaturesList = @($undoFeatureLists.Revertible)
|
||||
$nonRevertibleFeaturesList = @($combinedFeatureLists.NonRevertible)
|
||||
Write-Host "Backup overview prepared. Reverted=$($revertibleFeaturesList.Count), ReApplied=$($reappliedFeaturesList.Count), NonRevertible=$($nonRevertibleFeaturesList.Count)"
|
||||
|
||||
if ($revertibleFeaturesList.Count -eq 0 -and $reappliedFeaturesList.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 = Get-FriendlyRegistryBackupTarget -Target ([string]$SelectedBackup.Target)
|
||||
$featuresItemsControl.ItemsSource = $revertibleFeaturesList
|
||||
$overviewFeaturesSection.Visibility = if ($revertibleFeaturesList.Count -gt 0) { 'Visible' } else { 'Collapsed' }
|
||||
$reappliedFeaturesItemsControl.ItemsSource = $reappliedFeaturesList
|
||||
if ($reappliedFeaturesList.Count -gt 0) { $reappliedPanel.Visibility = 'Visible' } else { $reappliedPanel.Visibility = 'Collapsed' }
|
||||
if ($revertibleFeaturesList.Count -gt 0 -and $reappliedFeaturesList.Count -gt 0) { $reappliedSeparator.Visibility = 'Visible' } else { $reappliedSeparator.Visibility = 'Collapsed' }
|
||||
$overviewSummaryText.Visibility = 'Collapsed'
|
||||
$nonRevertibleFeaturesItemsControl.ItemsSource = $nonRevertibleFeaturesList
|
||||
|
||||
$hasNonRevertibleItems = ($nonRevertibleFeaturesList.Count -gt 0)
|
||||
if ($hasNonRevertibleItems) { $nonRevertiblePanel.Visibility = 'Visible' } else { $nonRevertiblePanel.Visibility = 'Collapsed' }
|
||||
if ($hasNonRevertibleItems -and ($revertibleFeaturesList.Count -gt 0 -or $reappliedFeaturesList.Count -gt 0)) { $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'
|
||||
$openDialog.DefaultExt = '.json'
|
||||
$openDialog.InitialDirectory = $script:RegistryBackupsPath
|
||||
|
||||
if ($openDialog.ShowDialog($window) -ne $true) {
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "Backup file selected: $($openDialog.FileName)"
|
||||
|
||||
try {
|
||||
$selectedBackup = Import-RegistryBackup -FilePath $openDialog.FileName
|
||||
Set-WindowThemeResources -window $window -usesDarkMode $usesDarkMode
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (-not (& $showRegistryOverview -SelectedBackup $selectedBackup -SelectedBackupFilePath $openDialog.FileName)) {
|
||||
$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')
|
||||
$reappliedSeparator = $window.FindName('ReappliedSeparator')
|
||||
$reappliedPanel = $window.FindName('ReappliedPanel')
|
||||
$reappliedFeaturesItemsControl = $window.FindName('ReappliedFeaturesItemsControl')
|
||||
$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) { Get-Translation -Key 'RestoreStartMenuSummaryScopeAllUsers' } else { Get-Translation -Key 'RestoreStartMenuSummaryScopeCurrentUser' }
|
||||
|
||||
return [PSCustomObject]@{
|
||||
Scope = $scopeValue
|
||||
Target = $scopeValue
|
||||
SummaryText = $summaryScopeText
|
||||
}
|
||||
}
|
||||
|
||||
$showStartMenuIntroState = {
|
||||
$backupFileText.Text = Get-Translation -Key 'RestoreFileNotSelected'
|
||||
$backupCreatedText.Text = Get-Translation -Key 'RestoreCreatedUnavailable'
|
||||
$overviewSummaryText.Visibility = 'Collapsed'
|
||||
$overviewPanel.Visibility = 'Collapsed'
|
||||
$startMenuIntroPanel.Visibility = 'Visible'
|
||||
$restoreModeTabs.SelectedIndex = 2
|
||||
}
|
||||
|
||||
$showStartMenuOverviewState = {
|
||||
param([string]$BackupFilePath)
|
||||
|
||||
$scopeInfo = & $getStartMenuScopeInfo
|
||||
$backupTargetText.Text = Get-FriendlyRegistryBackupTarget -Target $scopeInfo.Target
|
||||
$overviewSummaryText.Text = Get-Translation -Key 'RestoreStartMenuSummary' -FormatArgs @($scopeInfo.SummaryText)
|
||||
$backupFileText.Text = Split-Path -Path $BackupFilePath -Leaf
|
||||
|
||||
$createdText = Get-Translation -Key 'RestoreCreatedUnknown'
|
||||
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'
|
||||
$reappliedSeparator.Visibility = 'Collapsed'
|
||||
$reappliedPanel.Visibility = 'Collapsed'
|
||||
$nonRevertibleSeparator.Visibility = 'Collapsed'
|
||||
$nonRevertiblePanel.Visibility = 'Collapsed'
|
||||
$introInfoPanel.Visibility = 'Collapsed'
|
||||
$overviewPanel.Visibility = 'Visible'
|
||||
$restoreModeTabs.SelectedIndex = 1
|
||||
}
|
||||
|
||||
$updateStartMenuOverviewPanel = {
|
||||
if ($state.WizardStep -ne 'StartMenu') {
|
||||
return
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Show-MessageBox -Owner $window -Title 'Invalid Backup File' -Message "The selected file could not be loaded:`n$($_.Exception.Message)" -Button 'OK' -Icon 'Error' | Out-Null
|
||||
return
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)) {
|
||||
& $showStartMenuIntroState
|
||||
return
|
||||
}
|
||||
|
||||
& $showStartMenuOverviewState $state.SelectedStartMenuBackupFilePath
|
||||
}
|
||||
|
||||
$state.SelectedRegistryBackup = $selectedBackup
|
||||
$primaryActionBtn.Content = 'Restore from backup'
|
||||
}
|
||||
$updateStartMenuPrimaryActionText = {
|
||||
if ($state.WizardStep -ne 'StartMenu') {
|
||||
return
|
||||
}
|
||||
|
||||
$handleStartMenuPrimaryAction = {
|
||||
$scope = (& $getStartMenuScopeInfo).Scope
|
||||
$useManualBackupFile = -not ($startMenuAutoBackupCheck.IsChecked -eq $true)
|
||||
$isAutoBackupEnabled = ($startMenuAutoBackupCheck.IsChecked -eq $true)
|
||||
$hasSelectedManualFile = -not [string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)
|
||||
if ($isAutoBackupEnabled -or $hasSelectedManualFile) {
|
||||
$primaryActionBtn.Content = Get-Translation -Key 'RestorePrimaryActionRestoreBackup'
|
||||
}
|
||||
else {
|
||||
$primaryActionBtn.Content = Get-Translation -Key 'RestorePrimaryActionSelectBackupFile'
|
||||
}
|
||||
}
|
||||
|
||||
$refreshStartMenuUi = {
|
||||
& $updateStartMenuOverviewPanel
|
||||
& $updateStartMenuPrimaryActionText
|
||||
}
|
||||
|
||||
$enterSelectTypeStep = {
|
||||
$titleText.Text = Get-Translation -Key 'RestoreWindowTitle'
|
||||
$restoreModeTabs.SelectedIndex = 0
|
||||
$backBtn.Visibility = 'Visible'
|
||||
$backBtn.Content = Get-Translation -Key 'RestoreBackBtnCancel'
|
||||
$primaryActionBtn.Visibility = 'Collapsed'
|
||||
$chooseRegistryBtn.IsDefault = $true
|
||||
$primaryActionBtn.IsDefault = $false
|
||||
}
|
||||
|
||||
$enterRegistryStep = {
|
||||
$titleText.Text = Get-Translation -Key 'RestoreRegistryStepTitle'
|
||||
$restoreModeTabs.SelectedIndex = 1
|
||||
$introInfoPanel.Visibility = 'Visible'
|
||||
$overviewPanel.Visibility = 'Collapsed'
|
||||
$overviewFeaturesSection.Visibility = 'Visible'
|
||||
$overviewSummaryText.Visibility = 'Collapsed'
|
||||
$backBtn.Visibility = 'Visible'
|
||||
$backBtn.Content = Get-Translation -Key 'RestoreBackBtnBack'
|
||||
$primaryActionBtn.Visibility = 'Visible'
|
||||
$primaryActionBtn.Content = Get-Translation -Key 'RestorePrimaryActionSelectBackupFile'
|
||||
$primaryActionBtn.IsDefault = $true
|
||||
$chooseRegistryBtn.IsDefault = $false
|
||||
}
|
||||
|
||||
$enterStartMenuStep = {
|
||||
$titleText.Text = Get-Translation -Key 'RestoreStartMenuStepTitle'
|
||||
$restoreModeTabs.SelectedIndex = 2
|
||||
$backBtn.Visibility = 'Visible'
|
||||
$backBtn.Content = Get-Translation -Key 'RestoreBackBtnBack'
|
||||
$primaryActionBtn.Visibility = 'Visible'
|
||||
$primaryActionBtn.IsDefault = $true
|
||||
$chooseRegistryBtn.IsDefault = $false
|
||||
|
||||
# Show intro panel so user can configure scope & auto-detect
|
||||
$startMenuAutoBackupCheck.IsChecked = $true
|
||||
$state.SelectedStartMenuBackupFilePath = $null
|
||||
& $refreshStartMenuUi
|
||||
}
|
||||
|
||||
$showRegistryOverview = {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$SelectedBackup,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SelectedBackupFilePath
|
||||
)
|
||||
|
||||
$createdText = if ([string]::IsNullOrWhiteSpace($SelectedBackup.CreatedAt)) {
|
||||
Get-Translation -Key 'RestoreCreatedUnknown'
|
||||
}
|
||||
else {
|
||||
try {
|
||||
[DateTime]::Parse($SelectedBackup.CreatedAt).ToString('yyyy-MM-dd HH:mm')
|
||||
}
|
||||
catch {
|
||||
$SelectedBackup.CreatedAt
|
||||
}
|
||||
}
|
||||
|
||||
$selectedForwardFeatureIds = @(Get-SelectedForwardFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
|
||||
$selectedUndoFeatureIds = @(Get-SelectedUndoFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
|
||||
|
||||
$seenForwardFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($featureId in $selectedForwardFeatureIds) {
|
||||
[void]$seenForwardFeatureIds.Add([string]$featureId)
|
||||
}
|
||||
|
||||
$filteredUndoFeatureIds = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($featureId in $selectedUndoFeatureIds) {
|
||||
if ($seenForwardFeatureIds.Contains([string]$featureId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$filteredUndoFeatureIds.Add([string]$featureId)
|
||||
}
|
||||
|
||||
$forwardFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds $selectedForwardFeatureIds -Features $script:Features
|
||||
$undoFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds @($filteredUndoFeatureIds.ToArray()) -Features $script:Features
|
||||
$combinedFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds (Get-SelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup) -Features $script:Features
|
||||
|
||||
$revertibleFeaturesList = @($forwardFeatureLists.Revertible)
|
||||
$reappliedFeaturesList = @($undoFeatureLists.Revertible)
|
||||
$nonRevertibleFeaturesList = @($combinedFeatureLists.NonRevertible)
|
||||
Write-Host "Backup overview prepared. Reverted=$($revertibleFeaturesList.Count), ReApplied=$($reappliedFeaturesList.Count), NonRevertible=$($nonRevertibleFeaturesList.Count)"
|
||||
|
||||
if ($revertibleFeaturesList.Count -eq 0 -and $reappliedFeaturesList.Count -eq 0) {
|
||||
throw (Get-Translation -Key 'RestoreNoRestorableChangesError')
|
||||
}
|
||||
|
||||
$backupFileText.Text = Split-Path $SelectedBackupFilePath -Leaf
|
||||
$backupCreatedText.Text = $createdText
|
||||
$backupTargetText.Text = Get-FriendlyRegistryBackupTarget -Target ([string]$SelectedBackup.Target)
|
||||
$featuresItemsControl.ItemsSource = $revertibleFeaturesList
|
||||
$overviewFeaturesSection.Visibility = if ($revertibleFeaturesList.Count -gt 0) { 'Visible' } else { 'Collapsed' }
|
||||
$reappliedFeaturesItemsControl.ItemsSource = $reappliedFeaturesList
|
||||
if ($reappliedFeaturesList.Count -gt 0) { $reappliedPanel.Visibility = 'Visible' } else { $reappliedPanel.Visibility = 'Collapsed' }
|
||||
if ($revertibleFeaturesList.Count -gt 0 -and $reappliedFeaturesList.Count -gt 0) { $reappliedSeparator.Visibility = 'Visible' } else { $reappliedSeparator.Visibility = 'Collapsed' }
|
||||
$overviewSummaryText.Visibility = 'Collapsed'
|
||||
$nonRevertibleFeaturesItemsControl.ItemsSource = $nonRevertibleFeaturesList
|
||||
|
||||
$hasNonRevertibleItems = ($nonRevertibleFeaturesList.Count -gt 0)
|
||||
if ($hasNonRevertibleItems) { $nonRevertiblePanel.Visibility = 'Visible' } else { $nonRevertiblePanel.Visibility = 'Collapsed' }
|
||||
if ($hasNonRevertibleItems -and ($revertibleFeaturesList.Count -gt 0 -or $reappliedFeaturesList.Count -gt 0)) { $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
|
||||
}
|
||||
|
||||
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'
|
||||
$openDialog.Title = Get-Translation -Key 'RestoreSelectRegistryFileDialogTitle'
|
||||
$openDialog.Filter = 'Registry backup (*.json)|*.json'
|
||||
$openDialog.DefaultExt = '.json'
|
||||
$openDialog.InitialDirectory = $script:RegistryBackupsPath
|
||||
|
||||
if ($openDialog.ShowDialog($window) -ne $true) {
|
||||
return
|
||||
}
|
||||
|
||||
$state.SelectedStartMenuBackupFilePath = $openDialog.FileName
|
||||
Write-Host "Selected Start Menu backup file: $($state.SelectedStartMenuBackupFilePath)"
|
||||
& $refreshStartMenuUi
|
||||
return
|
||||
}
|
||||
Write-Host "Backup file selected: $($openDialog.FileName)"
|
||||
|
||||
if (-not $useManualBackupFile) {
|
||||
$scopeInfo = & $getStartMenuScopeInfo
|
||||
$autoBackupPath = Get-StartMenuBackupPath -Scope $scopeInfo.Scope
|
||||
if ($null -eq $autoBackupPath) {
|
||||
$scopeText = $scopeInfo.SummaryText
|
||||
Show-MessageBox -Owner $window -Title 'No Backup Found' -Message "No Start Menu backup file was found for $scopeText. Uncheck 'Automatically find Start Menu backup' to select a backup file manually." -Button 'OK' -Icon 'Warning' | Out-Null
|
||||
try {
|
||||
$selectedBackup = Import-RegistryBackup -FilePath $openDialog.FileName
|
||||
|
||||
if (-not (& $showRegistryOverview -SelectedBackup $selectedBackup -SelectedBackupFilePath $openDialog.FileName)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Show-MessageBox -Owner $window -Title (Get-Translation -Key 'RestoreInvalidBackupTitle') -Message (Get-Translation -Key 'RestoreInvalidBackupMessage' -FormatArgs @($_.Exception.Message)) -Button 'OK' -Icon 'Error' | Out-Null
|
||||
return
|
||||
}
|
||||
$state.SelectedStartMenuBackupFilePath = if ($scopeInfo.Scope -eq 'CurrentUser') { $autoBackupPath } else { $null }
|
||||
|
||||
$state.SelectedRegistryBackup = $selectedBackup
|
||||
$primaryActionBtn.Content = Get-Translation -Key 'RestorePrimaryActionRestoreFromBackup'
|
||||
}
|
||||
|
||||
$window.Tag = @{
|
||||
Result = 'Restore-StartMenu'
|
||||
StartMenuScope = $scope
|
||||
UseManualBackupFile = $useManualBackupFile
|
||||
BackupFilePath = $state.SelectedStartMenuBackupFilePath
|
||||
$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 = Get-Translation -Key 'RestoreSelectStartMenuFileDialogTitle'
|
||||
$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
|
||||
}
|
||||
|
||||
if (-not $useManualBackupFile) {
|
||||
$scopeInfo = & $getStartMenuScopeInfo
|
||||
$autoBackupPath = Get-StartMenuBackupPath -Scope $scopeInfo.Scope
|
||||
if ($null -eq $autoBackupPath) {
|
||||
$scopeText = $scopeInfo.SummaryText
|
||||
Show-MessageBox -Owner $window -Title (Get-Translation -Key 'RestoreNoBackupFoundTitle') -Message (Get-Translation -Key 'RestoreNoBackupFoundMessage' -FormatArgs @($scopeText)) -Button 'OK' -Icon 'Warning' | Out-Null
|
||||
return
|
||||
}
|
||||
$state.SelectedStartMenuBackupFilePath = if ($scopeInfo.Scope -eq 'CurrentUser') { $autoBackupPath } else { $null }
|
||||
}
|
||||
|
||||
$window.Tag = @{
|
||||
Result = 'Restore-StartMenu'
|
||||
StartMenuScope = $scope
|
||||
UseManualBackupFile = $useManualBackupFile
|
||||
BackupFilePath = $state.SelectedStartMenuBackupFilePath
|
||||
}
|
||||
$window.DialogResult = $true
|
||||
$window.Close()
|
||||
}
|
||||
$window.DialogResult = $true
|
||||
$window.Close()
|
||||
}
|
||||
|
||||
$setWizardStep = {
|
||||
param([string]$step)
|
||||
$setWizardStep = {
|
||||
param([string]$step)
|
||||
|
||||
$state.WizardStep = $step
|
||||
$state.WizardStep = $step
|
||||
|
||||
switch ($step) {
|
||||
'SelectType' { & $enterSelectTypeStep }
|
||||
'Registry' { & $enterRegistryStep }
|
||||
'StartMenu' { & $enterStartMenuStep }
|
||||
switch ($step) {
|
||||
'SelectType' { & $enterSelectTypeStep }
|
||||
'Registry' { & $enterRegistryStep }
|
||||
'StartMenu' { & $enterStartMenuStep }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$startMenuAutoBackupCheck.Add_Checked({
|
||||
$state.SelectedStartMenuBackupFilePath = $null
|
||||
& $refreshStartMenuUi
|
||||
})
|
||||
$startMenuAutoBackupCheck.Add_Unchecked({
|
||||
& $refreshStartMenuUi
|
||||
})
|
||||
$startMenuAutoBackupCheck.Add_Checked({
|
||||
$state.SelectedStartMenuBackupFilePath = $null
|
||||
& $refreshStartMenuUi
|
||||
})
|
||||
$startMenuAutoBackupCheck.Add_Unchecked({
|
||||
& $refreshStartMenuUi
|
||||
})
|
||||
|
||||
$startMenuScopeCombo.Add_SelectionChanged({
|
||||
$state.SelectedStartMenuBackupFilePath = $null
|
||||
& $refreshStartMenuUi
|
||||
})
|
||||
$startMenuScopeCombo.Add_SelectionChanged({
|
||||
$state.SelectedStartMenuBackupFilePath = $null
|
||||
& $refreshStartMenuUi
|
||||
})
|
||||
|
||||
$nonRevertibleWikiLink.Add_MouseLeftButtonUp({
|
||||
try {
|
||||
Start-Process 'https://github.com/Raphire/Win11Debloat/wiki/Reverting-Changes' | Out-Null
|
||||
}
|
||||
catch { }
|
||||
})
|
||||
$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') {
|
||||
$closeBtn.Add_Click({
|
||||
$window.Tag = New-RestoreDialogState
|
||||
$window.DialogResult = $false
|
||||
$window.Close()
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
if ($state.WizardStep -eq 'Registry') {
|
||||
$state.SelectedRegistryBackup = $null
|
||||
}
|
||||
$chooseRegistryBtn.Add_Click({ & $setWizardStep 'Registry' })
|
||||
$chooseStartMenuBtn.Add_Click({ & $setWizardStep 'StartMenu' })
|
||||
|
||||
if ($state.WizardStep -eq 'StartMenu') {
|
||||
$state.SelectedStartMenuBackupFilePath = $null
|
||||
$startMenuAutoBackupCheck.IsChecked = $true
|
||||
}
|
||||
$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'
|
||||
})
|
||||
|
||||
$primaryActionBtn.Add_Click({
|
||||
switch ($state.WizardStep) {
|
||||
'Registry' { & $handleRegistryPrimaryAction }
|
||||
'StartMenu' { & $handleStartMenuPrimaryAction }
|
||||
try {
|
||||
$null = $window.ShowDialog()
|
||||
}
|
||||
})
|
||||
|
||||
$window.Add_KeyDown({
|
||||
param($source, $e)
|
||||
if ($e.Key -eq 'Escape') {
|
||||
$window.Tag = New-RestoreDialogState
|
||||
$window.DialogResult = $false
|
||||
$window.Close()
|
||||
catch {
|
||||
$innerMessage = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { 'None' }
|
||||
throw "Failed to show restore backup dialog. Error: $($_.Exception.Message) Inner: $innerMessage"
|
||||
}
|
||||
})
|
||||
|
||||
& $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) {
|
||||
|
||||
@@ -35,10 +35,10 @@ function Show-RestoreBackupWindow {
|
||||
if ($restoreOpResult -and $restoreOpResult.Result) {
|
||||
$restoreResult.RestoredRegistry = $true
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
$successMessage = '[WhatIf] Registry backup would be restored (no changes made).'
|
||||
$successMessage = Get-Translation -Key 'RestoreRegistryWhatIfMessage'
|
||||
}
|
||||
else {
|
||||
$successMessage = 'Registry backup restored successfully. Some changes may require a restart to take effect.'
|
||||
$successMessage = Get-Translation -Key 'RestoreRegistrySuccessMessage'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,22 +70,22 @@ function Show-RestoreBackupWindow {
|
||||
|
||||
if ($successCount -eq 0) {
|
||||
$errorSummary = ($resultEntries | ForEach-Object { $_.Message }) -join [Environment]::NewLine
|
||||
throw "Failed to restore the Start Menu backup.`n$errorSummary"
|
||||
throw (Get-Translation -Key 'RestoreStartMenuFailedMessage' -FormatArgs @($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"
|
||||
$warningMessage = Get-Translation -Key 'RestoreStartMenuPartialSuccessMessage' -FormatArgs @($successCount, $failureSummary)
|
||||
}
|
||||
else {
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
$successMessage = '[WhatIf] Start Menu backup would be restored (no changes made).'
|
||||
$successMessage = Get-Translation -Key 'RestoreStartMenuWhatIfMessage'
|
||||
}
|
||||
elseif ($scope -eq 'AllUsers') {
|
||||
$successMessage = "The Start Menu backup was successfully restored for all users. The changes will apply the next time users sign in."
|
||||
$successMessage = Get-Translation -Key 'RestoreStartMenuAllUsersSuccessMessage'
|
||||
}
|
||||
else {
|
||||
$successMessage = "The Start Menu backup was successfully restored for the current user. The changes will apply the next time you sign in."
|
||||
$successMessage = Get-Translation -Key 'RestoreStartMenuCurrentUserSuccessMessage'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,19 +94,19 @@ function Show-RestoreBackupWindow {
|
||||
|
||||
if ($warningMessage) {
|
||||
Write-Host "$warningMessage"
|
||||
Show-MessageBox -Title 'Backup Restored' -Message $warningMessage -Icon Warning
|
||||
Show-MessageBox -Title (Get-Translation -Key 'RestoreBackupRestoredTitle') -Message $warningMessage -Icon Warning
|
||||
}
|
||||
elseif ($successMessage) {
|
||||
Write-Host "$successMessage"
|
||||
Show-MessageBox -Title 'Backup Restored' -Message $successMessage -Icon Success
|
||||
Show-MessageBox -Title (Get-Translation -Key 'RestoreBackupRestoredTitle') -Message $successMessage -Icon Success
|
||||
}
|
||||
|
||||
return $restoreResult
|
||||
}
|
||||
catch {
|
||||
$errorMessage = if ($_.Exception.Message) { $_.Exception.Message } else { 'An unexpected error occurred.' }
|
||||
$errorMessage = if ($_.Exception.Message) { $_.Exception.Message } else { Get-Translation -Key 'RestoreUnexpectedError' }
|
||||
Write-Error "Restore operation failed: $errorMessage"
|
||||
Show-MessageBox -Title 'Error' -Message "Restore failed: $errorMessage" -Icon Error
|
||||
Show-MessageBox -Title (Get-Translation -Key 'ErrorTitle') -Message (Get-Translation -Key 'RestoreOperationFailedMessage' -FormatArgs @($errorMessage)) -Icon Error
|
||||
return [PSCustomObject]@{
|
||||
RestoredRegistry = $false
|
||||
RestoredStartMenu = $false
|
||||
|
||||
@@ -6,6 +6,7 @@ param (
|
||||
[switch]$Silent,
|
||||
[switch]$Sysprep,
|
||||
[string]$LogPath,
|
||||
[string]$Language,
|
||||
[string]$User,
|
||||
[Alias('NoRestartExplorer')]
|
||||
[switch]$SkipExplorerRestart,
|
||||
|
||||
@@ -15,7 +15,7 @@ function Confirm-UnsafeAppRemoval {
|
||||
|
||||
# Microsoft Store warning
|
||||
if ($SelectedApps -contains "Microsoft.WindowsStore") {
|
||||
$result = Show-MessageBox -Message 'Are you sure that you wish to uninstall the Microsoft Store? This app cannot easily be reinstalled.' -Title 'Are you sure?' -Button 'YesNo' -Icon 'Warning' -Owner $Owner
|
||||
$result = Show-MessageBox -Message (Get-Translation -Key 'ConfirmRemoveStoreMessage') -Title (Get-Translation -Key 'ConfirmTitle') -Button 'YesNo' -Icon 'Warning' -Owner $Owner
|
||||
|
||||
if ($result -ne 'Yes') {
|
||||
return $false
|
||||
@@ -24,7 +24,7 @@ function Confirm-UnsafeAppRemoval {
|
||||
|
||||
# Windows Terminal warning
|
||||
if ($SelectedApps -contains "Microsoft.WindowsTerminal") {
|
||||
$result = Show-MessageBox -Message 'Are you sure that you wish to remove Windows Terminal? Windows Terminal is the default command-line app for Windows. Ensure you are not running Win11Debloat via Windows Terminal before proceeding to avoid a mid-process failure.' -Title 'Are you sure?' -Button 'YesNo' -Icon 'Warning' -Owner $Owner
|
||||
$result = Show-MessageBox -Message (Get-Translation -Key 'ConfirmRemoveTerminalMessage') -Title (Get-Translation -Key 'ConfirmTitle') -Button 'YesNo' -Icon 'Warning' -Owner $Owner
|
||||
|
||||
if ($result -ne 'Yes') {
|
||||
return $false
|
||||
|
||||
@@ -10,37 +10,37 @@ function Get-FriendlyRegistryBackupTarget {
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Target)) {
|
||||
return 'Unknown'
|
||||
return Get-Translation -Key 'RestoreTargetUnknown'
|
||||
}
|
||||
|
||||
if ($Target -eq 'DefaultUserProfile') {
|
||||
return 'Default user profile'
|
||||
return Get-Translation -Key 'RestoreTargetDefaultUserProfile'
|
||||
}
|
||||
|
||||
if ($Target -eq 'CurrentUser') {
|
||||
return 'Current user'
|
||||
return Get-Translation -Key 'RestoreTargetCurrentUser'
|
||||
}
|
||||
|
||||
if ($Target -eq 'AllUsers') {
|
||||
return 'All users'
|
||||
return Get-Translation -Key 'RestoreTargetAllUsers'
|
||||
}
|
||||
|
||||
if ($Target -like 'CurrentUser:*') {
|
||||
$userName = $Target.Substring(12)
|
||||
if ([string]::IsNullOrWhiteSpace($userName)) {
|
||||
return 'Current user'
|
||||
return Get-Translation -Key 'RestoreTargetCurrentUser'
|
||||
}
|
||||
|
||||
return "Current user ($userName)"
|
||||
return Get-Translation -Key 'RestoreTargetCurrentUserNamed' -FormatArgs @($userName)
|
||||
}
|
||||
|
||||
if ($Target -like 'User:*') {
|
||||
$userName = $Target.Substring(5)
|
||||
if ([string]::IsNullOrWhiteSpace($userName)) {
|
||||
return 'User'
|
||||
return Get-Translation -Key 'RestoreTargetUser'
|
||||
}
|
||||
|
||||
return "User ($userName)"
|
||||
return Get-Translation -Key 'RestoreTargetUserNamed' -FormatArgs @($userName)
|
||||
}
|
||||
|
||||
return $Target
|
||||
|
||||
@@ -15,7 +15,7 @@ function Get-RebootFeatureLabels {
|
||||
if ($script:Features.ContainsKey($paramKey) -and $script:Features[$paramKey].RequiresReboot -eq $true) {
|
||||
$feature = $script:Features[$paramKey]
|
||||
$isUndo = $script:UndoParams.ContainsKey($paramKey)
|
||||
$displayLabel = if ($isUndo -and $feature.UndoLabel) { $feature.UndoLabel } else { $feature.Label }
|
||||
$displayLabel = if ($isUndo -and $feature.UndoLabel) { Get-Translation -Key $paramKey -Field 'UndoLabel' -Section 'Features' } else { Get-Translation -Key $paramKey -Field 'Label' -Section 'Features' }
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$displayLabel)) {
|
||||
[void]$rebootFeatureLabels.Add([string]$displayLabel)
|
||||
|
||||
@@ -14,24 +14,24 @@ function Test-ConfigConsistency {
|
||||
param($Config)
|
||||
|
||||
if (-not $Config) {
|
||||
return 'Configuration is empty or could not be read.'
|
||||
return Get-Translation -Key 'ConfigEmptyOrUnreadable'
|
||||
}
|
||||
|
||||
if (-not $Config.Version) {
|
||||
return 'Configuration is missing a Version field.'
|
||||
return Get-Translation -Key 'ConfigMissingVersion'
|
||||
}
|
||||
|
||||
if (-not $Config.Apps -and -not $Config.Tweaks -and -not $Config.Deployment) {
|
||||
return 'The configuration file contains no importable data.'
|
||||
return Get-Translation -Key 'ConfigNoImportableData'
|
||||
}
|
||||
|
||||
if ($null -ne $Config.Apps) {
|
||||
if ($Config.Apps -isnot [string] -and $Config.Apps -isnot [System.Collections.IEnumerable]) {
|
||||
return 'Configuration Apps entries must be strings.'
|
||||
return Get-Translation -Key 'ConfigAppsMustBeStrings'
|
||||
}
|
||||
foreach ($app in @($Config.Apps)) {
|
||||
if ($app -isnot [string]) {
|
||||
return 'Configuration Apps entries must be strings.'
|
||||
return Get-Translation -Key 'ConfigAppsMustBeStrings'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,13 +41,13 @@ function Test-ConfigConsistency {
|
||||
if ($null -eq $category) { continue }
|
||||
|
||||
if ($category -is [string] -or $category -isnot [System.Collections.IEnumerable]) {
|
||||
return "Configuration $categoryName entries must contain Name and Value properties."
|
||||
return Get-Translation -Key 'ConfigEntriesMustHaveNameAndValue' -FormatArgs @($categoryName)
|
||||
}
|
||||
foreach ($setting in @($category)) {
|
||||
$hasName = if ($setting -is [System.Collections.IDictionary]) { $setting.Contains('Name') } else { $null -ne $setting.PSObject.Properties['Name'] }
|
||||
$hasValue = if ($setting -is [System.Collections.IDictionary]) { $setting.Contains('Value') } else { $null -ne $setting.PSObject.Properties['Value'] }
|
||||
if (-not $setting -or -not $hasName -or -not $hasValue -or $setting.Name -isnot [string] -or [string]::IsNullOrWhiteSpace($setting.Name)) {
|
||||
return "Configuration $categoryName entries must contain Name and Value properties."
|
||||
return Get-Translation -Key 'ConfigEntriesMustHaveNameAndValue' -FormatArgs @($categoryName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,31 +65,31 @@ function Test-ConfigConsistency {
|
||||
$scopeIndex = $null
|
||||
if ($hasScope) {
|
||||
if (-not [int]::TryParse("$($lookup['AppRemovalScopeIndex'])", [ref]$scopeIndex) -or $scopeIndex -notin @(0, 1, 2)) {
|
||||
return 'AppRemovalScopeIndex must be a supported numeric value (0, 1, or 2).'
|
||||
return Get-Translation -Key 'ConfigInvalidAppRemovalScopeIndex'
|
||||
}
|
||||
}
|
||||
|
||||
$userIndex = $null
|
||||
if ($hasUser) {
|
||||
if (-not [int]::TryParse("$($lookup['UserSelectionIndex'])", [ref]$userIndex) -or $userIndex -notin @(0, 1, 2)) {
|
||||
return 'UserSelectionIndex must be a supported numeric value (0, 1, or 2).'
|
||||
return Get-Translation -Key 'ConfigInvalidUserSelectionIndex'
|
||||
}
|
||||
}
|
||||
|
||||
# "Current user only" (index 1) is only valid together with "Current User" (index 0)
|
||||
if ($hasScope -and $scopeIndex -eq 1) {
|
||||
if (-not $hasUser -or $userIndex -ne 0) {
|
||||
return "App removal scope 'Current user only' (AppRemovalScopeIndex 1) requires the deployment target 'Current User' (UserSelectionIndex 0)."
|
||||
return Get-Translation -Key 'ConfigScopeRequiresCurrentUserTarget'
|
||||
}
|
||||
}
|
||||
|
||||
# "Target user only" (index 2) is only valid together with "Other User" (index 1)
|
||||
if ($hasScope -and $scopeIndex -eq 2) {
|
||||
if (-not $hasUser -or $userIndex -ne 1) {
|
||||
return "App removal scope 'Target user only' (AppRemovalScopeIndex 2) requires the deployment target 'Other User' (UserSelectionIndex 1)."
|
||||
return Get-Translation -Key 'ConfigScopeRequiresOtherUserTarget'
|
||||
}
|
||||
if (-not $lookup.ContainsKey('OtherUsername') -or [string]::IsNullOrWhiteSpace("$($lookup['OtherUsername'])")) {
|
||||
return "App removal scope 'Target user only' (AppRemovalScopeIndex 2) requires an 'OtherUsername' value."
|
||||
return Get-Translation -Key 'ConfigScopeRequiresOtherUsername'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Validates a username for the "apply changes to another user" deployment target.
|
||||
|
||||
.DESCRIPTION
|
||||
Rejects an empty name, the current user's own name (that's the "Current User" option
|
||||
instead), and a name with no matching local profile, in that order.
|
||||
|
||||
.OUTPUTS
|
||||
PSCustomObject with IsValid (bool), UserName (the trimmed input), and Message (a
|
||||
translated string describing the validation result, success or failure alike).
|
||||
#>
|
||||
function Test-TargetUserName {
|
||||
param(
|
||||
[AllowNull()]
|
||||
@@ -11,7 +23,7 @@ function Test-TargetUserName {
|
||||
return [PSCustomObject]@{
|
||||
IsValid = $false
|
||||
UserName = $normalizedUserName
|
||||
Message = 'Please enter a username'
|
||||
Message = Get-Translation -Key 'UsernameValidationEmpty'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +31,7 @@ function Test-TargetUserName {
|
||||
return [PSCustomObject]@{
|
||||
IsValid = $false
|
||||
UserName = $normalizedUserName
|
||||
Message = "Cannot enter your own username, use 'Current User' option instead"
|
||||
Message = Get-Translation -Key 'UsernameValidationOwnUsername'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,13 +39,13 @@ function Test-TargetUserName {
|
||||
return [PSCustomObject]@{
|
||||
IsValid = $false
|
||||
UserName = $normalizedUserName
|
||||
Message = 'User not found, please enter a valid username'
|
||||
Message = Get-Translation -Key 'UsernameValidationNotFound'
|
||||
}
|
||||
}
|
||||
|
||||
return [PSCustomObject]@{
|
||||
IsValid = $true
|
||||
UserName = $normalizedUserName
|
||||
Message = "User found: $normalizedUserName"
|
||||
Message = Get-Translation -Key 'UsernameValidationFound' -FormatArgs @($normalizedUserName)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user