Enhance user interaction with new dialog features and improve default browser handling
This commit is contained in:
@@ -4,14 +4,18 @@ A unified framework for managing Windows automation tasks with a modular, extens
|
||||
|
||||
## Quick Start
|
||||
|
||||
Clone or extract the repo to any location, then:
|
||||
|
||||
```powershell
|
||||
# Navigate to the root folder
|
||||
cd C:\Tools\ArnePowershellAutomation
|
||||
# Navigate to the repo folder (any path works)
|
||||
cd <your-repo-path>
|
||||
|
||||
# Run the main menu
|
||||
.\configure.ps1
|
||||
```
|
||||
|
||||
All paths are relative to `configure.ps1`, so the repo is portable.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Entry Point: `configure.ps1`
|
||||
@@ -218,6 +222,7 @@ When you select a feature for configuration, you'll be prompted for each setting
|
||||
- **Windows 11 Only for Toasts**: Toast notifications require WinRT (Windows 10+), but full Windows 11 support is assumed. Win10 may work but is untested.
|
||||
- **Scheduled Task Security**: The runner task executes as the current user with Limited RunLevel (no elevation). Features cannot perform system-wide administrative tasks; they're limited to user-level operations.
|
||||
- **DNS Suffix Matching**: Network detection relies on `Get-DnsClient` adapter DNS suffix. Make sure your network adapter is configured with the correct suffix for accurate detection.
|
||||
- **Manual Execution & Repetition Timer**: Executing the runner manually via menu option 5 ("Execute runner now") runs the task immediately but does **not** start the 2-minute repetition timer. The repetition timer only activates on logon/restart. For continuous testing, you may need to restart or wait for next logon.
|
||||
|
||||
## Extending with New Features
|
||||
|
||||
|
||||
+200
-12
@@ -180,19 +180,30 @@ function Read-OperationResult {
|
||||
# Feature discovery
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function Get-Features {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Dot-sources every .ps1 in internal\features\ and collects their $FeatureMeta.
|
||||
Returns an ordered list of [PSCustomObject]@{ File; Meta }.
|
||||
#>
|
||||
$featuresDir = Join-Path $script:InternalRoot 'features'
|
||||
$featureFiles = Get-ChildItem -Path $featuresDir -Filter '*.ps1' -ErrorAction SilentlyContinue |
|
||||
Sort-Object Name
|
||||
function Get-FeatureFiles {
|
||||
$featuresDir = Join-Path $script:InternalRoot 'features'
|
||||
if (-not (Test-Path $featuresDir)) {
|
||||
return @()
|
||||
}
|
||||
|
||||
return @(Get-ChildItem -Path $featuresDir -Filter '*.ps1' -ErrorAction SilentlyContinue |
|
||||
Sort-Object Name)
|
||||
}
|
||||
|
||||
function Get-FeatureCacheSignature {
|
||||
param([System.IO.FileInfo[]]$FeatureFiles)
|
||||
|
||||
return (($FeatureFiles | ForEach-Object {
|
||||
'{0}:{1}' -f $_.FullName, $_.LastWriteTimeUtc.Ticks
|
||||
}) -join '|')
|
||||
}
|
||||
|
||||
function Load-FeaturesFromFiles {
|
||||
param([System.IO.FileInfo[]]$FeatureFiles)
|
||||
|
||||
$features = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
|
||||
foreach ($file in $featureFiles) {
|
||||
foreach ($file in $FeatureFiles) {
|
||||
try {
|
||||
. $file.FullName # defines $FeatureMeta + Invoke-Feature
|
||||
$meta = $FeatureMeta # copy before the next iteration overwrites it
|
||||
@@ -208,7 +219,140 @@ function Get-Features {
|
||||
}
|
||||
}
|
||||
|
||||
return $features
|
||||
return @($features)
|
||||
}
|
||||
|
||||
function Complete-FeaturePrewarm {
|
||||
param([switch]$Wait)
|
||||
|
||||
if (-not $script:FeaturesPrewarmTask) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$task = $script:FeaturesPrewarmTask
|
||||
|
||||
if ($Wait) {
|
||||
$null = $task.Handle.AsyncWaitHandle.WaitOne()
|
||||
}
|
||||
|
||||
if (-not $task.Handle.IsCompleted) {
|
||||
return $false
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $task.PowerShell.EndInvoke($task.Handle)
|
||||
if ($result.Count -gt 0) {
|
||||
$payload = $result[0]
|
||||
$script:FeaturesCache = @($payload.Features)
|
||||
$script:FeaturesCacheSignature = [string]$payload.Signature
|
||||
|
||||
foreach ($errMsg in @($payload.Errors)) {
|
||||
Write-Log -Level Warn -Message $errMsg -Feature 'Configure'
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Log -Level Warn -Message "Feature prewarm completion failed: $_" -Feature 'Configure'
|
||||
}
|
||||
finally {
|
||||
try { $task.PowerShell.Dispose() } catch {}
|
||||
$script:FeaturesPrewarmTask = $null
|
||||
}
|
||||
|
||||
return $true
|
||||
}
|
||||
|
||||
function Start-FeaturePrewarmAsync {
|
||||
if ($script:FeaturesPrewarmTask) {
|
||||
return
|
||||
}
|
||||
|
||||
$featureFiles = Get-FeatureFiles
|
||||
if ($featureFiles.Count -eq 0) {
|
||||
$script:FeaturesCache = @()
|
||||
$script:FeaturesCacheSignature = ''
|
||||
return
|
||||
}
|
||||
|
||||
$ps = [powershell]::Create()
|
||||
$scriptBlock = @'
|
||||
param([string]$FeaturesDir)
|
||||
|
||||
$featureFiles = Get-ChildItem -Path $FeaturesDir -Filter '*.ps1' -ErrorAction SilentlyContinue |
|
||||
Sort-Object Name
|
||||
|
||||
$signature = (($featureFiles | ForEach-Object {
|
||||
'{0}:{1}' -f $_.FullName, $_.LastWriteTimeUtc.Ticks
|
||||
}) -join '|')
|
||||
|
||||
$features = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
$errors = [System.Collections.Generic.List[string]]::new()
|
||||
|
||||
foreach ($file in $featureFiles) {
|
||||
try {
|
||||
. $file.FullName
|
||||
$meta = $FeatureMeta
|
||||
$features.Add([PSCustomObject]@{
|
||||
File = $file.FullName
|
||||
Meta = $meta
|
||||
})
|
||||
}
|
||||
catch {
|
||||
$errors.Add(("Failed to load feature '{0}' during prewarm: {1}" -f $file.Name, $_.Exception.Message))
|
||||
}
|
||||
}
|
||||
|
||||
[PSCustomObject]@{
|
||||
Signature = $signature
|
||||
Features = @($features)
|
||||
Errors = @($errors)
|
||||
}
|
||||
'@
|
||||
|
||||
$null = $ps.AddScript($scriptBlock).AddArgument((Join-Path $script:InternalRoot 'features'))
|
||||
$handle = $ps.BeginInvoke()
|
||||
|
||||
$script:FeaturesPrewarmTask = @{
|
||||
PowerShell = $ps
|
||||
Handle = $handle
|
||||
}
|
||||
}
|
||||
|
||||
function Get-Features {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Dot-sources every .ps1 in internal\features\ and collects their $FeatureMeta.
|
||||
Returns an ordered list of [PSCustomObject]@{ File; Meta }.
|
||||
#>
|
||||
param([switch]$Refresh)
|
||||
|
||||
$featureFiles = Get-FeatureFiles
|
||||
$cacheSignature = Get-FeatureCacheSignature -FeatureFiles $featureFiles
|
||||
|
||||
if (-not $Refresh -and $script:FeaturesCache -and $script:FeaturesCacheSignature -eq $cacheSignature) {
|
||||
return $script:FeaturesCache
|
||||
}
|
||||
|
||||
if (-not $Refresh) {
|
||||
[void](Complete-FeaturePrewarm)
|
||||
|
||||
if ($script:FeaturesCache -and $script:FeaturesCacheSignature -eq $cacheSignature) {
|
||||
return $script:FeaturesCache
|
||||
}
|
||||
|
||||
if ($script:FeaturesPrewarmTask) {
|
||||
Write-Hint 'Loading feature metadata, please wait...'
|
||||
[void](Complete-FeaturePrewarm -Wait)
|
||||
|
||||
if ($script:FeaturesCache -and $script:FeaturesCacheSignature -eq $cacheSignature) {
|
||||
return $script:FeaturesCache
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$script:FeaturesCache = Load-FeaturesFromFiles -FeatureFiles $featureFiles
|
||||
$script:FeaturesCacheSignature = $cacheSignature
|
||||
return $script:FeaturesCache
|
||||
}
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
@@ -395,7 +539,10 @@ function Register-Runner {
|
||||
throw "Register-ScheduledTask returned without creating '$($script:TaskName)'."
|
||||
}
|
||||
|
||||
Write-Host " Task '$($script:TaskName)' registered successfully." -ForegroundColor Green
|
||||
# Start the task so runner begins executing
|
||||
Start-ScheduledTask -TaskName $script:TaskName -TaskPath $script:TaskPath -ErrorAction Stop
|
||||
|
||||
Write-Host " Task '$($script:TaskName)' registered and started successfully." -ForegroundColor Green
|
||||
Write-Host ''
|
||||
Write-Hint (" Triggers: at logon + every {0} minutes" -f $script:RunnerIntervalMinutes)
|
||||
Write-Hint " Runs: hidden PowerShell, current user, no elevation"
|
||||
@@ -604,10 +751,46 @@ function Show-FeatureMenu {
|
||||
}
|
||||
}
|
||||
|
||||
function Initialize-FeatureCache {
|
||||
try {
|
||||
Start-FeaturePrewarmAsync
|
||||
Write-Log -Level Info -Message 'Feature cache prewarm started in background.' -Feature 'Configure'
|
||||
}
|
||||
catch {
|
||||
Write-Log -Level Warn -Message "Feature cache prewarm failed: $_" -Feature 'Configure'
|
||||
}
|
||||
}
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# Main menu
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function Invoke-RunnerNow {
|
||||
$task = Get-RunnerTask
|
||||
if (-not $task) {
|
||||
Write-Host " ERROR: Task is not registered. Use option 2 to register first." -ForegroundColor Red
|
||||
Write-Host ''
|
||||
Pause-ForKey
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Host ''
|
||||
Write-Host " Executing runner task now..." -ForegroundColor Cyan
|
||||
Start-ScheduledTask -TaskName $script:TaskName -TaskPath $script:TaskPath -ErrorAction Stop
|
||||
Write-Host " Task execution started (runs async). Check logs in a moment." -ForegroundColor Green
|
||||
Write-Hint " Note: The 2-minute repetition timer only starts on next logon/restart."
|
||||
Write-Log -Level Info -Message "Runner task invoked manually from configure menu." -Feature 'Configure'
|
||||
}
|
||||
catch {
|
||||
Write-Host " ERROR: Failed to start task: $_" -ForegroundColor Red
|
||||
Write-Log -Level Error -Message "Failed to start runner task manually: $_" -Feature 'Configure'
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Pause-ForKey
|
||||
}
|
||||
|
||||
function Show-MainMenu {
|
||||
while ($true) {
|
||||
Write-Header 'PowerShell Automation Center'
|
||||
@@ -615,6 +798,7 @@ function Show-MainMenu {
|
||||
Write-Host ' 2 Register runner'
|
||||
Write-Host ' 3 Remove registered runner'
|
||||
Write-Host ' 4 Configure features'
|
||||
Write-Host ' 5 Execute runner now'
|
||||
Write-Host ''
|
||||
Write-Hint ' Q Exit'
|
||||
Write-Host ''
|
||||
@@ -627,6 +811,7 @@ function Show-MainMenu {
|
||||
'2' { Register-Runner }
|
||||
'3' { Remove-Runner }
|
||||
'4' { Show-FeatureMenu }
|
||||
'5' { Invoke-RunnerNow }
|
||||
{ $_ -in 'q', 'Q' } {
|
||||
Clear-Host
|
||||
return
|
||||
@@ -679,5 +864,8 @@ if ($AutoRemoveRunner) {
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $AutoRegisterRunner -and -not $AutoRemoveRunner) {
|
||||
Initialize-FeatureCache
|
||||
}
|
||||
Show-MainMenu
|
||||
|
||||
|
||||
@@ -1,4 +1,105 @@
|
||||
# ── Feature metadata ──────────────────────────────────────────────────────────
|
||||
# ── Browser detection helper ──────────────────────────────────────────────────
|
||||
|
||||
function Get-InstalledBrowsers {
|
||||
$knownBrowsers = @(
|
||||
'ChromeHTML',
|
||||
'FirefoxURL',
|
||||
'OperaGXStable',
|
||||
'Opera GXStable',
|
||||
'SafariHTML',
|
||||
'MSEdgeHTM',
|
||||
'BraveHTML',
|
||||
'Vivaldi',
|
||||
'IEexplore'
|
||||
)
|
||||
|
||||
$installed = @()
|
||||
foreach ($progId in $knownBrowsers) {
|
||||
if (Test-Path "HKLM:\SOFTWARE\Classes\$progId" -ErrorAction SilentlyContinue) {
|
||||
$installed += $progId
|
||||
}
|
||||
}
|
||||
|
||||
return @($installed | Sort-Object)
|
||||
}
|
||||
|
||||
function Get-BrowserSearchText {
|
||||
param([string]$ProgId)
|
||||
|
||||
if (-not $ProgId) { return 'browser' }
|
||||
|
||||
switch -Regex ($ProgId) {
|
||||
'^Opera\s?GXStable$' { return 'opera gx' }
|
||||
'^ChromeHTML$' { return 'chrome' }
|
||||
'^FirefoxURL' { return 'firefox' }
|
||||
'^MSEdgeHTM$' { return 'edge' }
|
||||
'^BraveHTML$' { return 'brave' }
|
||||
'^Vivaldi$' { return 'vivaldi' }
|
||||
default { return $ProgId }
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-DefaultBrowserGuidedChange {
|
||||
param([Parameter(Mandatory)][string]$TargetProgId)
|
||||
|
||||
$regPath = 'HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice'
|
||||
|
||||
try {
|
||||
$searchText = Get-BrowserSearchText -ProgId $TargetProgId
|
||||
|
||||
Stop-Process -ErrorAction Ignore -Name SystemSettings
|
||||
Start-Process 'ms-settings:defaultapps'
|
||||
|
||||
$ps = Get-Process -ErrorAction Stop SystemSettings
|
||||
do {
|
||||
Start-Sleep -Milliseconds 100
|
||||
$ps.Refresh()
|
||||
} while ([int]$ps.MainWindowHandle -eq 0)
|
||||
|
||||
Start-Sleep -Milliseconds 200
|
||||
|
||||
$shell = New-Object -ComObject WScript.Shell
|
||||
|
||||
foreach ($i in 1..5) {
|
||||
$shell.SendKeys('{TAB}')
|
||||
Start-Sleep -Milliseconds 100
|
||||
}
|
||||
|
||||
$shell.SendKeys($searchText)
|
||||
Start-Sleep -Seconds 1
|
||||
|
||||
$shell.SendKeys('{TAB}')
|
||||
Start-Sleep -Milliseconds 100
|
||||
$shell.SendKeys('{ENTER}')
|
||||
Start-Sleep -Milliseconds 200
|
||||
$shell.SendKeys('{ENTER}')
|
||||
Start-Sleep -Milliseconds 200
|
||||
$shell.SendKeys('%{F4}')
|
||||
Start-Sleep -Milliseconds 300
|
||||
|
||||
$browser = (Get-ItemProperty -Path $regPath -Name 'ProgId' -ErrorAction Stop).ProgId
|
||||
if ($browser -eq $TargetProgId) {
|
||||
Write-Log -Level Info -Message "Default browser successfully changed to '$browser'." -Feature 'DefaultBrowser'
|
||||
return $true
|
||||
}
|
||||
|
||||
Write-Log -Level Warn -Message "Browser change attempted, but current ProgId is '$browser' (expected '$TargetProgId')." -Feature 'DefaultBrowser'
|
||||
return $false
|
||||
}
|
||||
catch {
|
||||
Write-Log -Level Error -Message "Failed during guided default-browser change: $_" -Feature 'DefaultBrowser'
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# ── Feature metadata ──────────────────────────────────────────────────────────
|
||||
|
||||
$installedBrowsers = Get-InstalledBrowsers
|
||||
$browserListText = if ($installedBrowsers.Count -gt 0) {
|
||||
"Detected on this system: " + ($installedBrowsers -join ', ')
|
||||
} else {
|
||||
"No known browsers detected. Examples: ChromeHTML, FirefoxURL, OperaGXStable, SafariHTML"
|
||||
}
|
||||
|
||||
$FeatureMeta = @{
|
||||
Name = 'DefaultBrowser'
|
||||
@@ -9,7 +110,7 @@ $FeatureMeta = @{
|
||||
Label = 'Target ProgId'
|
||||
Type = 'string'
|
||||
Default = 'OperaGXStable'
|
||||
Description = 'ProgId of the desired default browser (e.g. OperaGXStable, ChromeHTML, FirefoxURL-308046B0AF4A39CB)'
|
||||
Description = $browserListText
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -23,16 +124,10 @@ function Invoke-Feature {
|
||||
)
|
||||
|
||||
if (-not $State) { $State = @{} }
|
||||
if (-not $State.ContainsKey('lastShownDate')) { $State['lastShownDate'] = $null }
|
||||
if (-not $State.ContainsKey('ignoreUntilDate')) { $State['ignoreUntilDate'] = $null }
|
||||
|
||||
$today = (Get-Date).ToString('yyyy-MM-dd')
|
||||
|
||||
# Only notify once per day
|
||||
if ($State['lastShownDate'] -eq $today) {
|
||||
Write-Log -Level Info -Message 'Already checked today, skipping.' -Feature 'DefaultBrowser'
|
||||
return $State
|
||||
}
|
||||
|
||||
$regPath = 'HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice'
|
||||
|
||||
try {
|
||||
@@ -49,7 +144,14 @@ function Invoke-Feature {
|
||||
Write-Log -Level Info `
|
||||
-Message "Default browser OK: '$currentProgId'." `
|
||||
-Feature 'DefaultBrowser'
|
||||
$State['lastShownDate'] = $today
|
||||
$State['ignoreUntilDate'] = $null
|
||||
return $State
|
||||
}
|
||||
|
||||
if ($State['ignoreUntilDate'] -eq $today) {
|
||||
Write-Log -Level Info `
|
||||
-Message "Mismatch ignored for today (current '$currentProgId', expected '$($Config['targetProgId'])')." `
|
||||
-Feature 'DefaultBrowser'
|
||||
return $State
|
||||
}
|
||||
|
||||
@@ -57,17 +159,29 @@ function Invoke-Feature {
|
||||
-Message "Default browser mismatch — found '$currentProgId', expected '$($Config['targetProgId'])'." `
|
||||
-Feature 'DefaultBrowser'
|
||||
|
||||
# Note: Windows 11 blocks programmatic default-browser changes via registry hash protection.
|
||||
# We guide the user to the Settings page instead.
|
||||
Show-ToastNotification `
|
||||
-Title 'Default Browser' `
|
||||
-Body ("Default browser is '$currentProgId'. Click below to set it to $($Config['targetProgId']).") `
|
||||
-Body ("Default browser is '$currentProgId'. Do you want to change it now?") `
|
||||
-Buttons @(
|
||||
@{ Label = 'Open Default Apps Settings'; Action = 'ms-settings:defaultapps' },
|
||||
@{ Label = 'Dismiss'; Action = 'dismiss' }
|
||||
@{ Label = 'Yes'; Action = 'ms-settings:defaultapps' },
|
||||
@{ Label = 'No'; Action = 'dismiss' }
|
||||
)
|
||||
|
||||
$State['lastShownDate'] = $today
|
||||
$decision = Show-ConfirmationDialog `
|
||||
-Title 'Default Browser' `
|
||||
-Message ("Default browser is '$currentProgId', expected '$($Config['targetProgId'])'.`n`nDo you want to change it now?") `
|
||||
-Feature 'DefaultBrowser' `
|
||||
-Default 'No'
|
||||
|
||||
if ($decision -eq 'Yes') {
|
||||
[void](Invoke-DefaultBrowserGuidedChange -TargetProgId $Config['targetProgId'])
|
||||
$State['ignoreUntilDate'] = $null
|
||||
}
|
||||
else {
|
||||
$State['ignoreUntilDate'] = $today
|
||||
Write-Log -Level Info -Message 'User selected No; mismatch notifications suppressed until tomorrow.' -Feature 'DefaultBrowser'
|
||||
}
|
||||
|
||||
return $State
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ function Invoke-Feature {
|
||||
|
||||
if (-not $State) { $State = @{} }
|
||||
if (-not $State.ContainsKey('lastShownDate')) { $State['lastShownDate'] = $null }
|
||||
if (-not $State.ContainsKey('snoozeUntil')) { $State['snoozeUntil'] = $null }
|
||||
|
||||
$today = (Get-Date).ToString('yyyy-MM-dd')
|
||||
|
||||
@@ -54,6 +55,21 @@ function Invoke-Feature {
|
||||
return $State
|
||||
}
|
||||
|
||||
if ($State['snoozeUntil']) {
|
||||
try {
|
||||
$snoozeUntil = [datetime]::Parse($State['snoozeUntil'])
|
||||
if ((Get-Date) -lt $snoozeUntil) {
|
||||
Write-Log -Level Info `
|
||||
-Message ("Snoozed until {0}, skipping." -f $snoozeUntil.ToString('HH:mm:ss')) `
|
||||
-Feature 'SandwichReminder'
|
||||
return $State
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Log -Level Warn -Message "Invalid snoozeUntil value '$($State['snoozeUntil'])', ignoring." -Feature 'SandwichReminder'
|
||||
}
|
||||
}
|
||||
|
||||
# Parse reminder time
|
||||
try {
|
||||
$targetTime = [datetime]::ParseExact($Config['reminderTime'], 'HH:mm', $null)
|
||||
@@ -86,17 +102,48 @@ function Invoke-Feature {
|
||||
return $State
|
||||
}
|
||||
|
||||
Write-Log -Level Info -Message 'Showing sandwich reminder toast.' -Feature 'SandwichReminder'
|
||||
Write-Log -Level Info -Message 'Showing sandwich reminder confirmation dialog.' -Feature 'SandwichReminder'
|
||||
|
||||
Show-ToastNotification `
|
||||
-Title 'Sandwich Order' `
|
||||
-Body 'Do you want to order a sandwich today?' `
|
||||
-Buttons @(
|
||||
@{ Label = 'Yes, order now!'; Action = $Config['url'] },
|
||||
@{ Label = 'No thanks'; Action = 'dismiss' }
|
||||
$decision = Show-MultiChoiceDialog `
|
||||
-Title 'Sandwich Order' `
|
||||
-Message 'Do you want to order a sandwich today?' `
|
||||
-Feature 'SandwichReminder' `
|
||||
-DefaultValue 'snooze15' `
|
||||
-Choices @(
|
||||
@{ Label = 'Order now'; Value = 'order' },
|
||||
@{ Label = 'Snooze 15 min'; Value = 'snooze15' },
|
||||
@{ Label = 'Snooze 1h'; Value = 'snooze60' },
|
||||
@{ Label = 'No'; Value = 'no' }
|
||||
)
|
||||
|
||||
$State['lastShownDate'] = $today
|
||||
if ($decision -eq 'order') {
|
||||
try {
|
||||
Start-Process $Config['url']
|
||||
Write-Log -Level Info -Message "Opened sandwich order URL: $($Config['url'])" -Feature 'SandwichReminder'
|
||||
}
|
||||
catch {
|
||||
Write-Log -Level Error -Message "Failed to open sandwich order URL '$($Config['url'])': $_" -Feature 'SandwichReminder'
|
||||
}
|
||||
|
||||
$State['lastShownDate'] = $today
|
||||
$State['snoozeUntil'] = $null
|
||||
}
|
||||
elseif ($decision -eq 'snooze60') {
|
||||
$until = (Get-Date).AddHours(1)
|
||||
$State['snoozeUntil'] = $until.ToString('o')
|
||||
Write-Log -Level Info -Message ("User snoozed sandwich reminder for 1 hour (until {0})." -f $until.ToString('HH:mm:ss')) -Feature 'SandwichReminder'
|
||||
}
|
||||
elseif ($decision -eq 'no') {
|
||||
$State['lastShownDate'] = $today
|
||||
$State['snoozeUntil'] = $null
|
||||
Write-Log -Level Info -Message 'User selected No for sandwich reminder; skipping for rest of day.' -Feature 'SandwichReminder'
|
||||
}
|
||||
else {
|
||||
$until = (Get-Date).AddMinutes(15)
|
||||
$State['snoozeUntil'] = $until.ToString('o')
|
||||
Write-Log -Level Info -Message ("User snoozed sandwich reminder for 15 minutes (until {0})." -f $until.ToString('HH:mm:ss')) -Feature 'SandwichReminder'
|
||||
}
|
||||
|
||||
return $State
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# PromptHelper.ps1 — shared foreground/system-modal confirmation dialog helper.
|
||||
|
||||
function Show-ConfirmationDialog {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Title,
|
||||
[Parameter(Mandatory)][string]$Message,
|
||||
[string]$Feature = 'PromptHelper',
|
||||
[string]$Default = 'No'
|
||||
)
|
||||
|
||||
try {
|
||||
$shell = New-Object -ComObject WScript.Shell
|
||||
# 4=YesNo, 32=Question icon, 4096=System modal, 65536=Foreground
|
||||
$result = $shell.Popup($Message, 0, $Title, 4 + 32 + 4096 + 65536)
|
||||
|
||||
switch ($result) {
|
||||
6 { return 'Yes' }
|
||||
7 { return 'No' }
|
||||
default { return $Default }
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Log -Level Error -Message "Failed to show confirmation dialog '$Title': $_" -Feature $Feature
|
||||
return $Default
|
||||
}
|
||||
}
|
||||
|
||||
function Show-MultiChoiceDialog {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Title,
|
||||
[Parameter(Mandatory)][string]$Message,
|
||||
[Parameter(Mandatory)][object[]]$Choices,
|
||||
[string]$Feature = 'PromptHelper',
|
||||
[string]$DefaultValue = ''
|
||||
)
|
||||
|
||||
try {
|
||||
Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null
|
||||
Add-Type -AssemblyName System.Drawing -ErrorAction SilentlyContinue | Out-Null
|
||||
|
||||
if ($Choices.Count -lt 1 -or $Choices.Count -gt 4) {
|
||||
throw 'Show-MultiChoiceDialog expects 1 to 4 choices.'
|
||||
}
|
||||
|
||||
$form = New-Object System.Windows.Forms.Form
|
||||
$form.Text = $Title
|
||||
$form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
|
||||
$form.TopMost = $true
|
||||
$form.Width = 480
|
||||
$form.Height = 190
|
||||
$form.MinimizeBox = $false
|
||||
$form.MaximizeBox = $false
|
||||
$form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
|
||||
|
||||
$label = New-Object System.Windows.Forms.Label
|
||||
$label.AutoSize = $false
|
||||
$label.Left = 12
|
||||
$label.Top = 12
|
||||
$label.Width = 440
|
||||
$label.Height = 80
|
||||
$label.Text = $Message
|
||||
$form.Controls.Add($label)
|
||||
|
||||
$form.Tag = $DefaultValue
|
||||
$gap = 10
|
||||
$availableWidth = $form.ClientSize.Width - 24 - ($gap * ($Choices.Count - 1))
|
||||
$buttonWidth = [Math]::Min(130, [Math]::Floor($availableWidth / $Choices.Count))
|
||||
$totalWidth = ($buttonWidth * $Choices.Count) + ($gap * ($Choices.Count - 1))
|
||||
$startX = [Math]::Max(12, [int](($form.ClientSize.Width - $totalWidth) / 2))
|
||||
|
||||
for ($i = 0; $i -lt $Choices.Count; $i++) {
|
||||
$choice = $Choices[$i]
|
||||
$button = New-Object System.Windows.Forms.Button
|
||||
$button.Left = $startX + ($i * ($buttonWidth + $gap))
|
||||
$button.Top = 105
|
||||
$button.Width = $buttonWidth
|
||||
$button.Height = 30
|
||||
$button.Text = [string]$choice.Label
|
||||
$button.Tag = [string]$choice.Value
|
||||
$button.Add_Click({
|
||||
param($sender, $eventArgs)
|
||||
$dlg = $sender.FindForm()
|
||||
$dlg.Tag = [string]$sender.Tag
|
||||
$dlg.DialogResult = [System.Windows.Forms.DialogResult]::OK
|
||||
$dlg.Close()
|
||||
})
|
||||
$form.Controls.Add($button)
|
||||
if ($i -eq 0) {
|
||||
$form.AcceptButton = $button
|
||||
}
|
||||
}
|
||||
|
||||
[void]$form.ShowDialog()
|
||||
$result = [string]$form.Tag
|
||||
$form.Dispose()
|
||||
|
||||
return $result
|
||||
}
|
||||
catch {
|
||||
Write-Log -Level Error -Message "Failed to show multi-choice dialog '$Title': $_" -Feature $Feature
|
||||
return $DefaultValue
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ $script:InternalRoot = $PSScriptRoot # runner.ps1 lives in internal\
|
||||
. (Join-Path $InternalRoot 'lib\Logging.ps1')
|
||||
. (Join-Path $InternalRoot 'lib\NetworkUtils.ps1')
|
||||
. (Join-Path $InternalRoot 'lib\ToastHelper.ps1')
|
||||
. (Join-Path $InternalRoot 'lib\PromptHelper.ps1')
|
||||
. (Join-Path $InternalRoot 'lib\Config.ps1')
|
||||
|
||||
Write-Log -Level Info -Message '────── Runner started ──────' -Feature 'Runner'
|
||||
|
||||
Reference in New Issue
Block a user