dsh-notify-yimit 0.1.2 → 0.1.3
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.
- package/README.en.md +2 -2
- package/README.md +4 -2
- package/cordis.patch.yml +6 -6
- package/lib/client.js +1127 -789
- package/lib/index.js +973 -716
- package/lib/toast-host.ps1 +289 -330
- package/lib/typert.host.js +193 -157
- package/package.json +56 -50
package/lib/toast-host.ps1
CHANGED
|
@@ -1,393 +1,352 @@
|
|
|
1
|
-
<#
|
|
2
|
-
|
|
3
|
-
由 dsh-notify-yimit 宿主插件在加载时 spawn 一次并常驻:所有桌面浮窗都在本进程内创建,
|
|
4
|
-
WPF 程序集只加载一次,通知创建延迟从 ~1s 冷启动降到 ~10ms(根治"总感觉慢")。
|
|
5
|
-
|
|
6
|
-
协议(UTF-8,一行一个 JSON):
|
|
7
|
-
stdin(入站命令):
|
|
8
|
-
{"cmd":"show","key":"running:s1","instance":1,"title":"...","text":"...",
|
|
9
|
-
"bg":"#286ebd","fg":"#e6edf3","durationSec":8,"sticky":true,"offsetY":0,
|
|
10
|
-
"sessionId":"s1","baseUrl":"http://127.0.0.1:3080",
|
|
11
|
-
"browserPath":"","winTitle":"dsh-notify-yimit-running:s1"}
|
|
12
|
-
{"cmd":"text","key":...,"text":"..."} # 原地更新内容(不重弹、零延迟)
|
|
13
|
-
{"cmd":"move","key":...,"top":588} # 平滑移动(220ms QuadraticEase)
|
|
14
|
-
{"cmd":"close","key":...} # 160ms 淡出 + 20px 下坠后关闭
|
|
15
|
-
{"cmd":"shutdown"} # 关闭全部窗口并退出
|
|
16
|
-
stdout(出站报告):
|
|
17
|
-
{"type":"pos","key":...,"instance":1,"left":...,"top":...,"height":...,
|
|
18
|
-
"hwnd":...,"workBottom":...}
|
|
19
|
-
{"type":"exit","key":...,"instance":...}
|
|
20
|
-
说明:主线程(STA)跑 WPF Dispatcher;stdin 由独立 runspace 线程读取并放入
|
|
21
|
-
ConcurrentQueue,命令在 Dispatcher 定时器(50ms)中处理——窗口操作线程安全。
|
|
22
|
-
#>
|
|
23
|
-
|
|
1
|
+
<# dsh-notify-yimit 常驻浮窗宿主(Windows PowerShell + WPF)。
|
|
2
|
+
优化点:JSON序列化深度增加、Tag闭包变量传递优化、BeginInvoke资源回收、接收外部Label参数实现多语言。 #>
|
|
24
3
|
Add-Type -AssemblyName PresentationFramework
|
|
25
4
|
Add-Type -AssemblyName PresentationCore
|
|
26
5
|
Add-Type -AssemblyName WindowsBase
|
|
27
6
|
|
|
28
|
-
# WPF 必须运行在 STA 主线程(Windows PowerShell 5.1 控制台主线程为 STA)。
|
|
29
7
|
if ([System.Threading.Thread]::CurrentThread.ApartmentState -ne [System.Threading.ApartmentState]::STA) {
|
|
30
|
-
|
|
31
|
-
|
|
8
|
+
[Console]::Error.WriteLine("dsh-notify-yimit: Host requires STA main thread (current $([System.Threading.Thread]::CurrentThread.ApartmentState))")
|
|
9
|
+
exit 1
|
|
32
10
|
}
|
|
33
11
|
|
|
34
|
-
# ── 协议输出:UTF-8 无 BOM(管道传中文必须固定编码,否则乱码) ────────────────
|
|
35
12
|
$script:utf8 = New-Object System.Text.UTF8Encoding($false)
|
|
36
13
|
$script:stdout = New-Object System.IO.StreamWriter([System.Console]::OpenStandardOutput(), $script:utf8)
|
|
37
14
|
$script:stdout.AutoFlush = $true
|
|
38
15
|
|
|
39
16
|
function Send-Report([object]$obj) {
|
|
40
|
-
|
|
17
|
+
try {
|
|
18
|
+
$script:stdout.WriteLine(($obj | ConvertTo-Json -Compress -Depth 10))
|
|
19
|
+
} catch { }
|
|
41
20
|
}
|
|
42
21
|
|
|
43
|
-
# 浮窗注册表(key → @{ Win; Body; Timer; Instance }),只在 WPF 主线程访问。
|
|
44
22
|
$script:toasts = @{}
|
|
45
23
|
$script:exitRequested = $false
|
|
46
24
|
|
|
47
|
-
# 颜色解析:#RGB / #RRGGBB / #AARRGGBB → Windows.Media.Color
|
|
48
25
|
function Parse-Color([string]$hex, [string]$fallback) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
26
|
+
$h = $hex.TrimStart('#')
|
|
27
|
+
if ($h.Length -eq 3) { $h = ($h.ToCharArray() | ForEach-Object { "$$_" }) -join '' }
|
|
28
|
+
if ($h.Length -eq 6) { $h = "FF$h" }
|
|
29
|
+
if ($h.Length -ne 8 -or $h -notmatch '^[0-9a-fA-F]{8}$') { $h = $fallback.TrimStart('#') }
|
|
30
|
+
return [System.Windows.Media.Color]::FromArgb(
|
|
31
|
+
[Convert]::ToInt32($h.Substring(0,2), 16),
|
|
32
|
+
[Convert]::ToInt32($h.Substring(2,2), 16),
|
|
33
|
+
[Convert]::ToInt32($h.Substring(4,2), 16),
|
|
34
|
+
[Convert]::ToInt32($h.Substring(6,2), 16))
|
|
58
35
|
}
|
|
59
36
|
|
|
60
|
-
# 按钮用 Border 直接实现:标准圆角矩形(10px)。忽略 = 透明底 + 细边框(fg 色);
|
|
61
|
-
# 跳转 = 浅色实心底 + 主色文字。悬停/按压用不透明压暗色反馈(避免透明窗口闪烁)。
|
|
62
37
|
$downP = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(158,164,170))
|
|
63
38
|
$downI = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(30,36,44))
|
|
64
39
|
$hoverP = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(196,202,208))
|
|
65
40
|
$hoverI = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(44,52,62))
|
|
66
41
|
|
|
67
|
-
function New-ToastButton([string]$label, [scriptblock]$onClick, [bool]$primary,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
$
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
$
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
$
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
42
|
+
function New-ToastButton([string]$label, [scriptblock]$onClick, [bool]$primary, [System.Windows.Media.SolidColorBrush]$fg, [System.Windows.Media.SolidColorBrush]$bg) {
|
|
43
|
+
$bd = New-Object System.Windows.Controls.Border
|
|
44
|
+
$bd.CornerRadius = New-Object System.Windows.CornerRadius(10)
|
|
45
|
+
$bd.Padding = New-Object System.Windows.Thickness(18,7,18,7)
|
|
46
|
+
$bd.Margin = New-Object System.Windows.Thickness(0,0,8,0)
|
|
47
|
+
$bd.Cursor = [System.Windows.Input.Cursors]::Hand
|
|
48
|
+
$bd.Focusable = $false
|
|
49
|
+
|
|
50
|
+
$txt = New-Object System.Windows.Controls.TextBlock
|
|
51
|
+
$txt.Text = $label
|
|
52
|
+
$txt.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
|
|
53
|
+
$txt.FontSize = 12
|
|
54
|
+
$txt.VerticalAlignment = [System.Windows.VerticalAlignment]::Center
|
|
55
|
+
$txt.IsHitTestVisible = $false
|
|
56
|
+
|
|
57
|
+
if ($primary) {
|
|
58
|
+
$bd.Background = $fg
|
|
59
|
+
$txt.Foreground = $bg
|
|
60
|
+
$bd.BorderBrush = [System.Windows.Media.Brushes]::Transparent
|
|
61
|
+
$bd.BorderThickness = New-Object System.Windows.Thickness(0)
|
|
62
|
+
} else {
|
|
63
|
+
$bd.Background = [System.Windows.Media.Brushes]::Transparent
|
|
64
|
+
$txt.Foreground = $fg
|
|
65
|
+
$bd.BorderBrush = $fg
|
|
66
|
+
$bd.BorderThickness = New-Object System.Windows.Thickness(1)
|
|
67
|
+
}
|
|
68
|
+
$bd.Child = $txt
|
|
69
|
+
|
|
70
|
+
$bd.Tag = @{ primary = $primary; onClick = $onClick; hoverP = $hoverP; hoverI = $hoverI; downP = $downP; downI = $downI; fg = $fg; bg = $bg }
|
|
71
|
+
|
|
72
|
+
$bd.Add_MouseEnter({
|
|
73
|
+
$info = $this.Tag
|
|
74
|
+
$this.Background = if ($info.primary) { $info.hoverP } else { $info.hoverI }
|
|
75
|
+
}) | Out-Null
|
|
76
|
+
|
|
77
|
+
$bd.Add_MouseLeave({
|
|
78
|
+
$info = $this.Tag
|
|
79
|
+
$this.Background = if ($info.primary) { $info.fg } else { [System.Windows.Media.Brushes]::Transparent }
|
|
80
|
+
}) | Out-Null
|
|
81
|
+
|
|
82
|
+
$bd.Add_MouseLeftButtonDown({
|
|
83
|
+
$info = $this.Tag
|
|
84
|
+
$this.Background = if ($info.primary) { $info.downP } else { $info.downI }
|
|
85
|
+
}) | Out-Null
|
|
86
|
+
|
|
87
|
+
$bd.Add_MouseLeftButtonUp({
|
|
88
|
+
$info = $this.Tag
|
|
89
|
+
try { if ($info.onClick -ne $null) { & $info.onClick } } catch { }
|
|
90
|
+
$this.Background = if ($info.primary) { $info.fg } else { [System.Windows.Media.Brushes]::Transparent }
|
|
91
|
+
}) | Out-Null
|
|
92
|
+
|
|
93
|
+
return $bd
|
|
114
94
|
}
|
|
115
95
|
|
|
116
|
-
# 动画缓动(脚本级,供各处复用)。
|
|
117
|
-
# 注意:PS 5.1 的 New-Object 无法用枚举参数调构造器(会报 "overload ... argument count: 1"),
|
|
118
|
-
# 必须默认构造后设置 EasingMode 属性。
|
|
119
96
|
$easeMove = New-Object System.Windows.Media.Animation.QuadraticEase
|
|
120
97
|
$easeMove.EasingMode = [System.Windows.Media.Animation.EasingMode]::EaseOut
|
|
121
98
|
$easeExit = New-Object System.Windows.Media.Animation.CubicEase
|
|
122
99
|
$easeExit.EasingMode = [System.Windows.Media.Animation.EasingMode]::EaseIn
|
|
123
100
|
|
|
124
|
-
# 淡出关闭:160ms EaseIn 淡出 + 20px 下坠,动画完成后 Close(Closed 事件上报 exit)。
|
|
125
|
-
# 防重入:$w.Tag='closing' 只执行一次(忽略按钮 + close 命令 + 定时器可能先后触发);
|
|
126
|
-
# 600ms 硬性兜底,确保即使动画/定时器异常也强制关闭(绝不挂起)。
|
|
127
101
|
function Close-WithFade($w) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
102
|
+
try {
|
|
103
|
+
if ($w.Tag -eq 'closing') { return }
|
|
104
|
+
$w.Tag = 'closing'
|
|
105
|
+
$fadeOut = New-Object System.Windows.Media.Animation.DoubleAnimation(1, 0, [TimeSpan]::FromMilliseconds(160))
|
|
106
|
+
$fadeOut.EasingFunction = $easeExit
|
|
107
|
+
$fadeOut.Add_Completed({ try { $w.Close() } catch { } }.GetNewClosure()) | Out-Null
|
|
108
|
+
$w.BeginAnimation([System.Windows.Window]::OpacityProperty, $fadeOut)
|
|
109
|
+
|
|
110
|
+
$dropAn = New-Object System.Windows.Media.Animation.DoubleAnimation($w.Top, ($w.Top + 20), [TimeSpan]::FromMilliseconds(160))
|
|
111
|
+
$dropAn.EasingFunction = $easeExit
|
|
112
|
+
$w.BeginAnimation([System.Windows.Window]::TopProperty, $dropAn)
|
|
113
|
+
|
|
114
|
+
$backstop = New-Object System.Windows.Threading.DispatcherTimer
|
|
115
|
+
$backstop.Interval = [TimeSpan]::FromMilliseconds(600)
|
|
116
|
+
$backstop.Tag = $w
|
|
117
|
+
$backstop.Add_Tick({
|
|
118
|
+
$this.Stop()
|
|
119
|
+
try { if ($this.Tag.IsVisible) { $this.Tag.Close() } } catch { }
|
|
120
|
+
}) | Out-Null
|
|
121
|
+
$backstop.Start()
|
|
122
|
+
} catch {
|
|
123
|
+
try { $w.Close() } catch { }
|
|
124
|
+
}
|
|
150
125
|
}
|
|
151
126
|
|
|
152
|
-
# 建窗:按 show 命令参数创建窗口;返回 Window(由调用方 Show() 显示)。
|
|
153
127
|
function New-ToastWindow($cmd) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
128
|
+
$bg = Parse-Color ([string]$cmd.bg) "#20272f"
|
|
129
|
+
$fg = Parse-Color ([string]$cmd.fg) "#e6edf3"
|
|
130
|
+
$bgBrush = New-Object System.Windows.Media.SolidColorBrush($bg)
|
|
131
|
+
$fgBrush = New-Object System.Windows.Media.SolidColorBrush($fg)
|
|
132
|
+
|
|
133
|
+
$win = New-Object System.Windows.Window
|
|
134
|
+
$win.WindowStyle = [System.Windows.WindowStyle]::None
|
|
135
|
+
$win.AllowsTransparency = $true
|
|
136
|
+
$win.Background = [System.Windows.Media.Brushes]::Transparent
|
|
137
|
+
$win.Topmost = $true
|
|
138
|
+
$win.ShowInTaskbar = $false
|
|
139
|
+
$win.Width = 340
|
|
140
|
+
$win.SizeToContent = [System.Windows.SizeToContent]::Height
|
|
141
|
+
if (-not [string]::IsNullOrEmpty([string]$cmd.winTitle)) { $win.Title = [string]$cmd.winTitle }
|
|
142
|
+
|
|
143
|
+
$border = New-Object System.Windows.Controls.Border
|
|
144
|
+
$border.Background = $bgBrush
|
|
145
|
+
$border.BorderBrush = $fgBrush
|
|
146
|
+
$border.BorderThickness = New-Object System.Windows.Thickness(1)
|
|
147
|
+
$border.CornerRadius = New-Object System.Windows.CornerRadius(12)
|
|
148
|
+
$border.Margin = New-Object System.Windows.Thickness(12)
|
|
149
|
+
|
|
150
|
+
$root = New-Object System.Windows.Controls.StackPanel
|
|
151
|
+
$root.Margin = New-Object System.Windows.Thickness(14,12,14,12)
|
|
152
|
+
$border.Child = $root
|
|
153
|
+
|
|
154
|
+
# 优化:支持多语言,接收传入的 label,如果没有则使用后备英文
|
|
155
|
+
$unnamedLabel = if ([string]::IsNullOrEmpty([string]$cmd.unnamedLabel)) { "(Unnamed session)" } else { [string]$cmd.unnamedLabel }
|
|
156
|
+
|
|
157
|
+
$titleText = New-Object System.Windows.Controls.TextBlock
|
|
158
|
+
$titleText.Text = if ([string]::IsNullOrEmpty([string]$cmd.title)) { $unnamedLabel } else { [string]$cmd.title }
|
|
159
|
+
$titleText.Foreground = $fgBrush
|
|
160
|
+
$titleText.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
|
|
161
|
+
$titleText.FontSize = 13
|
|
162
|
+
$titleText.FontWeight = [System.Windows.FontWeights]::SemiBold
|
|
163
|
+
$titleText.TextTrimming = [System.Windows.TextTrimming]::CharacterEllipsis
|
|
164
|
+
$titleText.Margin = New-Object System.Windows.Thickness(0,0,0,6)
|
|
165
|
+
$root.Children.Add($titleText) | Out-Null
|
|
166
|
+
|
|
167
|
+
$bodyText = New-Object System.Windows.Controls.TextBlock
|
|
168
|
+
$bodyText.Text = [string]$cmd.text
|
|
169
|
+
$bodyText.Foreground = $fgBrush
|
|
170
|
+
$bodyText.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
|
|
171
|
+
$bodyText.FontSize = 13
|
|
172
|
+
$bodyText.TextWrapping = [System.Windows.TextWrapping]::Wrap
|
|
173
|
+
$bodyText.Margin = New-Object System.Windows.Thickness(0,0,0,10)
|
|
174
|
+
$root.Children.Add($bodyText) | Out-Null
|
|
175
|
+
|
|
176
|
+
$btnRow = New-Object System.Windows.Controls.StackPanel
|
|
177
|
+
$btnRow.Orientation = [System.Windows.Controls.Orientation]::Horizontal
|
|
178
|
+
$btnRow.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Right
|
|
179
|
+
|
|
180
|
+
$ignoreLabel = if ([string]::IsNullOrEmpty([string]$cmd.ignoreLabel)) { "Ignore" } else { [string]$cmd.ignoreLabel }
|
|
181
|
+
$ignoreBtn = New-ToastButton $ignoreLabel ({ Close-WithFade $win }.GetNewClosure()) $false $fgBrush $bgBrush
|
|
182
|
+
$btnRow.Children.Add($ignoreBtn) | Out-Null
|
|
183
|
+
|
|
184
|
+
if (-not [string]::IsNullOrEmpty([string]$cmd.sessionId)) {
|
|
185
|
+
$jumpLabel = if ([string]::IsNullOrEmpty([string]$cmd.jumpLabel)) { "Open" } else { [string]$cmd.jumpLabel }
|
|
186
|
+
$jumpBtn = New-ToastButton $jumpLabel ({
|
|
187
|
+
$url = "$($cmd.baseUrl)/#dsh-notify-yimit/session=$($cmd.sessionId)"
|
|
188
|
+
try {
|
|
189
|
+
if (-not [string]::IsNullOrEmpty([string]$cmd.browserPath)) {
|
|
190
|
+
Start-Process -FilePath ([string]$cmd.browserPath) -ArgumentList $url
|
|
191
|
+
} else { Start-Process $url }
|
|
192
|
+
} catch { }
|
|
193
|
+
Close-WithFade $win
|
|
194
|
+
}.GetNewClosure()) $true $fgBrush $bgBrush
|
|
195
|
+
$btnRow.Children.Add($jumpBtn) | Out-Null
|
|
196
|
+
}
|
|
197
|
+
$root.Children.Add($btnRow) | Out-Null
|
|
198
|
+
$win.Content = $border
|
|
199
|
+
|
|
200
|
+
$win.Add_Loaded({
|
|
201
|
+
$work = [System.Windows.SystemParameters]::WorkArea
|
|
202
|
+
$targetLeft = $work.Right - $win.ActualWidth - 20
|
|
203
|
+
$targetTop = $work.Bottom - $win.ActualHeight - 20 - [int]$cmd.offsetY
|
|
204
|
+
$win.Left = $targetLeft
|
|
205
|
+
$win.Top = $targetTop + 32
|
|
206
|
+
$win.Opacity = 0
|
|
207
|
+
try {
|
|
208
|
+
$easingIn = New-Object System.Windows.Media.Animation.QuadraticEase
|
|
209
|
+
$easingIn.EasingMode = [System.Windows.Media.Animation.EasingMode]::EaseOut
|
|
210
|
+
$slideIn = New-Object System.Windows.Media.Animation.DoubleAnimation($targetTop + 32, $targetTop, [TimeSpan]::FromMilliseconds(260))
|
|
211
|
+
$slideIn.EasingFunction = $easingIn
|
|
212
|
+
$win.BeginAnimation([System.Windows.Window]::TopProperty, $slideIn)
|
|
213
|
+
$fadeIn = New-Object System.Windows.Media.Animation.DoubleAnimation(0, 1, [TimeSpan]::FromMilliseconds(180))
|
|
214
|
+
$fadeIn.EasingFunction = $easingIn
|
|
215
|
+
$win.BeginAnimation([System.Windows.Window]::OpacityProperty, $fadeIn)
|
|
216
|
+
|
|
217
|
+
$guard = New-Object System.Windows.Threading.DispatcherTimer
|
|
218
|
+
$guard.Interval = [TimeSpan]::FromMilliseconds(400)
|
|
219
|
+
$guard.Tag = @{ w = $win; top = $targetTop }
|
|
220
|
+
$guard.Add_Tick({
|
|
221
|
+
$this.Stop()
|
|
222
|
+
$g = $this.Tag
|
|
223
|
+
try {
|
|
224
|
+
if ($g.w.Opacity -lt 0.9) { $g.w.Opacity = 1 }
|
|
225
|
+
if ($g.w.Top -ne $g.top) { $g.w.Top = $g.top }
|
|
226
|
+
} catch { }
|
|
227
|
+
}) | Out-Null
|
|
228
|
+
$guard.Start()
|
|
229
|
+
} catch {
|
|
230
|
+
$win.Opacity = 1
|
|
231
|
+
$win.Top = $targetTop
|
|
220
232
|
}
|
|
221
|
-
} catch { }
|
|
222
|
-
Close-WithFade $win
|
|
223
|
-
}.GetNewClosure()) $true $fgBrush $bgBrush
|
|
224
|
-
$btnRow.Children.Add($jumpBtn) | Out-Null
|
|
225
|
-
}
|
|
226
|
-
$root.Children.Add($btnRow) | Out-Null
|
|
227
|
-
|
|
228
|
-
$win.Content = $border
|
|
229
|
-
|
|
230
|
-
# 右下角定位 + 滑入动画 + 淡入 + pos 回传(单 Loaded 处理器)。
|
|
231
|
-
# 关键顺序:先把窗口放到"起始位置"(目标下方 32px)且透明度 0,
|
|
232
|
-
# 再启动动画滑向目标并淡入——窗口从起始位置可见地滑入,而不是瞬间显示在终点。
|
|
233
|
-
$win.Add_Loaded({
|
|
234
|
-
$work = [System.Windows.SystemParameters]::WorkArea
|
|
235
|
-
$targetLeft = $work.Right - $win.ActualWidth - 20
|
|
236
|
-
$targetTop = $work.Bottom - $win.ActualHeight - 20 - [int]$cmd.offsetY
|
|
237
|
-
$win.Left = $targetLeft
|
|
238
|
-
$win.Top = $targetTop + 32
|
|
239
|
-
$win.Opacity = 0
|
|
240
|
-
try {
|
|
241
|
-
# 滑入 + 淡入(QuadraticEase EaseOut,260ms 快出缓停;淡入 180ms 先于位移结束)。
|
|
242
|
-
$easingIn = New-Object System.Windows.Media.Animation.QuadraticEase
|
|
243
|
-
$easingIn.EasingMode = [System.Windows.Media.Animation.EasingMode]::EaseOut
|
|
244
|
-
$slideIn = New-Object System.Windows.Media.Animation.DoubleAnimation($targetTop + 32, $targetTop, [TimeSpan]::FromMilliseconds(260))
|
|
245
|
-
$slideIn.EasingFunction = $easingIn
|
|
246
|
-
$win.BeginAnimation([System.Windows.Window]::TopProperty, $slideIn)
|
|
247
|
-
$fadeIn = New-Object System.Windows.Media.Animation.DoubleAnimation(0, 1, [TimeSpan]::FromMilliseconds(180))
|
|
248
|
-
$fadeIn.EasingFunction = $easingIn
|
|
249
|
-
$win.BeginAnimation([System.Windows.Window]::OpacityProperty, $fadeIn)
|
|
250
|
-
# 保险:400ms 后若动画异常导致不可见/未到位,强制恢复(窗口绝不能隐形挂起)。
|
|
251
|
-
$guard = New-Object System.Windows.Threading.DispatcherTimer
|
|
252
|
-
$guard.Interval = [TimeSpan]::FromMilliseconds(400)
|
|
253
|
-
$guard.Tag = @{ w = $win; top = $targetTop }
|
|
254
|
-
$guard.Add_Tick({
|
|
255
|
-
$this.Stop()
|
|
256
|
-
$g = $this.Tag
|
|
257
233
|
try {
|
|
258
|
-
|
|
259
|
-
|
|
234
|
+
$hwnd = [System.Windows.Interop.WindowInteropHelper]::new($win).Handle
|
|
235
|
+
Send-Report @{ type = 'pos'; key = [string]$cmd.key; instance = [int]$cmd.instance
|
|
236
|
+
left = [Math]::Round($targetLeft); top = [Math]::Round($targetTop)
|
|
237
|
+
height = [Math]::Round($win.ActualHeight); hwnd = $hwnd.ToInt64()
|
|
238
|
+
workBottom = [Math]::Round($work.Bottom) }
|
|
260
239
|
} catch { }
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
$win.Add_Closed({
|
|
284
|
-
if ($toastsRef.ContainsKey([string]$cmd.key)) {
|
|
285
|
-
$rec = $toastsRef[[string]$cmd.key]
|
|
286
|
-
if ($rec.Timer -ne $null) { try { $rec.Timer.Stop() } catch { } }
|
|
287
|
-
$toastsRef.Remove([string]$cmd.key) | Out-Null
|
|
240
|
+
}.GetNewClosure()) | Out-Null
|
|
241
|
+
|
|
242
|
+
$toastsRef = $script:toasts
|
|
243
|
+
$win.Add_Closed({
|
|
244
|
+
if ($toastsRef.ContainsKey([string]$cmd.key)) {
|
|
245
|
+
$rec = $toastsRef[[string]$cmd.key]
|
|
246
|
+
if ($rec.Timer -ne $null) { try { $rec.Timer.Stop() } catch { } }
|
|
247
|
+
$toastsRef.Remove([string]$cmd.key) | Out-Null
|
|
248
|
+
}
|
|
249
|
+
Send-Report @{ type = 'exit'; key = [string]$cmd.key; instance = [int]$cmd.instance }
|
|
250
|
+
}.GetNewClosure()) | Out-Null
|
|
251
|
+
|
|
252
|
+
$rec = @{ Win = $win; Body = $bodyText; Title = $titleText; Timer = $null; Instance = [int]$cmd.instance }
|
|
253
|
+
$script:toasts[[string]$cmd.key] = $rec
|
|
254
|
+
|
|
255
|
+
if (-not ($cmd.sticky -eq $true)) {
|
|
256
|
+
$dur = [Math]::Max(1, [int]$cmd.durationSec)
|
|
257
|
+
$timer = New-Object System.Windows.Threading.DispatcherTimer
|
|
258
|
+
$timer.Interval = [TimeSpan]::FromSeconds($dur)
|
|
259
|
+
$timer.Add_Tick({ $this.Stop(); Close-WithFade $win }.GetNewClosure()) | Out-Null
|
|
260
|
+
$timer.Start()
|
|
261
|
+
$rec.Timer = $timer
|
|
288
262
|
}
|
|
289
|
-
|
|
290
|
-
}.GetNewClosure()) | Out-Null
|
|
291
|
-
|
|
292
|
-
# 注册
|
|
293
|
-
$rec = @{ Win = $win; Body = $bodyText; Timer = $null; Instance = [int]$cmd.instance }
|
|
294
|
-
$script:toasts[[string]$cmd.key] = $rec
|
|
295
|
-
|
|
296
|
-
# 非 sticky:定时自动消失(先淡出再关闭)。
|
|
297
|
-
if (-not ($cmd.sticky -eq $true)) {
|
|
298
|
-
$dur = [Math]::Max(1, [int]$cmd.durationSec)
|
|
299
|
-
$timer = New-Object System.Windows.Threading.DispatcherTimer
|
|
300
|
-
$timer.Interval = [TimeSpan]::FromSeconds($dur)
|
|
301
|
-
$timer.Add_Tick({ $this.Stop(); Close-WithFade $win }.GetNewClosure()) | Out-Null
|
|
302
|
-
$timer.Start()
|
|
303
|
-
$rec.Timer = $timer
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return $win
|
|
263
|
+
return $win
|
|
307
264
|
}
|
|
308
265
|
|
|
309
|
-
# ── 命令处理(WPF 主线程 Dispatcher 上执行,窗口操作线程安全) ───────────────
|
|
310
266
|
function Invoke-Command([string]$line) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
267
|
+
$line = $line.Trim()
|
|
268
|
+
if ([string]::IsNullOrEmpty($line)) { return }
|
|
269
|
+
$cmd = $null
|
|
270
|
+
try { $cmd = $line | ConvertFrom-Json } catch { return }
|
|
271
|
+
if ($cmd -eq $null -or [string]::IsNullOrEmpty([string]$cmd.cmd)) { return }
|
|
272
|
+
|
|
273
|
+
switch ([string]$cmd.cmd) {
|
|
274
|
+
'show' {
|
|
275
|
+
if ($script:toasts.ContainsKey([string]$cmd.key)) {
|
|
276
|
+
$old = $script:toasts[[string]$cmd.key]
|
|
277
|
+
try { $old.Win.Close() } catch { }
|
|
278
|
+
$script:toasts.Remove([string]$cmd.key) | Out-Null
|
|
279
|
+
}
|
|
280
|
+
$w = New-ToastWindow $cmd
|
|
281
|
+
if ($w -ne $null) { $w.Show() }
|
|
282
|
+
}
|
|
283
|
+
'text' {
|
|
284
|
+
$rec = $script:toasts[[string]$cmd.key]
|
|
285
|
+
if ($rec -ne $null -and $rec.Body -ne $null) { $rec.Body.Text = [string]$cmd.text }
|
|
286
|
+
}
|
|
287
|
+
'title' {
|
|
288
|
+
$rec = $script:toasts[[string]$cmd.key]
|
|
289
|
+
if ($rec -ne $null -and $rec.Title -ne $null) { $rec.Title.Text = [string]$cmd.title }
|
|
290
|
+
}
|
|
291
|
+
'move' {
|
|
292
|
+
$rec = $script:toasts[[string]$cmd.key]
|
|
293
|
+
if ($rec -ne $null) {
|
|
294
|
+
$top = [int]$cmd.top
|
|
295
|
+
$slide = New-Object System.Windows.Media.Animation.DoubleAnimation($rec.Win.Top, $top, [TimeSpan]::FromMilliseconds(220))
|
|
296
|
+
$slide.EasingFunction = $easeMove
|
|
297
|
+
$rec.Win.BeginAnimation([System.Windows.Window]::TopProperty, $slide)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
'close' {
|
|
301
|
+
$rec = $script:toasts[[string]$cmd.key]
|
|
302
|
+
if ($rec -ne $null) { Close-WithFade $rec.Win }
|
|
303
|
+
}
|
|
304
|
+
'shutdown' { $script:exitRequested = $true }
|
|
343
305
|
}
|
|
344
|
-
'shutdown' { $script:exitRequested = $true }
|
|
345
|
-
}
|
|
346
306
|
}
|
|
347
307
|
|
|
348
|
-
# ── stdin 读取:独立 runspace 线程(阻塞 ReadLine 不卡 WPF 主线程) ───────────
|
|
349
308
|
$script:cmdQueue = New-Object 'System.Collections.Concurrent.ConcurrentQueue[string]'
|
|
350
309
|
$readerScript = {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
$q.Enqueue('{"cmd":"shutdown"}')
|
|
310
|
+
param($q)
|
|
311
|
+
try {
|
|
312
|
+
$reader = New-Object System.IO.StreamReader([System.Console]::OpenStandardInput(), (New-Object System.Text.UTF8Encoding($false)))
|
|
313
|
+
while ($true) {
|
|
314
|
+
$line = $reader.ReadLine()
|
|
315
|
+
if ($null -eq $line) { break }
|
|
316
|
+
$q.Enqueue($line)
|
|
317
|
+
}
|
|
318
|
+
} catch { }
|
|
319
|
+
$q.Enqueue('{"cmd":"shutdown"}')
|
|
362
320
|
}
|
|
321
|
+
|
|
363
322
|
$readerRs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
|
|
364
323
|
$readerRs.Open()
|
|
365
324
|
$readerPs = [System.Management.Automation.PowerShell]::Create()
|
|
366
325
|
$readerPs.Runspace = $readerRs
|
|
367
326
|
$readerPs.AddScript($readerScript).AddArgument($script:cmdQueue) | Out-Null
|
|
368
|
-
$readerPs.BeginInvoke()
|
|
327
|
+
$readerAsyncResult = $readerPs.BeginInvoke()
|
|
369
328
|
|
|
370
|
-
# ── 主循环:50ms 排空命令队列;收到 shutdown 后关闭全部窗口并退出 ─────────────
|
|
371
329
|
$cmdTimer = New-Object System.Windows.Threading.DispatcherTimer
|
|
372
330
|
$cmdTimer.Interval = [TimeSpan]::FromMilliseconds(50)
|
|
373
331
|
$cmdTimer.Add_Tick({
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
332
|
+
try {
|
|
333
|
+
$line = $null
|
|
334
|
+
while ($script:cmdQueue.TryDequeue([ref]$line)) {
|
|
335
|
+
if (-not [string]::IsNullOrEmpty($line)) { Invoke-Command $line }
|
|
336
|
+
if ($script:exitRequested) { break }
|
|
337
|
+
}
|
|
338
|
+
if ($script:exitRequested) {
|
|
339
|
+
$this.Stop()
|
|
340
|
+
foreach ($key in @($script:toasts.Keys)) {
|
|
341
|
+
try { $script:toasts[$key].Win.Close() } catch { }
|
|
342
|
+
}
|
|
343
|
+
[System.Windows.Threading.Dispatcher]::CurrentDispatcher.InvokeShutdown()
|
|
344
|
+
}
|
|
345
|
+
} catch { }
|
|
388
346
|
}) | Out-Null
|
|
389
347
|
$cmdTimer.Start()
|
|
390
|
-
|
|
391
348
|
[System.Windows.Threading.Dispatcher]::Run()
|
|
349
|
+
|
|
350
|
+
try { if ($readerAsyncResult -ne $null) { $readerPs.EndInvoke($readerAsyncResult) } } catch { }
|
|
392
351
|
try { $readerPs.Dispose() } catch { }
|
|
393
352
|
try { $readerRs.Dispose() } catch { }
|