dsh-notify-yimit 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,560 +1,782 @@
1
- <# dsh-notify-yimit 常驻浮窗宿主(Windows PowerShell + WPF)。
2
- 优化点:JSON序列化深度增加、Tag闭包变量传递优化、BeginInvoke资源回收、接收外部Label参数实现多语言。 #>
3
- Add-Type -AssemblyName PresentationFramework
4
- Add-Type -AssemblyName PresentationCore
5
- Add-Type -AssemblyName WindowsBase
6
-
7
- if ([System.Threading.Thread]::CurrentThread.ApartmentState -ne [System.Threading.ApartmentState]::STA) {
8
- [Console]::Error.WriteLine("dsh-notify-yimit: Host requires STA main thread (current $([System.Threading.Thread]::CurrentThread.ApartmentState))")
9
- exit 1
10
- }
11
-
12
- $script:utf8 = New-Object System.Text.UTF8Encoding($false)
13
- $script:stdout = New-Object System.IO.StreamWriter([System.Console]::OpenStandardOutput(), $script:utf8)
14
- $script:stdout.AutoFlush = $true
15
-
16
- function Send-Report([object]$obj) {
17
- try {
18
- $script:stdout.WriteLine(($obj | ConvertTo-Json -Compress -Depth 10))
19
- } catch { }
20
- }
21
-
22
- # 编辑模式几何回传:样板窗当前 left/top/width/height(工作区相对坐标)。
23
- function Send-Geometry($w) {
24
- try {
25
- $work = [System.Windows.SystemParameters]::WorkArea
26
- Send-Report @{
27
- type = 'geometry'
28
- left = [Math]::Round($w.Left - $work.Left)
29
- top = [Math]::Round($w.Top - $work.Top)
30
- width = [Math]::Round($w.ActualWidth)
31
- height = [Math]::Round($w.ActualHeight)
32
- }
33
- } catch { }
34
- }
35
-
36
- # 编辑模式样板窗:拖拽移动 + 右下角手柄缩放;拖拽/缩放结束后回传 geometry。
37
- # 位置与宽度写回配置后,所有后续浮窗按新几何弹出。
38
- function New-EditSample($cmd) {
39
- $bg = Parse-Color ([string]$cmd.bg) "#203a5c"
40
- $fg = Parse-Color ([string]$cmd.fg) "#e8f0fb"
41
- $bgBrush = New-Object System.Windows.Media.SolidColorBrush($bg)
42
- $fgBrush = New-Object System.Windows.Media.SolidColorBrush($fg)
43
-
44
- $win = New-Object System.Windows.Window
45
- $win.WindowStyle = [System.Windows.WindowStyle]::None
46
- $win.AllowsTransparency = $true
47
- $win.Background = [System.Windows.Media.Brushes]::Transparent
48
- $win.Topmost = $true
49
- $win.ShowInTaskbar = $false
50
- if ($null -ne $cmd.width) { $win.Width = [double]$cmd.width } else { $win.Width = 340 }
51
- $win.SizeToContent = [System.Windows.SizeToContent]::Height
52
-
53
- $border = New-Object System.Windows.Controls.Border
54
- $border.Background = $bgBrush
55
- $border.BorderBrush = $fgBrush
56
- $border.BorderThickness = New-Object System.Windows.Thickness(1)
57
- $border.CornerRadius = New-Object System.Windows.CornerRadius(12)
58
- $border.Margin = New-Object System.Windows.Thickness(12)
59
-
60
- $root = New-Object System.Windows.Controls.Grid
61
- $root.Margin = New-Object System.Windows.Thickness(14,12,14,12)
62
- $border.Child = $root
63
-
64
- $stack = New-Object System.Windows.Controls.StackPanel
65
- $titleText = New-Object System.Windows.Controls.TextBlock
66
- $titleText.Text = if ([string]::IsNullOrEmpty([string]$cmd.title)) { "Notification preview" } else { [string]$cmd.title }
67
- $titleText.Foreground = $fgBrush
68
- $titleText.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
69
- $titleText.FontSize = 13
70
- $titleText.FontWeight = [System.Windows.FontWeights]::SemiBold
71
- $titleText.Margin = New-Object System.Windows.Thickness(0,0,0,6)
72
- $stack.Children.Add($titleText) | Out-Null
73
- $bodyText = New-Object System.Windows.Controls.TextBlock
74
- $bodyText.Text = "拖动移动 · 拖拽窗口左右边缘调整宽度 · 完成后在设置页点「完成编辑」"
75
- $bodyText.Foreground = $fgBrush
76
- $bodyText.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
77
- $bodyText.FontSize = 12
78
- $bodyText.TextWrapping = [System.Windows.TextWrapping]::Wrap
79
- $stack.Children.Add($bodyText) | Out-Null
80
- $root.Children.Add($stack) | Out-Null
81
-
82
- # 左右边缘缩放条(像 Windows 窗口一样拖拽边缘调宽度):8px 宽、通高,
83
- # 半透明文字色填充提示可拖拽区域,光标为左右双向箭头。
84
- $edgeBrush = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromArgb(38, $fg.R, $fg.G, $fg.B))
85
- $leftStrip = New-Object System.Windows.Controls.Border
86
- $leftStrip.Width = 8
87
- $leftStrip.Cursor = [System.Windows.Input.Cursors]::SizeWE
88
- $leftStrip.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Left
89
- $leftStrip.VerticalAlignment = [System.Windows.VerticalAlignment]::Stretch
90
- $leftStrip.Background = $edgeBrush
91
- $leftStrip.CornerRadius = New-Object System.Windows.CornerRadius(12,0,0,12)
92
- $rightStrip = New-Object System.Windows.Controls.Border
93
- $rightStrip.Width = 8
94
- $rightStrip.Cursor = [System.Windows.Input.Cursors]::SizeWE
95
- $rightStrip.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Right
96
- $rightStrip.VerticalAlignment = [System.Windows.VerticalAlignment]::Stretch
97
- $rightStrip.Background = $edgeBrush
98
- $rightStrip.CornerRadius = New-Object System.Windows.CornerRadius(0,12,12,0)
99
-
100
- # 外层 Grid:边框 + 左右缩放条叠放(缩放条覆盖窗口左右边缘)
101
- $outer = New-Object System.Windows.Controls.Grid
102
- $outer.Children.Add($border) | Out-Null
103
- $outer.Children.Add($leftStrip) | Out-Null
104
- $outer.Children.Add($rightStrip) | Out-Null
105
-
106
- # 共享拖拽/缩放状态(事件回调看不到函数局部变量,放 Tag)
107
- $win.Tag = @{ resizing = $false; startX = 0.0; startWidth = 0.0; startLeft = 0.0; edge = 'right' }
108
-
109
- $win.Add_Loaded({
110
- $work = [System.Windows.SystemParameters]::WorkArea
111
- $pos = $cmd.position
112
- if ($null -ne $pos -and $null -ne $pos.left -and $null -ne $pos.top) {
113
- $win.Left = [double]$pos.left + $work.Left
114
- $win.Top = [double]$pos.top + $work.Top
115
- } else {
116
- $win.Left = $work.Right - $win.ActualWidth - 20
117
- $win.Top = $work.Bottom - $win.ActualHeight - 20
118
- }
119
- Send-Geometry $win
120
- }.GetNewClosure()) | Out-Null
121
-
122
- # 拖拽移动(缩放条区域除外):DragMove 阻塞至松开,结束后收敛到工作区内并回传几何。
123
- $win.Add_MouseLeftButtonDown({
124
- if ($win.Tag.resizing) { $win.Tag.resizing = $false; return }
125
- try {
126
- $win.DragMove()
127
- } catch { }
128
- try {
129
- $work = [System.Windows.SystemParameters]::WorkArea
130
- $win.Left = [Math]::Max($work.Left, [Math]::Min($win.Left, $work.Right - $win.ActualWidth))
131
- $win.Top = [Math]::Max($work.Top, [Math]::Min($win.Top, $work.Bottom - $win.ActualHeight))
132
- } catch { }
133
- Send-Geometry $win
134
- }.GetNewClosure()) | Out-Null
135
-
136
- # 拖拽边缘缩放宽度(260–520):右缘拖右变宽;左缘拖左变宽且右缘固定(窗口左移)。
137
- # 注意:$strip/$edge/$win 都作为函数参数,闭包才能可靠捕获。
138
- function Add-EdgeResize($strip, [string]$edge, $w) {
139
- $strip.Add_MouseLeftButtonDown({
140
- $w.Tag.resizing = $true
141
- $w.Tag.edge = $edge
142
- $w.Tag.startX = $_.GetPosition($w).X
143
- $w.Tag.startWidth = $w.ActualWidth
144
- $w.Tag.startLeft = $w.Left
145
- $strip.CaptureMouse()
146
- $_.Handled = $true
147
- }.GetNewClosure()) | Out-Null
148
- $strip.Add_MouseMove({
149
- if ($w.Tag.resizing) {
150
- $dx = $_.GetPosition($w).X - $w.Tag.startX
151
- if ($w.Tag.edge -eq 'left') {
152
- $nw = [Math]::Max(260, [Math]::Min(520, [Math]::Round($w.Tag.startWidth - $dx)))
153
- $w.Width = $nw
154
- $w.Left = $w.Tag.startLeft + ($w.Tag.startWidth - $w.Width)
155
- } else {
156
- $nw = [Math]::Max(260, [Math]::Min(520, [Math]::Round($w.Tag.startWidth + $dx)))
157
- $w.Width = $nw
158
- }
159
- $_.Handled = $true
160
- }
161
- }.GetNewClosure()) | Out-Null
162
- $strip.Add_MouseLeftButtonUp({
163
- if ($w.Tag.resizing) {
164
- $w.Tag.resizing = $false
165
- if ($strip.IsMouseCaptured) { $strip.ReleaseMouseCapture() }
166
- Send-Geometry $w
167
- $_.Handled = $true
168
- }
169
- }.GetNewClosure()) | Out-Null
170
- }
171
- Add-EdgeResize $leftStrip 'left' $win
172
- Add-EdgeResize $rightStrip 'right' $win
173
-
174
- $win.Add_Closed({
175
- if ($script:editSample -eq $win) { $script:editSample = $null }
176
- }.GetNewClosure()) | Out-Null
177
-
178
- $win.Content = $outer
179
- return $win
180
- }
181
-
182
- $script:toasts = @{}
183
- $script:exitRequested = $false
184
- # 编辑模式样板窗(不进入 toasts 注册表,不参与堆叠;edit-end 时关闭)。
185
- $script:editSample = $null
186
-
187
- function Parse-Color([string]$hex, [string]$fallback) {
188
- $h = $hex.TrimStart('#')
189
- if ($h.Length -eq 3) { $h = ($h.ToCharArray() | ForEach-Object { "$_$_" }) -join '' }
190
- if ($h.Length -eq 6) { $h = "FF$h" }
191
- if ($h.Length -ne 8 -or $h -notmatch '^[0-9a-fA-F]{8}$') {
192
- # 非法/缺失输入:改用回退色并同样做 3/6 位展开 + 补 FF(此前 FF 步骤在回退赋值之前,
193
- # 回退为 6 位时未补前缀,Substring 越界抛异常)。
194
- $h = $fallback.TrimStart('#')
195
- if ($h.Length -eq 3) { $h = ($h.ToCharArray() | ForEach-Object { "$_$_" }) -join '' }
196
- if ($h.Length -eq 6) { $h = "FF$h" }
197
- }
198
- return [System.Windows.Media.Color]::FromArgb(
199
- [Convert]::ToInt32($h.Substring(0,2), 16),
200
- [Convert]::ToInt32($h.Substring(2,2), 16),
201
- [Convert]::ToInt32($h.Substring(4,2), 16),
202
- [Convert]::ToInt32($h.Substring(6,2), 16))
203
- }
204
-
205
- $downP = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(158,164,170))
206
- $downI = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(30,36,44))
207
- $hoverP = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(196,202,208))
208
- $hoverI = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(44,52,62))
209
-
210
- function New-ToastButton([string]$label, [scriptblock]$onClick, [bool]$primary, [System.Windows.Media.SolidColorBrush]$fg, [System.Windows.Media.SolidColorBrush]$bg) {
211
- $bd = New-Object System.Windows.Controls.Border
212
- $bd.CornerRadius = New-Object System.Windows.CornerRadius(10)
213
- $bd.Padding = New-Object System.Windows.Thickness(18,7,18,7)
214
- $bd.Margin = New-Object System.Windows.Thickness(0,0,8,0)
215
- $bd.Cursor = [System.Windows.Input.Cursors]::Hand
216
- $bd.Focusable = $false
217
-
218
- $txt = New-Object System.Windows.Controls.TextBlock
219
- $txt.Text = $label
220
- $txt.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
221
- $txt.FontSize = 12
222
- $txt.VerticalAlignment = [System.Windows.VerticalAlignment]::Center
223
- $txt.IsHitTestVisible = $false
224
-
225
- if ($primary) {
226
- $bd.Background = $fg
227
- $txt.Foreground = $bg
228
- $bd.BorderBrush = [System.Windows.Media.Brushes]::Transparent
229
- $bd.BorderThickness = New-Object System.Windows.Thickness(0)
230
- } else {
231
- $bd.Background = [System.Windows.Media.Brushes]::Transparent
232
- $txt.Foreground = $fg
233
- $bd.BorderBrush = $fg
234
- $bd.BorderThickness = New-Object System.Windows.Thickness(1)
235
- }
236
- $bd.Child = $txt
237
-
238
- $bd.Tag = @{ primary = $primary; onClick = $onClick; hoverP = $hoverP; hoverI = $hoverI; downP = $downP; downI = $downI; fg = $fg; bg = $bg }
239
-
240
- $bd.Add_MouseEnter({
241
- $info = $this.Tag
242
- $this.Background = if ($info.primary) { $info.hoverP } else { $info.hoverI }
243
- }) | Out-Null
244
-
245
- $bd.Add_MouseLeave({
246
- $info = $this.Tag
247
- $this.Background = if ($info.primary) { $info.fg } else { [System.Windows.Media.Brushes]::Transparent }
248
- }) | Out-Null
249
-
250
- $bd.Add_MouseLeftButtonDown({
251
- $info = $this.Tag
252
- $this.Background = if ($info.primary) { $info.downP } else { $info.downI }
253
- }) | Out-Null
254
-
255
- $bd.Add_MouseLeftButtonUp({
256
- $info = $this.Tag
257
- try { if ($info.onClick -ne $null) { & $info.onClick } } catch { }
258
- $this.Background = if ($info.primary) { $info.fg } else { [System.Windows.Media.Brushes]::Transparent }
259
- }) | Out-Null
260
-
261
- return $bd
262
- }
263
-
264
- $easeMove = New-Object System.Windows.Media.Animation.QuadraticEase
265
- $easeMove.EasingMode = [System.Windows.Media.Animation.EasingMode]::EaseOut
266
- $easeExit = New-Object System.Windows.Media.Animation.CubicEase
267
- $easeExit.EasingMode = [System.Windows.Media.Animation.EasingMode]::EaseIn
268
-
269
- function Close-WithFade($w) {
270
- try {
271
- if ($w.Tag -eq 'closing') { return }
272
- $w.Tag = 'closing'
273
- $fadeOut = New-Object System.Windows.Media.Animation.DoubleAnimation(1, 0, [TimeSpan]::FromMilliseconds(160))
274
- $fadeOut.EasingFunction = $easeExit
275
- $fadeOut.Add_Completed({ try { $w.Close() } catch { } }.GetNewClosure()) | Out-Null
276
- $w.BeginAnimation([System.Windows.Window]::OpacityProperty, $fadeOut)
277
-
278
- $dropAn = New-Object System.Windows.Media.Animation.DoubleAnimation($w.Top, ($w.Top + 20), [TimeSpan]::FromMilliseconds(160))
279
- $dropAn.EasingFunction = $easeExit
280
- $w.BeginAnimation([System.Windows.Window]::TopProperty, $dropAn)
281
-
282
- $backstop = New-Object System.Windows.Threading.DispatcherTimer
283
- $backstop.Interval = [TimeSpan]::FromMilliseconds(600)
284
- $backstop.Tag = $w
285
- $backstop.Add_Tick({
286
- $this.Stop()
287
- try { if ($this.Tag.IsVisible) { $this.Tag.Close() } } catch { }
288
- }) | Out-Null
289
- $backstop.Start()
290
- } catch {
291
- try { $w.Close() } catch { }
292
- }
293
- }
294
-
295
- function New-ToastWindow($cmd) {
296
- $bg = Parse-Color ([string]$cmd.bg) "#20272f"
297
- $fg = Parse-Color ([string]$cmd.fg) "#e6edf3"
298
- $bgBrush = New-Object System.Windows.Media.SolidColorBrush($bg)
299
- $fgBrush = New-Object System.Windows.Media.SolidColorBrush($fg)
300
-
301
- $win = New-Object System.Windows.Window
302
- $win.WindowStyle = [System.Windows.WindowStyle]::None
303
- $win.AllowsTransparency = $true
304
- $win.Background = [System.Windows.Media.Brushes]::Transparent
305
- $win.Topmost = $true
306
- $win.ShowInTaskbar = $false
307
- # 通知窗口样式:宽度来自配置(默认 340);高度自适应内容。
308
- if ($null -ne $cmd.width) { $win.Width = [double]$cmd.width } else { $win.Width = 340 }
309
- $win.SizeToContent = [System.Windows.SizeToContent]::Height
310
- if (-not [string]::IsNullOrEmpty([string]$cmd.winTitle)) { $win.Title = [string]$cmd.winTitle }
311
-
312
- $border = New-Object System.Windows.Controls.Border
313
- $border.Background = $bgBrush
314
- $border.BorderBrush = $fgBrush
315
- $border.BorderThickness = New-Object System.Windows.Thickness(1)
316
- $border.CornerRadius = New-Object System.Windows.CornerRadius(12)
317
- $border.Margin = New-Object System.Windows.Thickness(12)
318
-
319
- $root = New-Object System.Windows.Controls.StackPanel
320
- $root.Margin = New-Object System.Windows.Thickness(14,12,14,12)
321
- $border.Child = $root
322
-
323
- # 优化:支持多语言,接收传入的 label,如果没有则使用后备英文
324
- $unnamedLabel = if ([string]::IsNullOrEmpty([string]$cmd.unnamedLabel)) { "(Unnamed session)" } else { [string]$cmd.unnamedLabel }
325
-
326
- $titleText = New-Object System.Windows.Controls.TextBlock
327
- $titleText.Text = if ([string]::IsNullOrEmpty([string]$cmd.title)) { $unnamedLabel } else { [string]$cmd.title }
328
- $titleText.Foreground = $fgBrush
329
- $titleText.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
330
- $titleText.FontSize = 13
331
- $titleText.FontWeight = [System.Windows.FontWeights]::SemiBold
332
- $titleText.TextTrimming = [System.Windows.TextTrimming]::CharacterEllipsis
333
- $titleText.Margin = New-Object System.Windows.Thickness(0,0,0,6)
334
- $root.Children.Add($titleText) | Out-Null
335
-
336
- $bodyText = New-Object System.Windows.Controls.TextBlock
337
- $bodyText.Text = [string]$cmd.text
338
- $bodyText.Foreground = $fgBrush
339
- $bodyText.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
340
- $bodyText.FontSize = 13
341
- $bodyText.TextWrapping = [System.Windows.TextWrapping]::Wrap
342
- $bodyText.Margin = New-Object System.Windows.Thickness(0,0,0,10)
343
- $root.Children.Add($bodyText) | Out-Null
344
-
345
- $btnRow = New-Object System.Windows.Controls.StackPanel
346
- $btnRow.Orientation = [System.Windows.Controls.Orientation]::Horizontal
347
- $btnRow.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Right
348
-
349
- $ignoreLabel = if ([string]::IsNullOrEmpty([string]$cmd.ignoreLabel)) { "Ignore" } else { [string]$cmd.ignoreLabel }
350
- $ignoreBtn = New-ToastButton $ignoreLabel ({ Close-WithFade $win }.GetNewClosure()) $false $fgBrush $bgBrush
351
- $btnRow.Children.Add($ignoreBtn) | Out-Null
352
-
353
- if (-not [string]::IsNullOrEmpty([string]$cmd.sessionId)) {
354
- $jumpLabel = if ([string]::IsNullOrEmpty([string]$cmd.jumpLabel)) { "Open" } else { [string]$cmd.jumpLabel }
355
- $jumpBtn = New-ToastButton $jumpLabel ({
356
- $url = "$($cmd.baseUrl)/#dsh-notify-yimit/session=$($cmd.sessionId)"
357
- try {
358
- if (-not [string]::IsNullOrEmpty([string]$cmd.browserPath)) {
359
- Start-Process -FilePath ([string]$cmd.browserPath) -ArgumentList $url
360
- } else { Start-Process $url }
361
- } catch { }
362
- Close-WithFade $win
363
- }.GetNewClosure()) $true $fgBrush $bgBrush
364
- $btnRow.Children.Add($jumpBtn) | Out-Null
365
- }
366
- $root.Children.Add($btnRow) | Out-Null
367
- $win.Content = $border
368
-
369
- $win.Add_Loaded({
370
- $work = [System.Windows.SystemParameters]::WorkArea
371
- # 通知窗口位置:配置了自定义位置(position)则按其坐标(工作区相对)+ 堆叠偏移;
372
- # 否则默认右下贴边(右/下留边 20)。
373
- $pos = $cmd.position
374
- if ($null -ne $pos -and $null -ne $pos.left -and $null -ne $pos.top) {
375
- $targetLeft = [double]$pos.left + $work.Left
376
- $targetTop = [double]$pos.top + $work.Top - [int]$cmd.offsetY
377
- } else {
378
- $targetLeft = $work.Right - $win.ActualWidth - 20
379
- $targetTop = $work.Bottom - $win.ActualHeight - 20 - [int]$cmd.offsetY
380
- }
381
- $win.Left = $targetLeft
382
- $win.Top = $targetTop + 32
383
- $win.Opacity = 0
384
- try {
385
- $easingIn = New-Object System.Windows.Media.Animation.QuadraticEase
386
- $easingIn.EasingMode = [System.Windows.Media.Animation.EasingMode]::EaseOut
387
- $slideIn = New-Object System.Windows.Media.Animation.DoubleAnimation($targetTop + 32, $targetTop, [TimeSpan]::FromMilliseconds(260))
388
- $slideIn.EasingFunction = $easingIn
389
- $win.BeginAnimation([System.Windows.Window]::TopProperty, $slideIn)
390
- $fadeIn = New-Object System.Windows.Media.Animation.DoubleAnimation(0, 1, [TimeSpan]::FromMilliseconds(180))
391
- $fadeIn.EasingFunction = $easingIn
392
- $win.BeginAnimation([System.Windows.Window]::OpacityProperty, $fadeIn)
393
-
394
- $guard = New-Object System.Windows.Threading.DispatcherTimer
395
- $guard.Interval = [TimeSpan]::FromMilliseconds(400)
396
- $guard.Tag = @{ w = $win; top = $targetTop }
397
- $guard.Add_Tick({
398
- $this.Stop()
399
- $g = $this.Tag
400
- try {
401
- if ($g.w.Opacity -lt 0.9) { $g.w.Opacity = 1 }
402
- if ($g.w.Top -ne $g.top) { $g.w.Top = $g.top }
403
- } catch { }
404
- }) | Out-Null
405
- $guard.Start()
406
- } catch {
407
- $win.Opacity = 1
408
- $win.Top = $targetTop
409
- }
410
- try {
411
- $hwnd = [System.Windows.Interop.WindowInteropHelper]::new($win).Handle
412
- Send-Report @{ type = 'pos'; key = [string]$cmd.key; instance = [int]$cmd.instance
413
- left = [Math]::Round($targetLeft); top = [Math]::Round($targetTop)
414
- height = [Math]::Round($win.ActualHeight); hwnd = $hwnd.ToInt64()
415
- workBottom = [Math]::Round($work.Bottom) }
416
- } catch { }
417
- }.GetNewClosure()) | Out-Null
418
-
419
- $toastsRef = $script:toasts
420
- $win.Add_Closed({
421
- if ($toastsRef.ContainsKey([string]$cmd.key)) {
422
- $rec = $toastsRef[[string]$cmd.key]
423
- if ($rec.Timer -ne $null) { try { $rec.Timer.Stop() } catch { } }
424
- $toastsRef.Remove([string]$cmd.key) | Out-Null
425
- }
426
- Send-Report @{ type = 'exit'; key = [string]$cmd.key; instance = [int]$cmd.instance }
427
- }.GetNewClosure()) | Out-Null
428
-
429
- $rec = @{ Win = $win; Body = $bodyText; Title = $titleText; Timer = $null; Instance = [int]$cmd.instance }
430
- $script:toasts[[string]$cmd.key] = $rec
431
-
432
- if (-not ($cmd.sticky -eq $true)) {
433
- $dur = [Math]::Max(1, [int]$cmd.durationSec)
434
- $timer = New-Object System.Windows.Threading.DispatcherTimer
435
- $timer.Interval = [TimeSpan]::FromSeconds($dur)
436
- $timer.Add_Tick({ $this.Stop(); Close-WithFade $win }.GetNewClosure()) | Out-Null
437
- $timer.Start()
438
- $rec.Timer = $timer
439
- }
440
- return $win
441
- }
442
-
443
- function Invoke-Command([string]$line) {
444
- $line = $line.Trim()
445
- if ([string]::IsNullOrEmpty($line)) { return }
446
- $cmd = $null
447
- try { $cmd = $line | ConvertFrom-Json } catch { return }
448
- if ($cmd -eq $null -or [string]::IsNullOrEmpty([string]$cmd.cmd)) { return }
449
-
450
- switch ([string]$cmd.cmd) {
451
- 'show' {
452
- if ($script:toasts.ContainsKey([string]$cmd.key)) {
453
- $old = $script:toasts[[string]$cmd.key]
454
- try { $old.Win.Close() } catch { }
455
- $script:toasts.Remove([string]$cmd.key) | Out-Null
456
- }
457
- $w = New-ToastWindow $cmd
458
- if ($w -ne $null) { $w.Show() }
459
- }
460
- 'text' {
461
- $rec = $script:toasts[[string]$cmd.key]
462
- if ($rec -ne $null -and $rec.Body -ne $null) { $rec.Body.Text = [string]$cmd.text }
463
- }
464
- 'title' {
465
- $rec = $script:toasts[[string]$cmd.key]
466
- if ($rec -ne $null -and $rec.Title -ne $null) { $rec.Title.Text = [string]$cmd.title }
467
- }
468
- 'move' {
469
- $rec = $script:toasts[[string]$cmd.key]
470
- if ($rec -ne $null) {
471
- $top = [int]$cmd.top
472
- $slide = New-Object System.Windows.Media.Animation.DoubleAnimation($rec.Win.Top, $top, [TimeSpan]::FromMilliseconds(220))
473
- $slide.EasingFunction = $easeMove
474
- $rec.Win.BeginAnimation([System.Windows.Window]::TopProperty, $slide)
475
- # 可选横向定位:left 数字 = 绝对 X(工作区相对);left 'default' = 默认右下贴边
476
- # (工作区右缘 - 宽度 - 20)。用于编辑调整位置/恢复默认后,在屏浮窗立即到位。
477
- if ($null -ne $cmd.left) {
478
- $newLeft = 0.0
479
- if ($cmd.left -eq 'default') {
480
- $work = [System.Windows.SystemParameters]::WorkArea
481
- $newLeft = $work.Right - $rec.Win.ActualWidth - 20
482
- } else {
483
- $newLeft = [double]$cmd.left
484
- }
485
- $slideL = New-Object System.Windows.Media.Animation.DoubleAnimation($rec.Win.Left, $newLeft, [TimeSpan]::FromMilliseconds(220))
486
- $slideL.EasingFunction = $easeMove
487
- $rec.Win.BeginAnimation([System.Windows.Window]::LeftProperty, $slideL)
488
- }
489
- }
490
- }
491
- 'close' {
492
- $rec = $script:toasts[[string]$cmd.key]
493
- if ($rec -ne $null) { Close-WithFade $rec.Win }
494
- }
495
- 'size' {
496
- $rec = $script:toasts[[string]$cmd.key]
497
- if ($rec -ne $null -and $null -ne $cmd.width) { $rec.Win.Width = [double]$cmd.width }
498
- }
499
- 'edit' {
500
- # 编辑模式:关闭旧样板(如有),弹新样板窗(可拖拽/缩放)。
501
- if ($script:editSample -ne $null) { try { $script:editSample.Close() } catch { } ; $script:editSample = $null }
502
- $w = New-EditSample $cmd
503
- if ($w -ne $null) { $w.Show(); $script:editSample = $w }
504
- }
505
- 'edit-end' {
506
- if ($script:editSample -ne $null) { try { $script:editSample.Close() } catch { } ; $script:editSample = $null }
507
- }
508
- 'shutdown' { $script:exitRequested = $true }
509
- }
510
- }
511
-
512
- $script:cmdQueue = New-Object 'System.Collections.Concurrent.ConcurrentQueue[string]'
513
- $readerScript = {
514
- param($q)
515
- try {
516
- $reader = New-Object System.IO.StreamReader([System.Console]::OpenStandardInput(), (New-Object System.Text.UTF8Encoding($false)))
517
- while ($true) {
518
- $line = $reader.ReadLine()
519
- if ($null -eq $line) { break }
520
- $q.Enqueue($line)
521
- }
522
- } catch { }
523
- $q.Enqueue('{"cmd":"shutdown"}')
524
- }
525
-
526
- $readerRs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
527
- $readerRs.Open()
528
- $readerPs = [System.Management.Automation.PowerShell]::Create()
529
- $readerPs.Runspace = $readerRs
530
- $readerPs.AddScript($readerScript).AddArgument($script:cmdQueue) | Out-Null
531
- $readerAsyncResult = $readerPs.BeginInvoke()
532
-
533
- $cmdTimer = New-Object System.Windows.Threading.DispatcherTimer
534
- $cmdTimer.Interval = [TimeSpan]::FromMilliseconds(50)
535
- $cmdTimer.Add_Tick({
536
- try {
537
- $line = $null
538
- while ($script:cmdQueue.TryDequeue([ref]$line)) {
539
- if (-not [string]::IsNullOrEmpty($line)) { Invoke-Command $line }
540
- if ($script:exitRequested) { break }
541
- }
542
- if ($script:exitRequested) {
543
- $this.Stop()
544
- foreach ($key in @($script:toasts.Keys)) {
545
- try { $script:toasts[$key].Win.Close() } catch { }
546
- }
547
- if ($script:editSample -ne $null) {
548
- try { $script:editSample.Close() } catch { }
549
- $script:editSample = $null
550
- }
551
- [System.Windows.Threading.Dispatcher]::CurrentDispatcher.InvokeShutdown()
552
- }
553
- } catch { }
554
- }) | Out-Null
555
- $cmdTimer.Start()
556
- [System.Windows.Threading.Dispatcher]::Run()
557
-
558
- try { if ($readerAsyncResult -ne $null) { $readerPs.EndInvoke($readerAsyncResult) } } catch { }
559
- try { $readerPs.Dispose() } catch { }
560
- try { $readerRs.Dispose() } catch { }
1
+ <# dsh-notify-yimit 常驻浮窗宿主(Windows PowerShell + WPF)。
2
+
3
+ 倒计时即进度(本版核心变化):
4
+ - 非粘性通知整卡抽象为倒计时:强调色 @12% "液面层"锚定左缘,ScaleX 1→0,
5
+ 右缘向左退去 —— 时间从右向左流走,剩余时间 = 剩余液面面积;
6
+ - 独立 2.5px 强调色"进度指针"亮线贴液面右缘同步左移(不被 ScaleX 压扁,
7
+ 在 bg≈accent 相近配色下仍 100% 饱和可辨,是可见性兜底信号);
8
+ - 动画严格线性(无缓动),保证视觉时长与真实剩余时间一致;
9
+ - 粘性通知(运行中/待审批/待回答)无倒计时,左侧强调色条改为呼吸脉动,
10
+ 与"会消失的卡"一眼区分。
11
+
12
+ 其余特性:
13
+ - 强调色(accent)体系:左侧类型色条 + 强调色描边 + 强调色主按钮 + 辉光阴影;
14
+ - 编辑模式样板窗:5 类型实时色卡预览 + 边缘圆点缩放把手 + 多语言提示;
15
+ - JSON 序列化深度增加、Tag 闭包变量传递、接收外部 Label 参数实现多语言。 #>
16
+
17
+ Add-Type -AssemblyName PresentationFramework
18
+ Add-Type -AssemblyName PresentationCore
19
+ Add-Type -AssemblyName WindowsBase
20
+
21
+ if ([System.Threading.Thread]::CurrentThread.ApartmentState -ne [System.Threading.ApartmentState]::STA) {
22
+ [Console]::Error.WriteLine("dsh-notify-yimit: Host requires STA main thread (current $([System.Threading.Thread]::CurrentThread.ApartmentState))")
23
+ exit 1
24
+ }
25
+
26
+ $script:utf8 = New-Object System.Text.UTF8Encoding($false)
27
+ $script:stdout = New-Object System.IO.StreamWriter([System.Console]::OpenStandardOutput(), $script:utf8)
28
+ $script:stdout.AutoFlush = $true
29
+
30
+ function Send-Report([object]$obj) {
31
+ try { $script:stdout.WriteLine(($obj | ConvertTo-Json -Compress -Depth 10)) } catch { }
32
+ }
33
+
34
+ # ───────────────────────── 颜色 / 效果工具 ─────────────────────────
35
+
36
+ function Convert-Hsl([System.Windows.Media.Color]$c, [double]$lightFactor) {
37
+ # 简易亮度调整:lightFactor > 1 提亮, < 1 压暗(用于主按钮 hover/按压变体)。
38
+ $f = [Math]::Max(0.0, $lightFactor)
39
+ return [System.Windows.Media.Color]::FromRgb(
40
+ [byte][Math]::Min(255, [int]($c.R * $f)),
41
+ [byte][Math]::Min(255, [int]($c.G * $f)),
42
+ [byte]([Math]::Min(255, [int]($c.B * $f))))
43
+ }
44
+
45
+ function New-GlowEffect([System.Windows.Media.Color]$c, [double]$opacity) {
46
+ # 强调色辉光阴影:无位移、大模糊,营造"类型色悬浮"感。
47
+ $effect = New-Object System.Windows.Media.Effects.DropShadowEffect
48
+ $effect.Color = $c
49
+ $effect.BlurRadius = 24
50
+ $effect.ShadowDepth = 0
51
+ $effect.Opacity = $opacity
52
+ return $effect
53
+ }
54
+
55
+ function Add-GridRow($grid) {
56
+ $row = New-Object System.Windows.Controls.RowDefinition
57
+ $row.Height = [System.Windows.GridLength]::Auto
58
+ $grid.RowDefinitions.Add($row) | Out-Null
59
+ }
60
+
61
+ # 编辑模式几何回传:样板窗当前 left/top/width/height(工作区相对坐标)。
62
+ function Send-Geometry($w) {
63
+ try {
64
+ $work = [System.Windows.SystemParameters]::WorkArea
65
+ Send-Report @{
66
+ type = 'geometry'
67
+ left = [Math]::Round($w.Left - $work.Left)
68
+ top = [Math]::Round($w.Top - $work.Top)
69
+ width = [Math]::Round($w.ActualWidth)
70
+ height = [Math]::Round($w.ActualHeight)
71
+ }
72
+ } catch { }
73
+ }
74
+
75
+ # ───────────────────────── 编辑模式样板窗 ─────────────────────────
76
+
77
+ # 编辑模式样板窗:拖拽移动 + 左右边缘圆点把手缩放;拖拽/缩放结束后回传 geometry。
78
+ # 强调色描边 + 辉光;内嵌 5 类型实时色卡预览(chips);多语言提示。
79
+ function New-EditSample($cmd) {
80
+ $bg = Parse-Color ([string]$cmd.bg) "#203a5c"
81
+ $fg = Parse-Color ([string]$cmd.fg) "#e8f0fb"
82
+ $accent = Parse-Color ([string]$cmd.accent) "#60a5fa"
83
+ $bgBrush = New-Object System.Windows.Media.SolidColorBrush($bg)
84
+ $fgBrush = New-Object System.Windows.Media.SolidColorBrush($fg)
85
+ $accentBrush = New-Object System.Windows.Media.SolidColorBrush($accent)
86
+
87
+ $win = New-Object System.Windows.Window
88
+ $win.WindowStyle = [System.Windows.WindowStyle]::None
89
+ $win.AllowsTransparency = $true
90
+ $win.Background = [System.Windows.Media.Brushes]::Transparent
91
+ $win.Topmost = $true
92
+ $win.ShowInTaskbar = $false
93
+ if ($null -ne $cmd.width) { $win.Width = [double]$cmd.width } else { $win.Width = 340 }
94
+ $win.SizeToContent = [System.Windows.SizeToContent]::Height
95
+ $win.Effect = New-GlowEffect $accent 0.25
96
+
97
+ $border = New-Object System.Windows.Controls.Border
98
+ $border.Background = $bgBrush
99
+ $border.BorderBrush = $accentBrush
100
+ $border.BorderThickness = New-Object System.Windows.Thickness(1)
101
+ $border.CornerRadius = New-Object System.Windows.CornerRadius(14)
102
+ $border.Margin = New-Object System.Windows.Thickness(12)
103
+
104
+ $root = New-Object System.Windows.Controls.StackPanel
105
+ $root.Margin = New-Object System.Windows.Thickness(16, 14, 16, 14)
106
+ $border.Child = $root
107
+
108
+ # 标题(带强调色小色块前缀)
109
+ $titleRow = New-Object System.Windows.Controls.StackPanel
110
+ $titleRow.Orientation = [System.Windows.Controls.Orientation]::Horizontal
111
+ $titleRow.Margin = New-Object System.Windows.Thickness(0, 0, 0, 8)
112
+ $titleDot = New-Object System.Windows.Controls.Border
113
+ $titleDot.Width = 4
114
+ $titleDot.Height = 14
115
+ $titleDot.CornerRadius = New-Object System.Windows.CornerRadius(2)
116
+ $titleDot.Background = $accentBrush
117
+ $titleDot.Margin = New-Object System.Windows.Thickness(0, 0, 8, 0)
118
+ $titleDot.VerticalAlignment = [System.Windows.VerticalAlignment]::Center
119
+ $titleRow.Children.Add($titleDot) | Out-Null
120
+ $titleText = New-Object System.Windows.Controls.TextBlock
121
+ $titleText.Text = if ([string]::IsNullOrEmpty([string]$cmd.title)) { "Notification preview" } else { [string]$cmd.title }
122
+ $titleText.Foreground = $fgBrush
123
+ $titleText.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
124
+ $titleText.FontSize = 13
125
+ $titleText.FontWeight = [System.Windows.FontWeights]::SemiBold
126
+ $titleText.VerticalAlignment = [System.Windows.VerticalAlignment]::Center
127
+ $titleRow.Children.Add($titleText) | Out-Null
128
+ $root.Children.Add($titleRow) | Out-Null
129
+
130
+ # 5 类型实时色卡预览:浮窗色条/描边/主按钮所见即所得
131
+ $chips = @($cmd.chips)
132
+ if ($chips.Count -gt 0) {
133
+ $chipRow = New-Object System.Windows.Controls.WrapPanel
134
+ $chipRow.Margin = New-Object System.Windows.Thickness(0, 0, 0, 8)
135
+ foreach ($c in $chips) {
136
+ if ($c -eq $null) { continue }
137
+ $chipAccent = Parse-Color ([string]$c.accent) "#60a5fa"
138
+ $chipBg = Parse-Color ([string]$c.bg) "#20272f"
139
+ $chip = New-Object System.Windows.Controls.Border
140
+ $chip.CornerRadius = New-Object System.Windows.CornerRadius(8)
141
+ $chip.Padding = New-Object System.Windows.Thickness(8, 3, 8, 4)
142
+ $chip.Margin = New-Object System.Windows.Thickness(0, 0, 6, 4)
143
+ $chip.Background = New-Object System.Windows.Media.SolidColorBrush($chipBg)
144
+ $chip.BorderBrush = New-Object System.Windows.Media.SolidColorBrush($chipAccent)
145
+ $chip.BorderThickness = New-Object System.Windows.Thickness(1)
146
+ $chipText = New-Object System.Windows.Controls.TextBlock
147
+ $chipText.Text = [string]$c.label
148
+ $chipText.Foreground = New-Object System.Windows.Media.SolidColorBrush($chipAccent)
149
+ $chipText.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
150
+ $chipText.FontSize = 11
151
+ $chipText.FontWeight = [System.Windows.FontWeights]::SemiBold
152
+ $chip.Child = $chipText
153
+ $chipRow.Children.Add($chip) | Out-Null
154
+ }
155
+ $root.Children.Add($chipRow) | Out-Null
156
+ }
157
+
158
+ # 提示文案(多语言,由 startEditMode 传入 hint)
159
+ $bodyText = New-Object System.Windows.Controls.TextBlock
160
+ $bodyText.Text = if ([string]::IsNullOrEmpty([string]$cmd.hint)) {
161
+ "Drag to move · Drag window edges to resize width · Click 'Finish editing' when done"
162
+ } else { [string]$cmd.hint }
163
+ $bodyText.Foreground = $fgBrush
164
+ $bodyText.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
165
+ $bodyText.FontSize = 12
166
+ $bodyText.TextWrapping = [System.Windows.TextWrapping]::Wrap
167
+ $root.Children.Add($bodyText) | Out-Null
168
+
169
+ # 左右边缘缩放把手:8px 通高,强调色半透明填充 + 三圆点把手,
170
+ # 悬停时完全不透明提示可拖拽区域,光标为左右双向箭头。
171
+ $edgeBrush = New-Object System.Windows.Media.SolidColorBrush(
172
+ [System.Windows.Media.Color]::FromArgb(115, $accent.R, $accent.G, $accent.B))
173
+ $leftStrip = New-Object System.Windows.Controls.Border
174
+ $leftStrip.Width = 8
175
+ $leftStrip.Cursor = [System.Windows.Input.Cursors]::SizeWE
176
+ $leftStrip.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Left
177
+ $leftStrip.VerticalAlignment = [System.Windows.VerticalAlignment]::Stretch
178
+ $leftStrip.Background = $edgeBrush
179
+ $leftStrip.CornerRadius = New-Object System.Windows.CornerRadius(14, 0, 0, 14)
180
+ $rightStrip = New-Object System.Windows.Controls.Border
181
+ $rightStrip.Width = 8
182
+ $rightStrip.Cursor = [System.Windows.Input.Cursors]::SizeWE
183
+ $rightStrip.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Right
184
+ $rightStrip.VerticalAlignment = [System.Windows.VerticalAlignment]::Stretch
185
+ $rightStrip.Background = $edgeBrush
186
+ $rightStrip.CornerRadius = New-Object System.Windows.CornerRadius(0, 14, 14, 0)
187
+
188
+ function Add-GripDots($strip, $dotColor) {
189
+ $dots = New-Object System.Windows.Controls.StackPanel
190
+ $dots.Orientation = [System.Windows.Controls.Orientation]::Horizontal
191
+ $dots.VerticalAlignment = [System.Windows.VerticalAlignment]::Center
192
+ $dots.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Center
193
+ foreach ($i in 1..3) {
194
+ $dot = New-Object System.Windows.Shapes.Ellipse
195
+ $dot.Width = 3; $dot.Height = 3
196
+ $dot.Margin = New-Object System.Windows.Thickness(1, 0, 1, 0)
197
+ $dot.Fill = New-Object System.Windows.Media.SolidColorBrush(
198
+ [System.Windows.Media.Color]::FromArgb(170, $dotColor.R, $dotColor.G, $dotColor.B))
199
+ $dots.Children.Add($dot) | Out-Null
200
+ }
201
+ $strip.Child = $dots
202
+ $strip.Opacity = 0.75
203
+ $strip.Add_MouseEnter({ $this.Opacity = 1.0 }) | Out-Null
204
+ $strip.Add_MouseLeave({ $this.Opacity = 0.75 }) | Out-Null
205
+ }
206
+ Add-GripDots $leftStrip $fg
207
+ Add-GripDots $rightStrip $fg
208
+
209
+ # 外层 Grid:边框 + 左右缩放把手叠放(把手覆盖窗口左右边缘)
210
+ $outer = New-Object System.Windows.Controls.Grid
211
+ $outer.Children.Add($border) | Out-Null
212
+ $outer.Children.Add($leftStrip) | Out-Null
213
+ $outer.Children.Add($rightStrip) | Out-Null
214
+
215
+ # 共享拖拽/缩放状态(事件回调看不到函数局部变量,放 Tag)
216
+ $win.Tag = @{ resizing = $false; startX = 0.0; startWidth = 0.0; startLeft = 0.0; edge = 'right' }
217
+
218
+ $win.Add_Loaded({
219
+ $work = [System.Windows.SystemParameters]::WorkArea
220
+ $pos = $cmd.position
221
+ if ($null -ne $pos -and $null -ne $pos.left -and $null -ne $pos.top) {
222
+ $win.Left = [double]$pos.left + $work.Left
223
+ $win.Top = [double]$pos.top + $work.Top
224
+ } else {
225
+ $win.Left = $work.Right - $win.ActualWidth - 20
226
+ $win.Top = $work.Bottom - $win.ActualHeight - 20
227
+ }
228
+ Send-Geometry $win
229
+ }.GetNewClosure()) | Out-Null
230
+
231
+ # 拖拽移动(缩放把手区域除外):DragMove 阻塞至松开,结束后收敛到工作区内并回传几何。
232
+ $win.Add_MouseLeftButtonDown({
233
+ if ($win.Tag.resizing) { $win.Tag.resizing = $false; return }
234
+ try { $win.DragMove() } catch { }
235
+ try {
236
+ $work = [System.Windows.SystemParameters]::WorkArea
237
+ $win.Left = [Math]::Max($work.Left, [Math]::Min($win.Left, $work.Right - $win.ActualWidth))
238
+ $win.Top = [Math]::Max($work.Top, [Math]::Min($win.Top, $work.Bottom - $win.ActualHeight))
239
+ } catch { }
240
+ Send-Geometry $win
241
+ }.GetNewClosure()) | Out-Null
242
+
243
+ # 拖拽边缘缩放宽度(260–520):右缘拖右变宽;左缘拖左变宽且右缘固定(窗口左移)。
244
+ # 注意:$strip/$edge/$win 都作为函数参数,闭包才能可靠捕获。
245
+ function Add-EdgeResize($strip, [string]$edge, $w) {
246
+ $strip.Add_MouseLeftButtonDown({
247
+ $w.Tag.resizing = $true
248
+ $w.Tag.edge = $edge
249
+ $w.Tag.startX = $_.GetPosition($w).X
250
+ $w.Tag.startWidth = $w.ActualWidth
251
+ $w.Tag.startLeft = $w.Left
252
+ $strip.CaptureMouse()
253
+ $_.Handled = $true
254
+ }.GetNewClosure()) | Out-Null
255
+ $strip.Add_MouseMove({
256
+ if ($w.Tag.resizing) {
257
+ $dx = $_.GetPosition($w).X - $w.Tag.startX
258
+ if ($w.Tag.edge -eq 'left') {
259
+ $nw = [Math]::Max(260, [Math]::Min(520, [Math]::Round($w.Tag.startWidth - $dx)))
260
+ $w.Width = $nw
261
+ $w.Left = $w.Tag.startLeft + ($w.Tag.startWidth - $w.Width)
262
+ } else {
263
+ $nw = [Math]::Max(260, [Math]::Min(520, [Math]::Round($w.Tag.startWidth + $dx)))
264
+ $w.Width = $nw
265
+ }
266
+ $_.Handled = $true
267
+ }
268
+ }.GetNewClosure()) | Out-Null
269
+ $strip.Add_MouseLeftButtonUp({
270
+ if ($w.Tag.resizing) {
271
+ $w.Tag.resizing = $false
272
+ if ($strip.IsMouseCaptured) { $strip.ReleaseMouseCapture() }
273
+ Send-Geometry $w
274
+ $_.Handled = $true
275
+ }
276
+ }.GetNewClosure()) | Out-Null
277
+ }
278
+ Add-EdgeResize $leftStrip 'left' $win
279
+ Add-EdgeResize $rightStrip 'right' $win
280
+
281
+ $win.Add_Closed({
282
+ if ($script:editSample -eq $win) { $script:editSample = $null }
283
+ }.GetNewClosure()) | Out-Null
284
+
285
+ $win.Content = $outer
286
+ return $win
287
+ }
288
+
289
+ $script:toasts = @{}
290
+ $script:exitRequested = $false
291
+ # 编辑模式样板窗(不进入 toasts 注册表,不参与堆叠;edit-end 时关闭)。
292
+ $script:editSample = $null
293
+
294
+ function Parse-Color([string]$hex, [string]$fallback) {
295
+ $h = $hex.TrimStart('#')
296
+ if ($h.Length -eq 3) { $h = ($h.ToCharArray() | ForEach-Object { "$_$_" }) -join '' }
297
+ if ($h.Length -eq 6) { $h = "FF$h" }
298
+ if ($h.Length -ne 8 -or $h -notmatch '^[0-9a-fA-F]{8}$') {
299
+ # 非法/缺失输入:改用回退色并同样做 3/6 位展开 + 补 FF。
300
+ $h = $fallback.TrimStart('#')
301
+ if ($h.Length -eq 3) { $h = ($h.ToCharArray() | ForEach-Object { "$_$_" }) -join '' }
302
+ if ($h.Length -eq 6) { $h = "FF$h" }
303
+ }
304
+ return [System.Windows.Media.Color]::FromArgb(
305
+ [Convert]::ToInt32($h.Substring(0, 2), 16),
306
+ [Convert]::ToInt32($h.Substring(2, 2), 16),
307
+ [Convert]::ToInt32($h.Substring(4, 2), 16),
308
+ [Convert]::ToInt32($h.Substring(6, 2), 16))
309
+ }
310
+
311
+ $downP = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(158, 164, 170))
312
+ $downI = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(30, 36, 44))
313
+ $hoverP = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(196, 202, 208))
314
+ $hoverI = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(44, 52, 62))
315
+
316
+ # 浮窗按钮:primary 时传入强调色画刷,hover/按压基于该色自动提亮/压暗;
317
+ # secondary 保持描边透明底样式。
318
+ function New-ToastButton([string]$label, [scriptblock]$onClick, [bool]$primary, [System.Windows.Media.SolidColorBrush]$fg, [System.Windows.Media.SolidColorBrush]$bg) {
319
+ $bd = New-Object System.Windows.Controls.Border
320
+ $bd.CornerRadius = New-Object System.Windows.CornerRadius(10)
321
+ $bd.Padding = New-Object System.Windows.Thickness(18, 7, 18, 7)
322
+ $bd.Margin = New-Object System.Windows.Thickness(0, 0, 8, 0)
323
+ $bd.Cursor = [System.Windows.Input.Cursors]::Hand
324
+ $bd.Focusable = $false
325
+
326
+ $txt = New-Object System.Windows.Controls.TextBlock
327
+ $txt.Text = $label
328
+ $txt.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
329
+ $txt.FontSize = 12
330
+ $txt.VerticalAlignment = [System.Windows.VerticalAlignment]::Center
331
+ $txt.IsHitTestVisible = $false
332
+
333
+ if ($primary) {
334
+ $bd.Background = $fg
335
+ $txt.Foreground = $bg
336
+ $bd.BorderBrush = [System.Windows.Media.Brushes]::Transparent
337
+ $bd.BorderThickness = New-Object System.Windows.Thickness(0)
338
+ } else {
339
+ $bd.Background = [System.Windows.Media.Brushes]::Transparent
340
+ $txt.Foreground = $fg
341
+ $bd.BorderBrush = $fg
342
+ $bd.BorderThickness = New-Object System.Windows.Thickness(1)
343
+ }
344
+
345
+ $bd.Child = $txt
346
+ # hover/down 变体:primary 基于传入的强调色提亮/压暗;secondary 用全局灰蓝变体。
347
+ $bd.Tag = @{
348
+ primary = $primary; onClick = $onClick
349
+ hoverP = (Convert-Hsl $fg.Color 1.15); downP = (Convert-Hsl $fg.Color 0.85)
350
+ hoverI = $hoverI; downI = $downI
351
+ fg = $fg; bg = $bg
352
+ }
353
+ $bd.Add_MouseEnter({
354
+ $info = $this.Tag
355
+ $this.Background = if ($info.primary) { $info.hoverP } else { $info.hoverI }
356
+ }) | Out-Null
357
+ $bd.Add_MouseLeave({
358
+ $info = $this.Tag
359
+ $this.Background = if ($info.primary) { $info.fg } else { [System.Windows.Media.Brushes]::Transparent }
360
+ }) | Out-Null
361
+ $bd.Add_MouseLeftButtonDown({
362
+ $info = $this.Tag
363
+ $this.Background = if ($info.primary) { $info.downP } else { $info.downI }
364
+ }) | Out-Null
365
+ $bd.Add_MouseLeftButtonUp({
366
+ $info = $this.Tag
367
+ try { if ($info.onClick -ne $null) { & $info.onClick } } catch { }
368
+ $this.Background = if ($info.primary) { $info.fg } else { [System.Windows.Media.Brushes]::Transparent }
369
+ }) | Out-Null
370
+ return $bd
371
+ }
372
+
373
+ $easeMove = New-Object System.Windows.Media.Animation.QuadraticEase
374
+ $easeMove.EasingMode = [System.Windows.Media.Animation.EasingMode]::EaseOut
375
+ $easeExit = New-Object System.Windows.Media.Animation.CubicEase
376
+ $easeExit.EasingMode = [System.Windows.Media.Animation.EasingMode]::EaseIn
377
+
378
+ function Close-WithFade($w) {
379
+ try {
380
+ # closing 标记改为 Tag 哈希表(同时携带卡片缩放变换供退场动画用)。
381
+ if ($w.Tag -is [hashtable]) {
382
+ if ($w.Tag.closing) { return }
383
+ $w.Tag.closing =$true
384
+ } else {
385
+ if ($w.Tag -eq 'closing') { return }
386
+ $w.Tag = @{ closing =$true; scale = $null }
387
+ }
388
+ # 淡出 + 下坠 + 轻微收缩(0.98):三者同步 200ms,"沉落"退场。
389
+ $fadeOut = New-Object System.Windows.Media.Animation.DoubleAnimation(1, 0, [TimeSpan]::FromMilliseconds(200))
390
+ $fadeOut.EasingFunction =$easeExit
391
+ $fadeOut.Add_Completed({
392
+ try { $w.Close() } catch { }
393
+ }.GetNewClosure()) | Out-Null
394
+ $w.BeginAnimation([System.Windows.Window]::OpacityProperty,$fadeOut)
395
+ $dropAn = New-Object System.Windows.Media.Animation.DoubleAnimation($w.Top, ($w.Top + 16), [TimeSpan]::FromMilliseconds(200))
396
+ $dropAn.EasingFunction =$easeExit
397
+ $w.BeginAnimation([System.Windows.Window]::TopProperty,$dropAn)
398
+ try {
399
+ $sc =$w.Tag.scale
400
+ if ($sc -ne$null) {
401
+ $sx = New-Object System.Windows.Media.Animation.DoubleAnimation(1, 0.98, [TimeSpan]::FromMilliseconds(200))
402
+ $sx.EasingFunction =$easeExit
403
+ $sy = New-Object System.Windows.Media.Animation.DoubleAnimation(1, 0.98, [TimeSpan]::FromMilliseconds(200))
404
+ $sy.EasingFunction =$easeExit
405
+ $sc.BeginAnimation([System.Windows.Media.ScaleTransform]::ScaleXProperty,$sx)
406
+ $sc.BeginAnimation([System.Windows.Media.ScaleTransform]::ScaleYProperty,$sy)
407
+ }
408
+ } catch { }
409
+ $backstop = New-Object System.Windows.Threading.DispatcherTimer
410
+ $backstop.Interval = [TimeSpan]::FromMilliseconds(600)
411
+ $backstop.Tag =$w
412
+ $backstop.Add_Tick({
413
+ $this.Stop()
414
+ try { if ($this.Tag.IsVisible) {$this.Tag.Close() } } catch { }
415
+ }) | Out-Null
416
+ $backstop.Start()
417
+ } catch {
418
+ try { $w.Close() } catch { }
419
+ }
420
+ }
421
+
422
+ # ───────────────────────── 通知浮窗 ─────────────────────────
423
+
424
+ function New-ToastWindow($cmd) {
425
+ $bg = Parse-Color ([string]$cmd.bg) "#20272f"
426
+ $fg = Parse-Color ([string]$cmd.fg) "#e6edf3"
427
+ $accent = Parse-Color ([string]$cmd.accent) "#60a5fa"
428
+ $bgBrush = New-Object System.Windows.Media.SolidColorBrush($bg)
429
+ $fgBrush = New-Object System.Windows.Media.SolidColorBrush($fg)
430
+ $accentBrush = New-Object System.Windows.Media.SolidColorBrush($accent)
431
+
432
+ # 提前计算:粘性判定与倒计时时长(Loaded 闭包与计时器共用)。
433
+ $isSticky = ($cmd.sticky -eq $true)
434
+ $dur = [Math]::Max(1, [int]$cmd.durationSec)
435
+
436
+ $win = New-Object System.Windows.Window
437
+ $win.WindowStyle = [System.Windows.WindowStyle]::None
438
+ $win.AllowsTransparency =$true
439
+ $win.Background = [System.Windows.Media.Brushes]::Transparent
440
+ $win.Topmost =$true
441
+ $win.ShowInTaskbar =$false
442
+ if ($null -ne$cmd.width) { $win.Width = [double]$cmd.width } else { $win.Width = 340 }
443
+ $win.SizeToContent = [System.Windows.SizeToContent]::Height
444
+ if (-not [string]::IsNullOrEmpty([string]$cmd.winTitle)) {$win.Title = [string]$cmd.winTitle }
445
+ $win.Effect = New-GlowEffect $accent 0.28
446
+
447
+ $border = New-Object System.Windows.Controls.Border
448
+ $border.Background =$bgBrush
449
+ $border.BorderBrush =$accentBrush
450
+ $border.BorderThickness = New-Object System.Windows.Thickness(1)
451
+ $border.CornerRadius = New-Object System.Windows.CornerRadius(14)
452
+ $border.Margin = New-Object System.Windows.Thickness(12)
453
+
454
+ # 卡片缩放变换:入场 0.96→1(底部锚点,"浮现"),退场 1→0.98("沉落")。
455
+ # 经 $win.Tag 传给 Close-WithFade,同时 Tag.closing 作防重入标记。
456
+ $cardT = New-Object System.Windows.Media.ScaleTransform(1, 1)
457
+ $border.RenderTransformOrigin = New-Object System.Windows.Point(0.5, 1)
458
+ $border.RenderTransform =$cardT
459
+
460
+ # 内容 Grid:3 行 = 头部(标题) / 正文 / 按钮行
461
+ $root = New-Object System.Windows.Controls.Grid
462
+ $root.Margin = New-Object System.Windows.Thickness(26, 12, 14, 12)
463
+ Add-GridRow $root
464
+ Add-GridRow $root
465
+ Add-GridRow $root
466
+
467
+ # ── 头部:仅标题 ──
468
+ $head = New-Object System.Windows.Controls.StackPanel
469
+ $head.Orientation = [System.Windows.Controls.Orientation]::Horizontal
470
+ $head.Margin = New-Object System.Windows.Thickness(0, 0, 0, 6)
471
+ $unnamedLabel = if ([string]::IsNullOrEmpty([string]$cmd.unnamedLabel)) { "(Unnamed session)" } else { [string]$cmd.unnamedLabel }
472
+ $titleText = New-Object System.Windows.Controls.TextBlock
473
+ $titleText.Text = if ([string]::IsNullOrEmpty([string]$cmd.title)) { $unnamedLabel } else { [string]$cmd.title }
474
+ $titleText.Foreground =$fgBrush
475
+ $titleText.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
476
+ $titleText.FontSize = 13
477
+ $titleText.FontWeight = [System.Windows.FontWeights]::SemiBold
478
+ $titleText.TextTrimming = [System.Windows.TextTrimming]::CharacterEllipsis
479
+ $titleText.VerticalAlignment = [System.Windows.VerticalAlignment]::Center
480
+ $head.Children.Add($titleText) | Out-Null
481
+ [System.Windows.Controls.Grid]::SetRow($head, 0)
482
+ $root.Children.Add($head) | Out-Null
483
+
484
+ # ── 正文 ──
485
+ $bodyText = New-Object System.Windows.Controls.TextBlock
486
+ $bodyText.Text = [string]$cmd.text
487
+ $bodyText.Foreground =$fgBrush
488
+ $bodyText.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
489
+ $bodyText.FontSize = 13
490
+ $bodyText.TextWrapping = [System.Windows.TextWrapping]::Wrap
491
+ $bodyText.Margin = New-Object System.Windows.Thickness(0, 0, 0, 10)
492
+ [System.Windows.Controls.Grid]::SetRow($bodyText, 1)
493
+ $root.Children.Add($bodyText) | Out-Null
494
+
495
+ # ── 按钮行 ──
496
+ $btnRow = New-Object System.Windows.Controls.StackPanel
497
+ $btnRow.Orientation = [System.Windows.Controls.Orientation]::Horizontal
498
+ $btnRow.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Right
499
+ $btnRow.Margin = New-Object System.Windows.Thickness(0, 0, 0, 2)
500
+ $ignoreLabel = if ([string]::IsNullOrEmpty([string]$cmd.ignoreLabel)) { "Ignore" } else { [string]$cmd.ignoreLabel }
501
+ $ignoreBtn = New-ToastButton $ignoreLabel ({ Close-WithFade $win }.GetNewClosure()) $false $fgBrush $bgBrush
502
+ $btnRow.Children.Add($ignoreBtn) | Out-Null
503
+ if (-not [string]::IsNullOrEmpty([string]$cmd.sessionId)) {
504
+ $jumpLabel = if ([string]::IsNullOrEmpty([string]$cmd.jumpLabel)) { "Open" } else { [string]$cmd.jumpLabel }
505
+ $jumpBtn = New-ToastButton $jumpLabel ({
506
+ $url = "$($cmd.baseUrl)/#dsh-notify-yimit/session=$($cmd.sessionId)"
507
+ try {
508
+ if (-not [string]::IsNullOrEmpty([string]$cmd.browserPath)) {
509
+ Start-Process -FilePath ([string]$cmd.browserPath) -ArgumentList $url
510
+ } else { Start-Process $url }
511
+ } catch { }
512
+ Close-WithFade $win
513
+ }.GetNewClosure()) $true $accentBrush $bgBrush
514
+ $btnRow.Children.Add($jumpBtn) | Out-Null
515
+ }
516
+ [System.Windows.Controls.Grid]::SetRow($btnRow, 2)
517
+ $root.Children.Add($btnRow) | Out-Null
518
+
519
+ # ── 倒计时液面层(唯一的倒计时视觉,无任何线条元素) ──
520
+ $inner = New-Object System.Windows.Controls.Grid
521
+ $tintLayer = New-Object System.Windows.Controls.Border
522
+ $tintLayer.CornerRadius = New-Object System.Windows.CornerRadius(13, 0, 0, 13)
523
+ $tintLayer.VerticalAlignment = [System.Windows.VerticalAlignment]::Stretch
524
+ $tintLayer.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Stretch
525
+ $tintBrush = New-Object System.Windows.Media.SolidColorBrush($accent)
526
+ $tintBrush.Opacity = 0.12
527
+ $tintLayer.Background =$tintBrush
528
+ $tintT = New-Object System.Windows.Media.ScaleTransform(1, 1)
529
+ $tintLayer.RenderTransformOrigin = New-Object System.Windows.Point(0, 0.5)
530
+ $tintLayer.RenderTransform =$tintT
531
+ $inner.Children.Add($tintLayer) | Out-Null
532
+ $inner.Children.Add($root) | Out-Null
533
+ $border.Child =$inner
534
+
535
+ # ── 左侧强调色条(粘性通知做呼吸脉动) ──
536
+ $accentStrip = New-Object System.Windows.Controls.Border
537
+ $accentStrip.Width = 4
538
+ $accentStrip.CornerRadius = New-Object System.Windows.CornerRadius(2)
539
+ $accentStrip.VerticalAlignment = [System.Windows.VerticalAlignment]::Stretch
540
+ $accentStrip.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Left
541
+ $accentStrip.Margin = New-Object System.Windows.Thickness(22, 26, 0, 26)
542
+ $accentStrip.Background =$accentBrush
543
+
544
+ # 外层只有:卡片 + 色条。进度指针已彻底移除 —— 任何通知都不再出现那根线。
545
+ $outer = New-Object System.Windows.Controls.Grid
546
+ $outer.Children.Add($border) | Out-Null
547
+ $outer.Children.Add($accentStrip) | Out-Null
548
+ $win.Content =$outer
549
+
550
+ $win.Tag = @{ closing =$false; scale = $cardT }
551
+
552
+ $win.Add_Loaded({
553
+ $work = [System.Windows.SystemParameters]::WorkArea
554
+ $pos =$cmd.position
555
+ if ($null -ne$pos -and $null -ne$pos.left -and $null -ne$pos.top) {
556
+ $targetLeft = [double]$pos.left + $work.Left
557
+ $targetTop = [double]$pos.top + $work.Top - [int]$cmd.offsetY
558
+ } else {
559
+ $targetLeft =$work.Right - $win.ActualWidth - 20
560
+ $targetTop =$work.Bottom - $win.ActualHeight - 20 - [int]$cmd.offsetY
561
+ }
562
+ $win.Left =$targetLeft
563
+ $win.Top =$targetTop + 36
564
+ $win.Opacity = 0
565
+ try {
566
+ $easingIn = New-Object System.Windows.Media.Animation.QuadraticEase
567
+ $easingIn.EasingMode = [System.Windows.Media.Animation.EasingMode]::EaseOut
568
+ # 入场三连:上滑 36px + 淡入 + 卡片 0.96→1 缩放(底部锚点),260ms 同步完成。
569
+ $slideIn = New-Object System.Windows.Media.Animation.DoubleAnimation($targetTop + 36, $targetTop, [TimeSpan]::FromMilliseconds(260))
570
+ $slideIn.EasingFunction =$easingIn
571
+ $win.BeginAnimation([System.Windows.Window]::TopProperty,$slideIn)
572
+ $fadeIn = New-Object System.Windows.Media.Animation.DoubleAnimation(0, 1, [TimeSpan]::FromMilliseconds(200))
573
+ $fadeIn.EasingFunction =$easingIn
574
+ $win.BeginAnimation([System.Windows.Window]::OpacityProperty,$fadeIn)
575
+ $scInX = New-Object System.Windows.Media.Animation.DoubleAnimation(0.96, 1, [TimeSpan]::FromMilliseconds(260))
576
+ $scInX.EasingFunction =$easingIn
577
+ $scInY = New-Object System.Windows.Media.Animation.DoubleAnimation(0.96, 1, [TimeSpan]::FromMilliseconds(260))
578
+ $scInY.EasingFunction =$easingIn
579
+ $cardT.BeginAnimation([System.Windows.Media.ScaleTransform]::ScaleXProperty,$scInX)
580
+ $cardT.BeginAnimation([System.Windows.Media.ScaleTransform]::ScaleYProperty,$scInY)
581
+ $guard = New-Object System.Windows.Threading.DispatcherTimer
582
+ $guard.Interval = [TimeSpan]::FromMilliseconds(400)
583
+ $guard.Tag = @{ w =$win; top = $targetTop }
584
+ $guard.Add_Tick({
585
+ $this.Stop()
586
+ $g =$this.Tag
587
+ try {
588
+ if ($g.w.Opacity -lt 0.9) {$g.w.Opacity = 1 }
589
+ if ($g.w.Top -ne$g.top) { $g.w.Top =$g.top }
590
+ } catch { }
591
+ }) | Out-Null
592
+ $guard.Start()
593
+ } catch {
594
+ $win.Opacity = 1
595
+ $win.Top =$targetTop
596
+ }
597
+ try {
598
+ $hwnd = [System.Windows.Interop.WindowInteropHelper]::new($win).Handle
599
+ Send-Report @{
600
+ type = 'pos'; key = [string]$cmd.key; instance = [int]$cmd.instance
601
+ left = [Math]::Round($targetLeft); top = [Math]::Round($targetTop)
602
+ height = [Math]::Round($win.ActualHeight); hwnd =$hwnd.ToInt64()
603
+ workBottom = [Math]::Round($work.Bottom)
604
+ }
605
+ } catch { }
606
+
607
+ # ── 停留阶段:非粘性 = 液面匀速右→左;粘性 = 色条呼吸 ──
608
+ if (-not $isSticky) {
609
+ try {
610
+ $shrink = New-Object System.Windows.Media.Animation.DoubleAnimation(
611
+ 1, 0, [TimeSpan]::FromSeconds($dur))
612
+ # 无缓动:视觉时长 = 真实剩余时间。
613
+ $tintT.BeginAnimation([System.Windows.Media.ScaleTransform]::ScaleXProperty,$shrink)
614
+ } catch { }
615
+ } else {
616
+ try {
617
+ $pulse = New-Object System.Windows.Media.Animation.DoubleAnimation(0.55, 1.0,
618
+ [TimeSpan]::FromMilliseconds(1100))
619
+ $pulse.AutoReverse =$true
620
+ $pulse.RepeatBehavior = [System.Windows.Media.Animation.RepeatBehavior]::Forever
621
+ $pulse.EasingFunction = New-Object System.Windows.Media.Animation.QuadraticEase
622
+ $accentStrip.BeginAnimation([System.Windows.UIElement]::OpacityProperty,$pulse)
623
+ } catch { }
624
+ }
625
+ }.GetNewClosure()) | Out-Null
626
+
627
+ $toastsRef =$script:toasts
628
+ $win.Add_Closed({
629
+ if ($toastsRef.ContainsKey([string]$cmd.key)) {
630
+ $rec =$toastsRef[[string]$cmd.key]
631
+ if ($rec.Timer -ne$null) {
632
+ try { $rec.Timer.Stop() } catch { }
633
+ }
634
+ $toastsRef.Remove([string]$cmd.key) | Out-Null
635
+ }
636
+ Send-Report @{ type = 'exit'; key = [string]$cmd.key; instance = [int]$cmd.instance }
637
+ }.GetNewClosure()) | Out-Null
638
+
639
+ $rec = @{ Win =$win; Body = $bodyText; Title =$titleText; Timer = $null; Instance = [int]$cmd.instance }
640
+ $script:toasts[[string]$cmd.key] = $rec
641
+
642
+ if (-not $isSticky) {
643
+ $timer = New-Object System.Windows.Threading.DispatcherTimer
644
+ $timer.Interval = [TimeSpan]::FromSeconds($dur)
645
+ $timer.Add_Tick({$this.Stop(); Close-WithFade $win }.GetNewClosure()) | Out-Null
646
+ $timer.Start()
647
+ $rec.Timer =$timer
648
+ }
649
+
650
+ return $win
651
+ }
652
+
653
+ # ───────────────────────── 命令处理 ─────────────────────────
654
+
655
+ function Invoke-Command([string]$line) {
656
+ $line = $line.Trim()
657
+ if ([string]::IsNullOrEmpty($line)) { return }
658
+ $cmd = $null
659
+ try { $cmd = $line | ConvertFrom-Json } catch { return }
660
+ if ($cmd -eq $null -or [string]::IsNullOrEmpty([string]$cmd.cmd)) { return }
661
+
662
+ switch ([string]$cmd.cmd) {
663
+ 'show' {
664
+ if ($script:toasts.ContainsKey([string]$cmd.key)) {
665
+ $old = $script:toasts[[string]$cmd.key]
666
+ try { $old.Win.Close() } catch { }
667
+ $script:toasts.Remove([string]$cmd.key) | Out-Null
668
+ }
669
+ $w = New-ToastWindow $cmd
670
+ if ($w -ne $null) { $w.Show() }
671
+ }
672
+ 'text' {
673
+ $rec = $script:toasts[[string]$cmd.key]
674
+ if ($rec -ne $null -and $rec.Body -ne $null) { $rec.Body.Text = [string]$cmd.text }
675
+ }
676
+ 'title' {
677
+ $rec = $script:toasts[[string]$cmd.key]
678
+ if ($rec -ne $null -and $rec.Title -ne $null) { $rec.Title.Text = [string]$cmd.title }
679
+ }
680
+ 'move' {
681
+ $rec = $script:toasts[[string]$cmd.key]
682
+ if ($rec -ne $null) {
683
+ $top = [int]$cmd.top
684
+ $slide = New-Object System.Windows.Media.Animation.DoubleAnimation($rec.Win.Top, $top, [TimeSpan]::FromMilliseconds(220))
685
+ $slide.EasingFunction = $easeMove
686
+ $rec.Win.BeginAnimation([System.Windows.Window]::TopProperty, $slide)
687
+ # 可选横向定位:left 数字 = 绝对 X(工作区相对);left 'default' = 默认右下贴边
688
+ # (工作区右缘 - 宽度 - 20)。用于编辑调整位置/恢复默认后,在屏浮窗立即到位。
689
+ if ($null -ne $cmd.left) {
690
+ $newLeft = 0.0
691
+ if ($cmd.left -eq 'default') {
692
+ $work = [System.Windows.SystemParameters]::WorkArea
693
+ $newLeft = $work.Right - $rec.Win.ActualWidth - 20
694
+ } else {
695
+ $newLeft = [double]$cmd.left
696
+ }
697
+ $slideL = New-Object System.Windows.Media.Animation.DoubleAnimation($rec.Win.Left, $newLeft, [TimeSpan]::FromMilliseconds(220))
698
+ $slideL.EasingFunction = $easeMove
699
+ $rec.Win.BeginAnimation([System.Windows.Window]::LeftProperty, $slideL)
700
+ }
701
+ }
702
+ }
703
+ 'close' {
704
+ $rec = $script:toasts[[string]$cmd.key]
705
+ if ($rec -ne $null) { Close-WithFade $rec.Win }
706
+ }
707
+ 'size' {
708
+ $rec = $script:toasts[[string]$cmd.key]
709
+ if ($rec -ne $null -and $null -ne $cmd.width) { $rec.Win.Width = [double]$cmd.width }
710
+ }
711
+ 'edit' {
712
+ # 编辑模式:关闭旧样板(如有),弹新样板窗(可拖拽/缩放,内嵌类型色卡预览)。
713
+ if ($script:editSample -ne $null) {
714
+ try { $script:editSample.Close() } catch { }
715
+ $script:editSample = $null
716
+ }
717
+ $w = New-EditSample $cmd
718
+ if ($w -ne $null) { $w.Show(); $script:editSample = $w }
719
+ }
720
+ 'edit-end' {
721
+ if ($script:editSample -ne $null) {
722
+ try { $script:editSample.Close() } catch { }
723
+ $script:editSample = $null
724
+ }
725
+ }
726
+ 'shutdown' { $script:exitRequested = $true }
727
+ }
728
+ }
729
+
730
+ # ───────────────────────── 主循环 ─────────────────────────
731
+
732
+ $script:cmdQueue = New-Object 'System.Collections.Concurrent.ConcurrentQueue[string]'
733
+
734
+ $readerScript = {
735
+ param($q)
736
+ try {
737
+ $reader = New-Object System.IO.StreamReader([System.Console]::OpenStandardInput(), (New-Object System.Text.UTF8Encoding($false)))
738
+ while ($true) {
739
+ $line = $reader.ReadLine()
740
+ if ($null -eq $line) { break }
741
+ $q.Enqueue($line)
742
+ }
743
+ } catch { }
744
+ $q.Enqueue('{"cmd":"shutdown"}')
745
+ }
746
+
747
+ $readerRs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
748
+ $readerRs.Open()
749
+ $readerPs = [System.Management.Automation.PowerShell]::Create()
750
+ $readerPs.Runspace = $readerRs
751
+ $readerPs.AddScript($readerScript).AddArgument($script:cmdQueue) | Out-Null
752
+ $readerAsyncResult = $readerPs.BeginInvoke()
753
+
754
+ $cmdTimer = New-Object System.Windows.Threading.DispatcherTimer
755
+ $cmdTimer.Interval = [TimeSpan]::FromMilliseconds(50)
756
+ $cmdTimer.Add_Tick({
757
+ try {
758
+ $line = $null
759
+ while ($script:cmdQueue.TryDequeue([ref]$line)) {
760
+ if (-not [string]::IsNullOrEmpty($line)) { Invoke-Command $line }
761
+ if ($script:exitRequested) { break }
762
+ }
763
+ if ($script:exitRequested) {
764
+ $this.Stop()
765
+ foreach ($key in @($script:toasts.Keys)) {
766
+ try { $script:toasts[$key].Win.Close() } catch { }
767
+ }
768
+ if ($script:editSample -ne $null) {
769
+ try { $script:editSample.Close() } catch { }
770
+ $script:editSample = $null
771
+ }
772
+ [System.Windows.Threading.Dispatcher]::CurrentDispatcher.InvokeShutdown()
773
+ }
774
+ } catch { }
775
+ }) | Out-Null
776
+ $cmdTimer.Start()
777
+
778
+ [System.Windows.Threading.Dispatcher]::Run()
779
+
780
+ try { if ($readerAsyncResult -ne $null) { $readerPs.EndInvoke($readerAsyncResult) } } catch { }
781
+ try { $readerPs.Dispose() } catch { }
782
+ try { $readerRs.Dispose() } catch { }