dsh-notify-yimit 0.1.5 → 0.1.7

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,782 +1,785 @@
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 { }
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
+ # 缺省 true = 保留「跳转会话」;桌面端宿主会显式传 jumpEnabled=false 来隐藏它。
504
+ $jumpEnabled = $true
505
+ if ($cmd.PSObject.Properties.Name -contains 'jumpEnabled') { $jumpEnabled = [bool]$cmd.jumpEnabled }
506
+ if ($jumpEnabled -and -not [string]::IsNullOrEmpty([string]$cmd.sessionId)) {
507
+ $jumpLabel = if ([string]::IsNullOrEmpty([string]$cmd.jumpLabel)) { "Open" } else { [string]$cmd.jumpLabel }
508
+ $jumpBtn = New-ToastButton $jumpLabel ({
509
+ $url = "$($cmd.baseUrl)/#dsh-notify-yimit/session=$($cmd.sessionId)"
510
+ try {
511
+ if (-not [string]::IsNullOrEmpty([string]$cmd.browserPath)) {
512
+ Start-Process -FilePath ([string]$cmd.browserPath) -ArgumentList $url
513
+ } else { Start-Process $url }
514
+ } catch { }
515
+ Close-WithFade $win
516
+ }.GetNewClosure()) $true $accentBrush $bgBrush
517
+ $btnRow.Children.Add($jumpBtn) | Out-Null
518
+ }
519
+ [System.Windows.Controls.Grid]::SetRow($btnRow, 2)
520
+ $root.Children.Add($btnRow) | Out-Null
521
+
522
+ # ── 倒计时液面层(唯一的倒计时视觉,无任何线条元素) ──
523
+ $inner = New-Object System.Windows.Controls.Grid
524
+ $tintLayer = New-Object System.Windows.Controls.Border
525
+ $tintLayer.CornerRadius = New-Object System.Windows.CornerRadius(13, 0, 0, 13)
526
+ $tintLayer.VerticalAlignment = [System.Windows.VerticalAlignment]::Stretch
527
+ $tintLayer.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Stretch
528
+ $tintBrush = New-Object System.Windows.Media.SolidColorBrush($accent)
529
+ $tintBrush.Opacity = 0.12
530
+ $tintLayer.Background =$tintBrush
531
+ $tintT = New-Object System.Windows.Media.ScaleTransform(1, 1)
532
+ $tintLayer.RenderTransformOrigin = New-Object System.Windows.Point(0, 0.5)
533
+ $tintLayer.RenderTransform =$tintT
534
+ $inner.Children.Add($tintLayer) | Out-Null
535
+ $inner.Children.Add($root) | Out-Null
536
+ $border.Child =$inner
537
+
538
+ # ── 左侧强调色条(粘性通知做呼吸脉动) ──
539
+ $accentStrip = New-Object System.Windows.Controls.Border
540
+ $accentStrip.Width = 4
541
+ $accentStrip.CornerRadius = New-Object System.Windows.CornerRadius(2)
542
+ $accentStrip.VerticalAlignment = [System.Windows.VerticalAlignment]::Stretch
543
+ $accentStrip.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Left
544
+ $accentStrip.Margin = New-Object System.Windows.Thickness(22, 26, 0, 26)
545
+ $accentStrip.Background =$accentBrush
546
+
547
+ # 外层只有:卡片 + 色条。进度指针已彻底移除 —— 任何通知都不再出现那根线。
548
+ $outer = New-Object System.Windows.Controls.Grid
549
+ $outer.Children.Add($border) | Out-Null
550
+ $outer.Children.Add($accentStrip) | Out-Null
551
+ $win.Content =$outer
552
+
553
+ $win.Tag = @{ closing =$false; scale = $cardT }
554
+
555
+ $win.Add_Loaded({
556
+ $work = [System.Windows.SystemParameters]::WorkArea
557
+ $pos =$cmd.position
558
+ if ($null -ne$pos -and $null -ne$pos.left -and $null -ne$pos.top) {
559
+ $targetLeft = [double]$pos.left + $work.Left
560
+ $targetTop = [double]$pos.top + $work.Top - [int]$cmd.offsetY
561
+ } else {
562
+ $targetLeft =$work.Right - $win.ActualWidth - 20
563
+ $targetTop =$work.Bottom - $win.ActualHeight - 20 - [int]$cmd.offsetY
564
+ }
565
+ $win.Left =$targetLeft
566
+ $win.Top =$targetTop + 36
567
+ $win.Opacity = 0
568
+ try {
569
+ $easingIn = New-Object System.Windows.Media.Animation.QuadraticEase
570
+ $easingIn.EasingMode = [System.Windows.Media.Animation.EasingMode]::EaseOut
571
+ # 入场三连:上滑 36px + 淡入 + 卡片 0.96→1 缩放(底部锚点),260ms 同步完成。
572
+ $slideIn = New-Object System.Windows.Media.Animation.DoubleAnimation($targetTop + 36, $targetTop, [TimeSpan]::FromMilliseconds(260))
573
+ $slideIn.EasingFunction =$easingIn
574
+ $win.BeginAnimation([System.Windows.Window]::TopProperty,$slideIn)
575
+ $fadeIn = New-Object System.Windows.Media.Animation.DoubleAnimation(0, 1, [TimeSpan]::FromMilliseconds(200))
576
+ $fadeIn.EasingFunction =$easingIn
577
+ $win.BeginAnimation([System.Windows.Window]::OpacityProperty,$fadeIn)
578
+ $scInX = New-Object System.Windows.Media.Animation.DoubleAnimation(0.96, 1, [TimeSpan]::FromMilliseconds(260))
579
+ $scInX.EasingFunction =$easingIn
580
+ $scInY = New-Object System.Windows.Media.Animation.DoubleAnimation(0.96, 1, [TimeSpan]::FromMilliseconds(260))
581
+ $scInY.EasingFunction =$easingIn
582
+ $cardT.BeginAnimation([System.Windows.Media.ScaleTransform]::ScaleXProperty,$scInX)
583
+ $cardT.BeginAnimation([System.Windows.Media.ScaleTransform]::ScaleYProperty,$scInY)
584
+ $guard = New-Object System.Windows.Threading.DispatcherTimer
585
+ $guard.Interval = [TimeSpan]::FromMilliseconds(400)
586
+ $guard.Tag = @{ w =$win; top = $targetTop }
587
+ $guard.Add_Tick({
588
+ $this.Stop()
589
+ $g =$this.Tag
590
+ try {
591
+ if ($g.w.Opacity -lt 0.9) {$g.w.Opacity = 1 }
592
+ if ($g.w.Top -ne$g.top) { $g.w.Top =$g.top }
593
+ } catch { }
594
+ }) | Out-Null
595
+ $guard.Start()
596
+ } catch {
597
+ $win.Opacity = 1
598
+ $win.Top =$targetTop
599
+ }
600
+ try {
601
+ $hwnd = [System.Windows.Interop.WindowInteropHelper]::new($win).Handle
602
+ Send-Report @{
603
+ type = 'pos'; key = [string]$cmd.key; instance = [int]$cmd.instance
604
+ left = [Math]::Round($targetLeft); top = [Math]::Round($targetTop)
605
+ height = [Math]::Round($win.ActualHeight); hwnd =$hwnd.ToInt64()
606
+ workBottom = [Math]::Round($work.Bottom)
607
+ }
608
+ } catch { }
609
+
610
+ # ── 停留阶段:非粘性 = 液面匀速右→左;粘性 = 色条呼吸 ──
611
+ if (-not $isSticky) {
612
+ try {
613
+ $shrink = New-Object System.Windows.Media.Animation.DoubleAnimation(
614
+ 1, 0, [TimeSpan]::FromSeconds($dur))
615
+ # 无缓动:视觉时长 = 真实剩余时间。
616
+ $tintT.BeginAnimation([System.Windows.Media.ScaleTransform]::ScaleXProperty,$shrink)
617
+ } catch { }
618
+ } else {
619
+ try {
620
+ $pulse = New-Object System.Windows.Media.Animation.DoubleAnimation(0.55, 1.0,
621
+ [TimeSpan]::FromMilliseconds(1100))
622
+ $pulse.AutoReverse =$true
623
+ $pulse.RepeatBehavior = [System.Windows.Media.Animation.RepeatBehavior]::Forever
624
+ $pulse.EasingFunction = New-Object System.Windows.Media.Animation.QuadraticEase
625
+ $accentStrip.BeginAnimation([System.Windows.UIElement]::OpacityProperty,$pulse)
626
+ } catch { }
627
+ }
628
+ }.GetNewClosure()) | Out-Null
629
+
630
+ $toastsRef =$script:toasts
631
+ $win.Add_Closed({
632
+ if ($toastsRef.ContainsKey([string]$cmd.key)) {
633
+ $rec =$toastsRef[[string]$cmd.key]
634
+ if ($rec.Timer -ne$null) {
635
+ try { $rec.Timer.Stop() } catch { }
636
+ }
637
+ $toastsRef.Remove([string]$cmd.key) | Out-Null
638
+ }
639
+ Send-Report @{ type = 'exit'; key = [string]$cmd.key; instance = [int]$cmd.instance }
640
+ }.GetNewClosure()) | Out-Null
641
+
642
+ $rec = @{ Win =$win; Body = $bodyText; Title =$titleText; Timer = $null; Instance = [int]$cmd.instance }
643
+ $script:toasts[[string]$cmd.key] = $rec
644
+
645
+ if (-not $isSticky) {
646
+ $timer = New-Object System.Windows.Threading.DispatcherTimer
647
+ $timer.Interval = [TimeSpan]::FromSeconds($dur)
648
+ $timer.Add_Tick({$this.Stop(); Close-WithFade $win }.GetNewClosure()) | Out-Null
649
+ $timer.Start()
650
+ $rec.Timer =$timer
651
+ }
652
+
653
+ return $win
654
+ }
655
+
656
+ # ───────────────────────── 命令处理 ─────────────────────────
657
+
658
+ function Invoke-Command([string]$line) {
659
+ $line = $line.Trim()
660
+ if ([string]::IsNullOrEmpty($line)) { return }
661
+ $cmd = $null
662
+ try { $cmd = $line | ConvertFrom-Json } catch { return }
663
+ if ($cmd -eq $null -or [string]::IsNullOrEmpty([string]$cmd.cmd)) { return }
664
+
665
+ switch ([string]$cmd.cmd) {
666
+ 'show' {
667
+ if ($script:toasts.ContainsKey([string]$cmd.key)) {
668
+ $old = $script:toasts[[string]$cmd.key]
669
+ try { $old.Win.Close() } catch { }
670
+ $script:toasts.Remove([string]$cmd.key) | Out-Null
671
+ }
672
+ $w = New-ToastWindow $cmd
673
+ if ($w -ne $null) { $w.Show() }
674
+ }
675
+ 'text' {
676
+ $rec = $script:toasts[[string]$cmd.key]
677
+ if ($rec -ne $null -and $rec.Body -ne $null) { $rec.Body.Text = [string]$cmd.text }
678
+ }
679
+ 'title' {
680
+ $rec = $script:toasts[[string]$cmd.key]
681
+ if ($rec -ne $null -and $rec.Title -ne $null) { $rec.Title.Text = [string]$cmd.title }
682
+ }
683
+ 'move' {
684
+ $rec = $script:toasts[[string]$cmd.key]
685
+ if ($rec -ne $null) {
686
+ $top = [int]$cmd.top
687
+ $slide = New-Object System.Windows.Media.Animation.DoubleAnimation($rec.Win.Top, $top, [TimeSpan]::FromMilliseconds(220))
688
+ $slide.EasingFunction = $easeMove
689
+ $rec.Win.BeginAnimation([System.Windows.Window]::TopProperty, $slide)
690
+ # 可选横向定位:left 数字 = 绝对 X(工作区相对);left 'default' = 默认右下贴边
691
+ # (工作区右缘 - 宽度 - 20)。用于编辑调整位置/恢复默认后,在屏浮窗立即到位。
692
+ if ($null -ne $cmd.left) {
693
+ $newLeft = 0.0
694
+ if ($cmd.left -eq 'default') {
695
+ $work = [System.Windows.SystemParameters]::WorkArea
696
+ $newLeft = $work.Right - $rec.Win.ActualWidth - 20
697
+ } else {
698
+ $newLeft = [double]$cmd.left
699
+ }
700
+ $slideL = New-Object System.Windows.Media.Animation.DoubleAnimation($rec.Win.Left, $newLeft, [TimeSpan]::FromMilliseconds(220))
701
+ $slideL.EasingFunction = $easeMove
702
+ $rec.Win.BeginAnimation([System.Windows.Window]::LeftProperty, $slideL)
703
+ }
704
+ }
705
+ }
706
+ 'close' {
707
+ $rec = $script:toasts[[string]$cmd.key]
708
+ if ($rec -ne $null) { Close-WithFade $rec.Win }
709
+ }
710
+ 'size' {
711
+ $rec = $script:toasts[[string]$cmd.key]
712
+ if ($rec -ne $null -and $null -ne $cmd.width) { $rec.Win.Width = [double]$cmd.width }
713
+ }
714
+ 'edit' {
715
+ # 编辑模式:关闭旧样板(如有),弹新样板窗(可拖拽/缩放,内嵌类型色卡预览)。
716
+ if ($script:editSample -ne $null) {
717
+ try { $script:editSample.Close() } catch { }
718
+ $script:editSample = $null
719
+ }
720
+ $w = New-EditSample $cmd
721
+ if ($w -ne $null) { $w.Show(); $script:editSample = $w }
722
+ }
723
+ 'edit-end' {
724
+ if ($script:editSample -ne $null) {
725
+ try { $script:editSample.Close() } catch { }
726
+ $script:editSample = $null
727
+ }
728
+ }
729
+ 'shutdown' { $script:exitRequested = $true }
730
+ }
731
+ }
732
+
733
+ # ───────────────────────── 主循环 ─────────────────────────
734
+
735
+ $script:cmdQueue = New-Object 'System.Collections.Concurrent.ConcurrentQueue[string]'
736
+
737
+ $readerScript = {
738
+ param($q)
739
+ try {
740
+ $reader = New-Object System.IO.StreamReader([System.Console]::OpenStandardInput(), (New-Object System.Text.UTF8Encoding($false)))
741
+ while ($true) {
742
+ $line = $reader.ReadLine()
743
+ if ($null -eq $line) { break }
744
+ $q.Enqueue($line)
745
+ }
746
+ } catch { }
747
+ $q.Enqueue('{"cmd":"shutdown"}')
748
+ }
749
+
750
+ $readerRs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
751
+ $readerRs.Open()
752
+ $readerPs = [System.Management.Automation.PowerShell]::Create()
753
+ $readerPs.Runspace = $readerRs
754
+ $readerPs.AddScript($readerScript).AddArgument($script:cmdQueue) | Out-Null
755
+ $readerAsyncResult = $readerPs.BeginInvoke()
756
+
757
+ $cmdTimer = New-Object System.Windows.Threading.DispatcherTimer
758
+ $cmdTimer.Interval = [TimeSpan]::FromMilliseconds(50)
759
+ $cmdTimer.Add_Tick({
760
+ try {
761
+ $line = $null
762
+ while ($script:cmdQueue.TryDequeue([ref]$line)) {
763
+ if (-not [string]::IsNullOrEmpty($line)) { Invoke-Command $line }
764
+ if ($script:exitRequested) { break }
765
+ }
766
+ if ($script:exitRequested) {
767
+ $this.Stop()
768
+ foreach ($key in @($script:toasts.Keys)) {
769
+ try { $script:toasts[$key].Win.Close() } catch { }
770
+ }
771
+ if ($script:editSample -ne $null) {
772
+ try { $script:editSample.Close() } catch { }
773
+ $script:editSample = $null
774
+ }
775
+ [System.Windows.Threading.Dispatcher]::CurrentDispatcher.InvokeShutdown()
776
+ }
777
+ } catch { }
778
+ }) | Out-Null
779
+ $cmdTimer.Start()
780
+
781
+ [System.Windows.Threading.Dispatcher]::Run()
782
+
783
+ try { if ($readerAsyncResult -ne $null) { $readerPs.EndInvoke($readerAsyncResult) } } catch { }
784
+ try { $readerPs.Dispose() } catch { }
785
+ try { $readerRs.Dispose() } catch { }