diff --git a/functions/private/Set-WinUtilTaskbarItem.ps1 b/functions/private/Set-WinUtilTaskbarItem.ps1 index 4bd4e2b5..40a69abc 100644 --- a/functions/private/Set-WinUtilTaskbarItem.ps1 +++ b/functions/private/Set-WinUtilTaskbarItem.ps1 @@ -4,20 +4,32 @@ function Set-WinUtilTaskbaritem { .SYNOPSIS Modifies the Taskbaritem of the WPF Form - .PARAMETER state & value + .PARAMETER value Value can be between 0 and 1, 0 being no progress done yet and 1 being fully completed - State can be 'None' > No progress, 'Indeterminate' > Without value, 'Normal' > when using value, 'Error' > Red (when using value), 'Paused' > Yellow (when using value) + Value does not affect item without setting the state to 'Normal', 'Error' or 'Paused' + Set-WinUtilTaskbaritem -value 0.5 + .PARAMETER state + State can be 'None' > No progress, 'Indeterminate' > inf. loading gray, 'Normal' > Gray, 'Error' > Red, 'Paused' > Yellow + no value needed: + - Set-WinUtilTaskbaritem -state "None" + - Set-WinUtilTaskbaritem -state "Indeterminate" + value needed: + - Set-WinUtilTaskbaritem -state "Error" + - Set-WinUtilTaskbaritem -state "Normal" + - Set-WinUtilTaskbaritem -state "Paused" .PARAMETER overlay - Overlay icon to display on the taskbar item - - .EXAMPLE - Set-WinUtilTaskbaritem -value 0.5 -state "Normal" - Set-WinUtilTaskbaritem -state "Error" - Set-WinUtilTaskbaritem -state "Indeterminate" + Overlay icon to display on the taskbar item, there are the presets 'logo' and 'checkmark' or you can specify a path/link to an image file. + CTT logo preset: + - Set-WinUtilTaskbaritem -overlay "logo" + Checkmark preset: + - Set-WinUtilTaskbaritem -overlay "checkmark" + Custom icon: + - Set-WinUtilTaskbaritem -overlay "C:\path\to\icon.png" + + .PARAMETER description + Description to display on the taskbar item preview Set-WinUtilTaskbaritem -description "This is a description" - Set-WinUtilTaskbaritem -overlay "C:\path\to\icon.png" - #> param ( [double]$value, @@ -32,37 +44,29 @@ function Set-WinUtilTaskbaritem { if ($state) { switch ($state) { + 'None' { $sync["Form"].taskbarItemInfo.ProgressState = "None" } 'Indeterminate' { $sync["Form"].taskbarItemInfo.ProgressState = "Indeterminate" } 'Normal' { $sync["Form"].taskbarItemInfo.ProgressState = "Normal" } 'Error' { $sync["Form"].taskbarItemInfo.ProgressState = "Error" } 'Paused' { $sync["Form"].taskbarItemInfo.ProgressState = "Paused" } - default { $sync["Form"].taskbarItemInfo.ProgressState = "None" } + default { write-host "Invalid state" } } } - if ($overlay -and (Test-Path $overlay)) { - # Read the image file as a byte array - $imageBytes = [System.IO.File]::ReadAllBytes($overlay) - - # Convert the byte array to a Base64 string - [System.Convert]::ToBase64String($imageBytes) - - # Load the image file as a bitmap - $bitmap = [System.Drawing.Bitmap]::new($overlay) - - # Create a streaming image by streaming the bitmap to a memory stream - $memoryStream = [System.IO.MemoryStream]::new() - $bitmap.Save($memoryStream, [System.Drawing.Imaging.ImageFormat]::Png) - $memoryStream.Position = 0 - - # Create a bitmap image from the memory stream - $bitmapImage = [System.Windows.Media.Imaging.BitmapImage]::new() - $bitmapImage.BeginInit() - $bitmapImage.StreamSource = $memoryStream - $bitmapImage.EndInit() - $bitmapImage.Freeze() - - $sync["Form"].taskbarItemInfo.Overlay = $bitmapImage + if ($overlay) { + switch ($overlay) { + 'logo' { + $sync["Form"].taskbarItemInfo.Overlay = (ConvertTo-Bitmap -image "$env:LOCALAPPDATA\winutil\cttlogo.png") + } + 'checkmark' { + $sync["Form"].taskbarItemInfo.Overlay = (ConvertTo-Bitmap -image "$env:LOCALAPPDATA\winutil\cttcheckmark.png") + } + default { + if (Test-Path $overlay) { + $sync["Form"].taskbarItemInfo.Overlay = (ConvertTo-Bitmap -image $overlay) + } + } + } } if ($description) { diff --git a/functions/private/convertTo-Bitmap.ps1 b/functions/private/convertTo-Bitmap.ps1 new file mode 100644 index 00000000..53570ded --- /dev/null +++ b/functions/private/convertTo-Bitmap.ps1 @@ -0,0 +1,31 @@ +function ConvertTo-Bitmap { + <# + .SYNOPSIS + Converts an image file to a Bitmap object + + .PARAMETER image + The path to the image file to convert + + .EXAMPLE + ConvertTo-Bitmap -image "C:\path\to\image.png" + #> + param ( + $image + ) + + # Read the image file as a byte array + $imageBytes = [System.IO.File]::ReadAllBytes($image) + + # Convert the byte array to a Base64 string + $base64String = [System.Convert]::ToBase64String($imageBytes) + + # Create a streaming image by streaming the base64 string to a bitmap streamsource + $bitmap = New-Object System.Windows.Media.Imaging.BitmapImage + $bitmap.BeginInit() + $bitmap.StreamSource = [System.IO.MemoryStream][System.Convert]::FromBase64String($base64String) + $bitmap.EndInit() + $bitmap.Freeze() + + # Return the bitmap object + return $bitmap +} \ No newline at end of file diff --git a/functions/public/Invoke-WPFFeatureInstall.ps1 b/functions/public/Invoke-WPFFeatureInstall.ps1 index 69c57955..e2dda9e6 100644 --- a/functions/public/Invoke-WPFFeatureInstall.ps1 +++ b/functions/public/Invoke-WPFFeatureInstall.ps1 @@ -18,15 +18,15 @@ function Invoke-WPFFeatureInstall { param($Features, $DebugPreference) $sync.ProcessRunning = $true if ($Features.count -eq 1){ - $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 }) + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" }) } else { - $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 }) + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }) } Invoke-WinUtilFeatureInstall $Features $sync.ProcessRunning = $false - $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "$env:TEMP\cttcheck.png" }) + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }) Write-Host "===================================" Write-Host "--- Features are Installed ---" diff --git a/functions/public/Invoke-WPFGetIso.ps1 b/functions/public/Invoke-WPFGetIso.ps1 index e376f093..9d00ffe9 100644 --- a/functions/public/Invoke-WPFGetIso.ps1 +++ b/functions/public/Invoke-WPFGetIso.ps1 @@ -89,7 +89,7 @@ function Invoke-WPFGetIso { return } - Set-WinUtilTaskbaritem -state "Indeterminate" + Set-WinUtilTaskbaritem -state "Indeterminate" -overlay "logo" # Detect the file size of the ISO and compare it with the free space of the system drive $isoSize = (Get-Item -Path $filePath).Length @@ -245,7 +245,7 @@ function Invoke-WPFGetIso { $sync.BusyMessage.Visibility="Hidden" $sync.ProcessRunning = $false - Set-WinUtilTaskbaritem -state "None" + Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" } diff --git a/functions/public/Invoke-WPFInstall.ps1 b/functions/public/Invoke-WPFInstall.ps1 index 4994d564..93d605d0 100644 --- a/functions/public/Invoke-WPFInstall.ps1 +++ b/functions/public/Invoke-WPFInstall.ps1 @@ -24,9 +24,9 @@ function Invoke-WPFInstall { Invoke-WPFRunspace -ArgumentList $PackagesToInstall -DebugPreference $DebugPreference -ScriptBlock { param($PackagesToInstall, $DebugPreference) if ($PackagesToInstall.count -eq 1){ - $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 }) + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" }) } else { - $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 }) + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }) } $packagesWinget, $packagesChoco = { $packagesWinget = [System.Collections.Generic.List`1[System.Object]]::new() @@ -58,7 +58,7 @@ function Invoke-WPFInstall { Write-Host "===========================================" Write-Host "-- Installs have finished ---" Write-Host "===========================================" - $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "$env:TEMP\cttcheck.png" }) + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }) } Catch { Write-Host "===========================================" @@ -66,7 +66,6 @@ function Invoke-WPFInstall { Write-Host "===========================================" $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Error" }) } - Start-Sleep -Seconds 5 $sync.ProcessRunning = $False } } diff --git a/functions/public/Invoke-WPFMicrowin.ps1 b/functions/public/Invoke-WPFMicrowin.ps1 index d796491e..6aaf96d1 100644 --- a/functions/public/Invoke-WPFMicrowin.ps1 +++ b/functions/public/Invoke-WPFMicrowin.ps1 @@ -40,10 +40,11 @@ public class PowerManagement { if ($SaveDialog.FileName -eq "") { Write-Host "No file name for the target image was specified" + Set-WinUtilTaskbaritem -state "Error" -value 1 return } - Set-WinUtilTaskbaritem -state "Indeterminate" + Set-WinUtilTaskbaritem -state "Indeterminate" -overlay "logo" Write-Host "Target ISO location: $($SaveDialog.FileName)" @@ -474,7 +475,7 @@ public class PowerManagement { #$msg = "Done. ISO image is located here: $env:temp\microwin.iso" $msg = "Done. ISO image is located here: $($SaveDialog.FileName)" Write-Host $msg - Set-WinUtilTaskbaritem -state "None" + Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information) } else { Write-Host "ISO creation failed. The "$($mountDir)" directory has not been removed." @@ -487,6 +488,5 @@ public class PowerManagement { # Allow the machine to sleep again (optional) [PowerManagement]::SetThreadExecutionState(0) $sync.ProcessRunning = $false - Set-WinUtilTaskbaritem -state "None" } } \ No newline at end of file diff --git a/functions/public/Invoke-WPFShortcut.ps1 b/functions/public/Invoke-WPFShortcut.ps1 index 8a26b82a..5f88035a 100644 --- a/functions/public/Invoke-WPFShortcut.ps1 +++ b/functions/public/Invoke-WPFShortcut.ps1 @@ -27,12 +27,6 @@ function Invoke-WPFShortcut { $ArgumentsToSourceExe = "$powershell '$IRM'" $DestinationName = "WinUtil.lnk" - Invoke-WebRequest -Uri "https://christitus.com/images/logo-full.png" -OutFile "$env:TEMP\cttlogo.png" - - if (Test-Path -Path "$env:TEMP\cttlogo.png") { - $iconPath = "$env:LOCALAPPDATA\winutil\cttlogo.ico" - ConvertTo-Icon -bitmapPath "$env:TEMP\cttlogo.png" -iconPath $iconPath - } } } @@ -54,8 +48,8 @@ function Invoke-WPFShortcut { $Shortcut = $WshShell.CreateShortcut($FileBrowser.FileName) $Shortcut.TargetPath = $SourceExe $Shortcut.Arguments = $ArgumentsToSourceExe - if ($iconPath -ne $null) { - $shortcut.IconLocation = $iconPath + if (Test-Path -Path $ctticologo) { + $shortcut.IconLocation = $ctticologo } # Save the Shortcut to disk diff --git a/functions/public/Invoke-WPFUnInstall.ps1 b/functions/public/Invoke-WPFUnInstall.ps1 index 062190fc..fd8b3f55 100644 --- a/functions/public/Invoke-WPFUnInstall.ps1 +++ b/functions/public/Invoke-WPFUnInstall.ps1 @@ -33,9 +33,9 @@ function Invoke-WPFUnInstall { Invoke-WPFRunspace -ArgumentList $PackagesToInstall -DebugPreference $DebugPreference -ScriptBlock { param($PackagesToInstall, $DebugPreference) if ($PackagesToInstall.count -eq 1){ - $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 }) + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" }) } else { - $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 }) + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }) } $packagesWinget, $packagesChoco = { $packagesWinget = [System.Collections.Generic.List`1[System.Object]]::new() @@ -67,7 +67,7 @@ function Invoke-WPFUnInstall { Write-Host "===========================================" Write-Host "-- Uninstalls have finished ---" Write-Host "===========================================" - $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "$env:TEMP\cttcheck.png" }) + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }) } Catch { Write-Host "===========================================" @@ -77,9 +77,5 @@ function Invoke-WPFUnInstall { } $sync.ProcessRunning = $False - $ButtonType = [System.Windows.MessageBoxButton]::OK - $MessageboxTitle = "Uninstalls are Finished " - $Messageboxbody = ("Done") - $MessageIcon = [System.Windows.MessageBoxImage]::Information } } diff --git a/functions/public/Invoke-WPFtweaksbutton.ps1 b/functions/public/Invoke-WPFtweaksbutton.ps1 index 44f5bab0..79bcfbbd 100644 --- a/functions/public/Invoke-WPFtweaksbutton.ps1 +++ b/functions/public/Invoke-WPFtweaksbutton.ps1 @@ -31,9 +31,9 @@ function Invoke-WPFtweaksbutton { $sync.ProcessRunning = $true if ($Tweaks.count -eq 1){ - $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 }) + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" }) } else { - $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 }) + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }) } $cnt = 0 # Execute other selected tweaks @@ -45,7 +45,7 @@ function Invoke-WPFtweaksbutton { } $sync.ProcessRunning = $false - $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "$env:TEMP\cttcheck.png" }) + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }) Write-Host "=================================" Write-Host "-- Tweaks are Finished ---" Write-Host "=================================" diff --git a/functions/public/Invoke-WPFundoall.ps1 b/functions/public/Invoke-WPFundoall.ps1 index d6f0beab..d706b815 100644 --- a/functions/public/Invoke-WPFundoall.ps1 +++ b/functions/public/Invoke-WPFundoall.ps1 @@ -25,9 +25,9 @@ function Invoke-WPFundoall { $sync.ProcessRunning = $true if ($Tweaks.count -eq 1){ - $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 }) + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" }) } else { - $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 }) + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }) } $cnt = 0 @@ -38,7 +38,7 @@ function Invoke-WPFundoall { } $sync.ProcessRunning = $false - $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "$env:TEMP\cttcheck.png" }) + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }) Write-Host "==================================" Write-Host "--- Undo Tweaks are Finished ---" Write-Host "==================================" diff --git a/scripts/main.ps1 b/scripts/main.ps1 index 5303d16b..a85ac22c 100644 --- a/scripts/main.ps1 +++ b/scripts/main.ps1 @@ -154,19 +154,6 @@ Invoke-WPFRunspace -ScriptBlock { # Print the logo Invoke-WPFFormVariables -# download the logo -$logoUrl = "https://christitus.com/images/logo-full.png" -# Download the image -$logoPath = "$env:TEMP\cttlogo.png" -Invoke-WebRequest -Uri $logoUrl -OutFile $logoPath - -# download the check -$CheckUrl = "https://raw.githubusercontent.com/ChrisTitusTech/winutil/main/docs/assets/check.png" - -# Download the image -$CheckPath = "$env:TEMP\cttcheck.png" -Invoke-WebRequest -Uri $CheckUrl -OutFile $CheckPath - # Progress bar in taskbaritem > Set-WinUtilProgressbar $sync["Form"].TaskbarItemInfo = New-Object System.Windows.Shell.TaskbarItemInfo Set-WinUtilTaskbaritem -state "None" @@ -305,9 +292,6 @@ Add-Type @" } } - # Set the overlay icon for the taskbar - Set-WinUtilTaskbaritem -overlay $logoPath - $rect = New-Object RECT [Window]::GetWindowRect($windowHandle, [ref]$rect) $width = $rect.Right - $rect.Left @@ -446,8 +430,25 @@ $sync["SearchBar"].Add_TextChanged({ $label.Visibility = "Collapsed"} }) -$sync["Form"].Add_Activated({ - Set-WinUtilTaskbaritem -overlay $logoPath -state "None" +$cttpnglogo = "$env:LOCALAPPDATA\winutil\cttlogo.png" +$ctticologo = "$env:LOCALAPPDATA\winutil\cttlogo.ico" +if (-NOT (Test-Path -Path $cttpnglogo) -or -NOT (Test-Path -Path $ctticologo)) { + Invoke-WebRequest -Uri "https://christitus.com/images/logo-full.png" -OutFile $cttpnglogo +} + +$cttpngcheckmark = "$env:LOCALAPPDATA\winutil\cttcheckmark.png" +$ctticocheckmark = "$env:LOCALAPPDATA\winutil\cttcheckmark.ico" +if (-NOT (Test-Path -Path $cttpngcheckmark) -or -NOT (Test-Path -Path $ctticocheckmark)) { + Invoke-WebRequest -Uri "https://christitus.com/images/checkmark.png" -OutFile $cttpngcheckmark +} + +ConvertTo-Icon -bitmapPath $cttpnglogo -iconPath $ctticologo + +Set-WinUtilTaskbaritem -overlay "logo" | Out-Null +$sync["Form"].icon = $cttpnglogo + +$sync["Form"].Add_Activated({ + Set-WinUtilTaskbaritem -overlay "logo" }) # Define event handler for button click diff --git a/winutil.ps1 b/winutil.ps1 index 483d5e12..10cbfc84 100644 --- a/winutil.ps1 +++ b/winutil.ps1 @@ -8,7 +8,7 @@ Author : Chris Titus @christitustech Runspace Author: @DeveloperDurp GitHub : https://github.com/ChrisTitusTech - Version : 24.07.13 + Version : 24.07.14 #> param ( [switch]$Debug, @@ -45,7 +45,7 @@ Add-Type -AssemblyName System.Windows.Forms # Variable to sync between runspaces $sync = [Hashtable]::Synchronized(@{}) $sync.PSScriptRoot = $PSScriptRoot -$sync.version = "24.07.13" +$sync.version = "24.07.14" $sync.configs = @{} $sync.ProcessRunning = $false @@ -62,6 +62,27 @@ If (([Security.Principal.WindowsIdentity]::GetCurrent()).Owner.Value -ne "S-1-5- # Set PowerShell window title $Host.UI.RawUI.WindowTitle = $myInvocation.MyCommand.Definition + "(Admin)" clear-host +function ConvertTo-Bitmap { + param ( + $image + ) + + # Read the image file as a byte array + $imageBytes = [System.IO.File]::ReadAllBytes($image) + + # Convert the byte array to a Base64 string + $base64String = [System.Convert]::ToBase64String($imageBytes) + + # Create a streaming image by streaming the base64 string to a bitmap streamsource + $bitmap = New-Object System.Windows.Media.Imaging.BitmapImage + $bitmap.BeginInit() + $bitmap.StreamSource = [System.IO.MemoryStream][System.Convert]::FromBase64String($base64String) + $bitmap.EndInit() + $bitmap.Freeze() + + # Return the bitmap object + return $bitmap +} function ConvertTo-Icon { <# @@ -922,12 +943,18 @@ function Install-WinUtilProgramChoco { } if(($chocoInstallStatus -eq 0) -AND ($tryUpgrade -eq $false)){ Write-Host "$($Program.choco) installed successfully using Chocolatey." + $X++ + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value ($x/$count) }) continue } else { Write-Host "Failed to install $($Program.choco) using Chocolatey, Chocolatey output:`n`n$(Get-Content -Path $installOutputFilePath)." + $X++ + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Error" -value ($x/$count) }) } } catch { Write-Host "Failed to install $($Program.choco) due to an error: $_" + $X++ + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Error" -value ($x/$count) }) } } @@ -939,15 +966,20 @@ function Install-WinUtilProgramChoco { $chocoUninstallStatus = $(Start-Process -FilePath "choco" -ArgumentList "uninstall $($Program.choco) -y" -Wait -PassThru).ExitCode if($chocoUninstallStatus -eq 0){ Write-Host "$($Program.choco) uninstalled successfully using Chocolatey." + $x++ + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value ($x/$count) }) continue } else { Write-Host "Failed to uninstall $($Program.choco) using Chocolatey, Chocolatey output:`n`n$(Get-Content -Path $uninstallOutputFilePath)." + $x++ + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Error" -value ($x/$count) }) } } catch { Write-Host "Failed to uninstall $($Program.choco) due to an error: $_" + $x++ + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Error" -value ($x/$count) }) } } - $x++ } Write-Progress -Activity "$manage Applications" -Status "Finished" -Completed @@ -1002,20 +1034,24 @@ Function Install-WinUtilProgramWinget { $status = $(Start-Process -FilePath "winget" -ArgumentList "install --id $($Program.winget) --silent --accept-source-agreements --accept-package-agreements" -Wait -PassThru -NoNewWindow).ExitCode if($status -eq 0) { Write-Host "$($Program.winget) installed successfully." + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -value ($i/$count) }) continue } if ($status -eq -1978335189) { Write-Host "$($Program.winget) No applicable update found" + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -value ($i/$count) }) continue } Write-Host "Attempt with User scope" $status = $(Start-Process -FilePath "winget" -ArgumentList "install --id $($Program.winget) --scope user --silent --accept-source-agreements --accept-package-agreements" -Wait -PassThru -NoNewWindow).ExitCode if($status -eq 0) { Write-Host "$($Program.winget) installed successfully with User scope." + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -value ($i/$count) }) continue } if ($status -eq -1978335189) { Write-Host "$($Program.winget) No applicable update found" + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -value ($i/$count) }) continue } Write-Host "Attempt with User prompt" @@ -1030,15 +1066,18 @@ Function Install-WinUtilProgramWinget { } if($status -eq 0) { Write-Host "$($Program.winget) installed successfully with User prompt." + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -value ($i/$count) }) continue } if ($status -eq -1978335189) { Write-Host "$($Program.winget) No applicable update found" + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -value ($i/$count) }) continue } } catch { Write-Host "Failed to install $($Program.winget). With winget" $failedPackages += $Program + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Error" -value ($i/$count) }) } } elseif($manage -eq "Uninstalling") { @@ -1054,7 +1093,9 @@ Function Install-WinUtilProgramWinget { } catch { Write-Host "Failed to uninstall $($Program.winget) due to an error: $_" $failedPackages += $Program + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Error" }) } + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -value ($i/$count) }) } else { throw "[Install-WinUtilProgramWinget] Invalid Value for Parameter 'manage', Provided Value is: $manage" @@ -1084,6 +1125,7 @@ function Install-WinUtilWinget { Write-Host "`nWinget is not Installed. Continuing with install.`r" -ForegroundColor Red } + # Gets the computer's information if ($null -eq $sync.ComputerInfo){ $ComputerInfo = Get-ComputerInfo -ErrorAction Stop @@ -1128,6 +1170,7 @@ function Install-WinUtilWinget { throw [WingetFailedInstall]::new('Failed to install!') } } + } function Test-CompatibleImage() { <# @@ -1967,6 +2010,8 @@ function Invoke-WinUtilFeatureInstall { $CheckBox ) + $x = 0 + $CheckBox | ForEach-Object { if($sync.configs.feature.$psitem.feature){ Foreach( $feature in $sync.configs.feature.$psitem.feature ){ @@ -1977,9 +2022,11 @@ function Invoke-WinUtilFeatureInstall { Catch{ if ($psitem.Exception.Message -like "*requires elevation*"){ Write-Warning "Unable to Install $feature due to permissions. Are you running as admin?" + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Error" }) } else{ + Write-Warning "Unable to Install $feature due to unhandled exception" Write-Warning $psitem.Exception.StackTrace } @@ -1997,15 +2044,19 @@ function Invoke-WinUtilFeatureInstall { Catch{ if ($psitem.Exception.Message -like "*requires elevation*"){ Write-Warning "Unable to Install $feature due to permissions. Are you running as admin?" + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Error" }) } else{ + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Error" }) Write-Warning "Unable to Install $feature due to unhandled exception" Write-Warning $psitem.Exception.StackTrace } } } } + $X++ + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -value ($x/$CheckBox.Count) }) } } function Invoke-WinUtilGPU { @@ -2749,6 +2800,69 @@ Function Set-WinUtilService { } } +function Set-WinUtilTaskbaritem { + <# + + .SYNOPSIS + Modifies the Taskbaritem of the WPF Form + + .PARAMETER state & value + Value can be between 0 and 1, 0 being no progress done yet and 1 being fully completed + State can be 'None' > No progress, 'Indeterminate' > Without value, 'Normal' > when using value, 'Error' > Red (when using value), 'Paused' > Yellow (when using value) + + .PARAMETER overlay + Overlay icon to display on the taskbar item + + .EXAMPLE + Set-WinUtilTaskbaritem -value 0.5 -state "Normal" + Set-WinUtilTaskbaritem -state "Error" + Set-WinUtilTaskbaritem -state "Indeterminate" + Set-WinUtilTaskbaritem -description "This is a description" + Set-WinUtilTaskbaritem -overlay "C:\path\to\icon.png" + + #> + param ( + [double]$value, + $state, + [string]$overlay, + $description + ) + + if ($value) { + $sync["Form"].taskbarItemInfo.ProgressValue = $value + } + + if ($state) { + switch ($state) { + 'None' { $sync["Form"].taskbarItemInfo.ProgressState = "None" } + 'Indeterminate' { $sync["Form"].taskbarItemInfo.ProgressState = "Indeterminate" } + 'Normal' { $sync["Form"].taskbarItemInfo.ProgressState = "Normal" } + 'Error' { $sync["Form"].taskbarItemInfo.ProgressState = "Error" } + 'Paused' { $sync["Form"].taskbarItemInfo.ProgressState = "Paused" } + default { write-host "Invalid state" } + } + } + + if ($overlay) { + switch ($overlay) { + 'logo' { + $sync["Form"].taskbarItemInfo.Overlay = (ConvertTo-Bitmap -image "$env:LOCALAPPDATA\winutil\cttlogo.png") + } + 'checkmark' { + $sync["Form"].taskbarItemInfo.Overlay = (ConvertTo-Bitmap -image "$env:LOCALAPPDATA\winutil\cttcheckmark.png") + } + default { + if (Test-Path $overlay) { + $sync["Form"].taskbarItemInfo.Overlay = (ConvertTo-Bitmap -image $overlay) + } + } + } + } + + if ($description) { + $sync["Form"].taskbarItemInfo.Description = $description + } +} function Set-WinUtilUITheme { <# @@ -3344,12 +3458,18 @@ function Invoke-WPFFeatureInstall { Invoke-WPFRunspace -ArgumentList $Features -DebugPreference $DebugPreference -ScriptBlock { param($Features, $DebugPreference) - $sync.ProcessRunning = $true + if ($Features.count -eq 1){ + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" }) + } else { + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }) + } Invoke-WinUtilFeatureInstall $Features $sync.ProcessRunning = $false + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }) + Write-Host "===================================" Write-Host "--- Features are Installed ---" Write-Host "--- A Reboot may be required ---" @@ -3753,6 +3873,7 @@ function Invoke-WPFGetInstalled { param($checkbox, $DebugPreference) $sync.ProcessRunning = $true + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" }) if($checkbox -eq "winget"){ Write-Host "Getting Installed Programs..." @@ -3771,6 +3892,7 @@ function Invoke-WPFGetInstalled { Write-Host "Done..." $sync.ProcessRunning = $false + $sync.form.Dispatcher.Invoke([action] { Set-WinUtilTaskbaritem -state "None" }) } } function Invoke-WPFGetIso { @@ -3791,6 +3913,7 @@ function Invoke-WPFGetIso { $sync.BusyText.Text="N Busy" + Write-Host " _ __ __ _ " Write-Host " /\/\ (_) ___ _ __ ___ / / /\ \ \(_) _ __ " Write-Host " / \ | | / __|| '__| / _ \ \ \/ \/ /| || '_ \ " @@ -3863,6 +3986,8 @@ function Invoke-WPFGetIso { return } + Set-WinUtilTaskbaritem -state "Indeterminate" -overlay "logo" + # Detect the file size of the ISO and compare it with the free space of the system drive $isoSize = (Get-Item -Path $filePath).Length Write-Debug "Size of ISO file: $($isoSize) bytes" @@ -3879,6 +4004,7 @@ function Invoke-WPFGetIso { { # It's critical and we can't continue. Output an error Write-Host "You don't have enough space for this operation. You need at least $([Math]::Round(($isoSize / ([Math]::Pow(1024, 2))) * 2, 2)) MB of free space to copy the ISO files to a temp directory and to be able to perform additional operations." + Set-WinUtilTaskbaritem -state "Error" -value 1 return } else @@ -3897,6 +4023,7 @@ function Invoke-WPFGetIso { Write-Error "Failed to mount the image. Error: $($_.Exception.Message)" Write-Error "This is NOT winutil's problem, your ISO might be corrupt, or there is a problem on the system" Write-Error "Please refer to this wiki for more details https://github.com/ChrisTitusTech/winutil/blob/main/wiki/Error-in-Winutil-MicroWin-during-ISO-mounting%2Cmd" + Set-WinUtilTaskbaritem -state "Error" -value 1 return } # storing off values in hidden fields for further steps @@ -3974,6 +4101,7 @@ function Invoke-WPFGetIso { $msg = "Neither install.wim nor install.esd exist in the image, this could happen if you use unofficial Windows images. Please don't use shady images from the internet, use only official images. Here are instructions how to download ISO images if the Microsoft website is not showing the link to download and ISO. https://www.techrepublic.com/article/how-to-download-a-windows-10-iso-file-without-using-the-media-creation-tool/" Write-Host $msg [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error) + Set-WinUtilTaskbaritem -state "Error" -value 1 throw } elseif ((-not (Test-Path -Path $wimFile -PathType Leaf)) -and (Test-Path -Path $wimFile.Replace(".wim", ".esd").Trim() -PathType Leaf)) @@ -4014,6 +4142,7 @@ function Invoke-WPFGetIso { $sync.BusyMessage.Visibility="Hidden" $sync.ProcessRunning = $false + Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" } @@ -4100,8 +4229,14 @@ function Invoke-WPFInstall { return } + Invoke-WPFRunspace -ArgumentList $PackagesToInstall -DebugPreference $DebugPreference -ScriptBlock { param($PackagesToInstall, $DebugPreference) + if ($PackagesToInstall.count -eq 1){ + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" }) + } else { + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }) + } $packagesWinget, $packagesChoco = { $packagesWinget = [System.Collections.Generic.List`1[System.Object]]::new() $packagesChoco = [System.Collections.Generic.List`1[System.Object]]::new() @@ -4132,11 +4267,13 @@ function Invoke-WPFInstall { Write-Host "===========================================" Write-Host "-- Installs have finished ---" Write-Host "===========================================" + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }) } Catch { Write-Host "===========================================" Write-Host "Error: $_" Write-Host "===========================================" + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Error" }) } Start-Sleep -Seconds 5 $sync.ProcessRunning = $False @@ -4159,6 +4296,8 @@ function Invoke-WPFInstallUpgrade { return } + # Set-WinUtilTaskbaritem -state "Indeterminate" + Update-WinUtilProgramWinget Write-Host "===========================================" @@ -4172,6 +4311,7 @@ function Invoke-WPFMicrowin { Invoke MicroWin routines... #> + if($sync.ProcessRunning) { $msg = "GetIso process is currently running." [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning) @@ -4207,9 +4347,12 @@ public class PowerManagement { if ($SaveDialog.FileName -eq "") { Write-Host "No file name for the target image was specified" + Set-WinUtilTaskbaritem -state "Error" -value 1 return } + Set-WinUtilTaskbaritem -state "Indeterminate" -overlay "logo" + Write-Host "Target ISO location: $($SaveDialog.FileName)" $index = $sync.MicrowinWindowsFlavors.SelectedValue.Split(":")[0].Trim() @@ -4242,6 +4385,7 @@ public class PowerManagement { $msg = "The export process has failed and MicroWin processing cannot continue" Write-Host "Failed to export the image" [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error) + Set-WinUtilTaskbaritem -state "Error" -value 1 return } } @@ -4255,6 +4399,7 @@ public class PowerManagement { $dlg_msg = $msg + "`n`nIf you want more information, the version of the image selected is $($imgVersion)`n`nIf an image has been incorrectly marked as incompatible, report an issue to the developers." Write-Host $msg [System.Windows.MessageBox]::Show($dlg_msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Exclamation) + Set-WinUtilTaskbaritem -state "Error" -value 1 return } @@ -4263,6 +4408,7 @@ public class PowerManagement { if (-not $mountDirExists -or -not $scratchDirExists) { Write-Error "Required directories '$mountDirExists' '$scratchDirExists' and do not exist." + Set-WinUtilTaskbaritem -state "Error" -value 1 return } @@ -4277,6 +4423,7 @@ public class PowerManagement { else { Write-Host "Could not mount image. Exiting..." + Set-WinUtilTaskbaritem -state "Error" -value 1 return } @@ -4533,6 +4680,7 @@ public class PowerManagement { if (-not (Test-Path -Path "$mountDir\sources\install.wim")) { Write-Error "Something went wrong and '$mountDir\sources\install.wim' doesn't exist. Please report this bug to the devs" + Set-WinUtilTaskbaritem -state "Error" -value 1 return } Write-Host "Windows image completed. Continuing with boot.wim." @@ -4634,6 +4782,7 @@ public class PowerManagement { #$msg = "Done. ISO image is located here: $env:temp\microwin.iso" $msg = "Done. ISO image is located here: $($SaveDialog.FileName)" Write-Host $msg + Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" [System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information) } else { Write-Host "ISO creation failed. The "$($mountDir)" directory has not been removed." @@ -4879,12 +5028,6 @@ function Invoke-WPFShortcut { $ArgumentsToSourceExe = "$powershell '$IRM'" $DestinationName = "WinUtil.lnk" - Invoke-WebRequest -Uri "https://christitus.com/images/logo-full.png" -OutFile "$env:TEMP\cttlogo.png" - - if (Test-Path -Path "$env:TEMP\cttlogo.png") { - $iconPath = "$env:LOCALAPPDATA\winutil\cttlogo.ico" - ConvertTo-Icon -bitmapPath "$env:TEMP\cttlogo.png" -iconPath $iconPath - } } } @@ -4906,8 +5049,8 @@ function Invoke-WPFShortcut { $Shortcut = $WshShell.CreateShortcut($FileBrowser.FileName) $Shortcut.TargetPath = $SourceExe $Shortcut.Arguments = $ArgumentsToSourceExe - if ($iconPath -ne $null) { - $shortcut.IconLocation = $iconPath + if (Test-Path -Path $ctticologo) { + $shortcut.IconLocation = $ctticologo } # Save the Shortcut to disk @@ -5065,15 +5208,22 @@ function Invoke-WPFtweaksbutton { $sync.ProcessRunning = $true + if ($Tweaks.count -eq 1){ + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" }) + } else { + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }) + } $cnt = 0 # Execute other selected tweaks foreach ($tweak in $Tweaks) { Write-Debug "This is a tweak to run $tweak count: $cnt" Invoke-WinUtilTweaks $tweak $cnt += 1 + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -value ($cnt/$Tweaks.Count) }) } $sync.ProcessRunning = $false + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }) Write-Host "=================================" Write-Host "-- Tweaks are Finished ---" Write-Host "=================================" @@ -5164,12 +5314,21 @@ function Invoke-WPFundoall { param($Tweaks, $DebugPreference) $sync.ProcessRunning = $true + if ($Tweaks.count -eq 1){ + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" }) + } else { + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }) + } + $cnt = 0 Foreach ($tweak in $tweaks){ Invoke-WinUtilTweaks $tweak -undo $true + $cnt += 1 + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -value ($cnt/$Tweaks.Count) }) } $sync.ProcessRunning = $false + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }) Write-Host "==================================" Write-Host "--- Undo Tweaks are Finished ---" Write-Host "==================================" @@ -5365,8 +5524,14 @@ function Invoke-WPFUnInstall { if($confirm -eq "No"){return} + Invoke-WPFRunspace -ArgumentList $PackagesToInstall -DebugPreference $DebugPreference -ScriptBlock { param($PackagesToInstall, $DebugPreference) + if ($PackagesToInstall.count -eq 1){ + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" }) + } else { + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }) + } $packagesWinget, $packagesChoco = { $packagesWinget = [System.Collections.Generic.List`1[System.Object]]::new() $packagesChoco = [System.Collections.Generic.List`1[System.Object]]::new() @@ -5392,23 +5557,21 @@ function Invoke-WPFUnInstall { Install-WinUtilProgramChoco -ProgramsToInstall $packagesChoco -Manage "Uninstalling" } - $ButtonType = [System.Windows.MessageBoxButton]::OK - $MessageboxTitle = "Uninstalls are Finished " - $Messageboxbody = ("Done") - $MessageIcon = [System.Windows.MessageBoxImage]::Information - [System.Windows.MessageBox]::Show($Messageboxbody, $MessageboxTitle, $ButtonType, $MessageIcon) Write-Host "===========================================" Write-Host "-- Uninstalls have finished ---" Write-Host "===========================================" + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }) } Catch { Write-Host "===========================================" Write-Host "Error: $_" Write-Host "===========================================" + $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Error" }) } $sync.ProcessRunning = $False + } } function Invoke-WPFUpdatesdefault { @@ -14945,6 +15108,10 @@ Invoke-WPFRunspace -ScriptBlock { # Print the logo Invoke-WPFFormVariables +# Progress bar in taskbaritem > Set-WinUtilProgressbar +$sync["Form"].TaskbarItemInfo = New-Object System.Windows.Shell.TaskbarItemInfo +Set-WinUtilTaskbaritem -state "None" + # Set the titlebar $sync["Form"].title = $sync["Form"].title + " " + $sync.version # Set the commands that will run when the form is closed @@ -15079,38 +15246,6 @@ Add-Type @" } } - - # Using a TaskbarItem Overlay until someone figures out how to replace the icon correctly - - # URL of the image - $imageUrl = "https://christitus.com/images/logo-full.png" - - # Download the image - $imagePath = "$env:TEMP\logo-full.png" - Invoke-WebRequest -Uri $imageUrl -OutFile $imagePath - - # Read the image file as a byte array - $imageBytes = [System.IO.File]::ReadAllBytes($imagePath) - - # Convert the byte array to a Base64 string - $base64String = [System.Convert]::ToBase64String($imageBytes) - - # Create a streaming image by streaming the base64 string to a bitmap streamsource - $bitmap = New-Object System.Windows.Media.Imaging.BitmapImage - $bitmap.BeginInit() - $bitmap.StreamSource = [System.IO.MemoryStream][System.Convert]::FromBase64String($base64String) - $bitmap.EndInit() - $bitmap.Freeze() - - # Ensure TaskbarItemInfo is created if not already - if (-not $sync["Form"].TaskbarItemInfo) { - $sync["Form"].TaskbarItemInfo = New-Object System.Windows.Shell.TaskbarItemInfo - } - - # Set the overlay icon for the taskbar - $sync["Form"].TaskbarItemInfo.Overlay = $bitmap - - $rect = New-Object RECT [Window]::GetWindowRect($windowHandle, [ref]$rect) $width = $rect.Right - $rect.Left @@ -15249,6 +15384,27 @@ $sync["SearchBar"].Add_TextChanged({ $label.Visibility = "Collapsed"} }) +$cttpnglogo = "$env:LOCALAPPDATA\winutil\cttlogo.png" +$ctticologo = "$env:LOCALAPPDATA\winutil\cttlogo.ico" +if (-NOT (Test-Path -Path $cttpnglogo) -or -NOT (Test-Path -Path $ctticologo)) { + Invoke-WebRequest -Uri "https://christitus.com/images/logo-full.png" -OutFile $cttpnglogo +} + +$cttpngcheckmark = "$env:LOCALAPPDATA\winutil\cttcheckmark.png" +$ctticocheckmark = "$env:LOCALAPPDATA\winutil\cttcheckmark.ico" +if (-NOT (Test-Path -Path $cttpngcheckmark) -or -NOT (Test-Path -Path $ctticocheckmark)) { + Invoke-WebRequest -Uri "https://christitus.com/images/checkmark.png" -OutFile $cttpngcheckmark +} + +ConvertTo-Icon -bitmapPath $cttpnglogo -iconPath $ctticologo + +Set-WinUtilTaskbaritem -overlay "logo" | Out-Null +$sync["Form"].icon = $cttpnglogo + +$sync["Form"].Add_Activated({ + Set-WinUtilTaskbaritem -overlay "logo" +}) + # Define event handler for button click $sync["SettingsButton"].Add_Click({ Write-Debug "SettingsButton clicked"