dsh-mini 0.0.0-stage → 0.1.0
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.md +230 -2
- package/app/HudWindow.cs +1278 -0
- package/app/RegionCapture.cs +552 -0
- package/app/SelectionWatcher.cs +850 -0
- package/app/Service.cs +412 -0
- package/app/app.manifest +22 -0
- package/app/bin/DSH/345/260/217/347/252/227.exe +0 -0
- package/app/bin/Microsoft.Web.WebView2.Core.dll +0 -0
- package/app/bin/Microsoft.Web.WebView2.WinForms.dll +0 -0
- package/app/bin/WebView2Loader.dll +0 -0
- package/app/build-app.ps1 +112 -0
- package/app/dev.ps1 +63 -0
- package/app/hud-page.g.cs +164 -0
- package/app/hud-page.js +157 -0
- package/app/probe/clear-composer.js +8 -0
- package/app/probe/dom-probe.js +40 -0
- package/app/probe/drag.ps1 +35 -0
- package/app/probe/dump-composer.js +5 -0
- package/app/probe/frame-probe.js +21 -0
- package/app/probe/selection-sample.txt +4 -0
- package/app/probe/top-probe.js +39 -0
- package/app/vendor/Microsoft.Web.WebView2.Core.dll +0 -0
- package/app/vendor/Microsoft.Web.WebView2.WinForms.dll +0 -0
- package/app/vendor/WebView2Loader.dll +0 -0
- package/cordis.patch.yml +16 -0
- package/lib/client.js +622 -0
- package/lib/index.js +521 -0
- package/package.json +49 -4
- package/scripts/hud-shot.ps1 +168 -0
- package/scripts/native-status.ps1 +63 -0
- package/scripts/win-hud.ps1 +264 -0
- package//345/274/200/345/217/221/346/226/271/346/241/210-WebView2/345/216/237/347/224/237/345/260/217/347/252/227.md +216 -0
package/app/HudWindow.cs
ADDED
|
@@ -0,0 +1,1278 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Diagnostics;
|
|
4
|
+
using System.Drawing;
|
|
5
|
+
using System.Drawing.Imaging;
|
|
6
|
+
using System.IO;
|
|
7
|
+
using System.Runtime.InteropServices;
|
|
8
|
+
using System.Text;
|
|
9
|
+
using System.Threading;
|
|
10
|
+
using System.Windows.Forms;
|
|
11
|
+
using Microsoft.Web.WebView2.Core;
|
|
12
|
+
using Microsoft.Web.WebView2.WinForms;
|
|
13
|
+
|
|
14
|
+
namespace DshHud
|
|
15
|
+
{
|
|
16
|
+
internal static class Native
|
|
17
|
+
{
|
|
18
|
+
internal const int WM_DPICHANGED = 0x02E0;
|
|
19
|
+
|
|
20
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
21
|
+
internal static extern bool SetProcessDpiAwarenessContext(IntPtr value);
|
|
22
|
+
|
|
23
|
+
[DllImport("user32.dll")]
|
|
24
|
+
internal static extern uint GetDpiForSystem();
|
|
25
|
+
|
|
26
|
+
[DllImport("user32.dll")]
|
|
27
|
+
internal static extern IntPtr MonitorFromPoint(POINT pt, uint flags);
|
|
28
|
+
|
|
29
|
+
[DllImport("shcore.dll")]
|
|
30
|
+
internal static extern int GetDpiForMonitor(IntPtr monitor, int dpiType, out uint dpiX, out uint dpiY);
|
|
31
|
+
|
|
32
|
+
[DllImport("user32.dll")]
|
|
33
|
+
internal static extern bool GetMonitorInfo(IntPtr monitor, ref MONITORINFO info);
|
|
34
|
+
|
|
35
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
36
|
+
internal struct RECTW
|
|
37
|
+
{
|
|
38
|
+
public int Left;
|
|
39
|
+
public int Top;
|
|
40
|
+
public int Right;
|
|
41
|
+
public int Bottom;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
45
|
+
internal struct MONITORINFO
|
|
46
|
+
{
|
|
47
|
+
public int cbSize;
|
|
48
|
+
public RECTW rcMonitor;
|
|
49
|
+
public RECTW rcWork;
|
|
50
|
+
public uint dwFlags;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
internal const uint MONITOR_DEFAULTTOPRIMARY = 2;
|
|
54
|
+
|
|
55
|
+
/// <summary>Primary monitor work area in physical pixels.</summary>
|
|
56
|
+
internal static Rectangle WorkArea()
|
|
57
|
+
{
|
|
58
|
+
try
|
|
59
|
+
{
|
|
60
|
+
POINT pt;
|
|
61
|
+
pt.X = 0;
|
|
62
|
+
pt.Y = 0;
|
|
63
|
+
IntPtr mon = MonitorFromPoint(pt, MONITOR_DEFAULTTOPRIMARY);
|
|
64
|
+
MONITORINFO mi = new MONITORINFO();
|
|
65
|
+
mi.cbSize = Marshal.SizeOf(typeof(MONITORINFO));
|
|
66
|
+
if (GetMonitorInfo(mon, ref mi))
|
|
67
|
+
{
|
|
68
|
+
return Rectangle.FromLTRB(mi.rcWork.Left, mi.rcWork.Top, mi.rcWork.Right, mi.rcWork.Bottom);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch
|
|
72
|
+
{
|
|
73
|
+
}
|
|
74
|
+
return Screen.PrimaryScreen.WorkingArea;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
78
|
+
internal struct POINT
|
|
79
|
+
{
|
|
80
|
+
public int X;
|
|
81
|
+
public int Y;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// <summary>Effective DPI of the monitor that owns the given point (0,0 = primary).</summary>
|
|
85
|
+
internal static float EffectiveScale()
|
|
86
|
+
{
|
|
87
|
+
try
|
|
88
|
+
{
|
|
89
|
+
POINT pt;
|
|
90
|
+
pt.X = 0;
|
|
91
|
+
pt.Y = 0;
|
|
92
|
+
IntPtr mon = MonitorFromPoint(pt, 2 /*MONITOR_DEFAULTTOPRIMARY*/);
|
|
93
|
+
uint dx, dy;
|
|
94
|
+
if (GetDpiForMonitor(mon, 0 /*MDT_EFFECTIVE_DPI*/, out dx, out dy) == 0 && dx >= 96)
|
|
95
|
+
{
|
|
96
|
+
return dx / 96f;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch
|
|
100
|
+
{
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
uint dpi = GetDpiForSystem();
|
|
104
|
+
if (dpi >= 96)
|
|
105
|
+
{
|
|
106
|
+
return dpi / 96f;
|
|
107
|
+
}
|
|
108
|
+
return 1f;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
[DllImport("user32.dll")]
|
|
112
|
+
internal static extern bool ReleaseCapture();
|
|
113
|
+
|
|
114
|
+
[DllImport("user32.dll")]
|
|
115
|
+
internal static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
|
|
116
|
+
|
|
117
|
+
[DllImport("user32.dll")]
|
|
118
|
+
internal static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
119
|
+
|
|
120
|
+
[DllImport("user32.dll")]
|
|
121
|
+
internal static extern bool ShowWindow(IntPtr hWnd, int cmd);
|
|
122
|
+
|
|
123
|
+
internal const int SW_RESTORE = 9;
|
|
124
|
+
[DllImport("dwmapi.dll")]
|
|
125
|
+
internal static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int value, int size);
|
|
126
|
+
|
|
127
|
+
internal const int WM_NCHITTEST = 0x0084;
|
|
128
|
+
internal const int WM_NCLBUTTONDOWN = 0x00A1;
|
|
129
|
+
internal const int HTCLIENT = 1;
|
|
130
|
+
internal const int HTCAPTION = 2;
|
|
131
|
+
internal const int HTLEFT = 10;
|
|
132
|
+
internal const int HTRIGHT = 11;
|
|
133
|
+
internal const int HTTOP = 12;
|
|
134
|
+
internal const int HTTOPLEFT = 13;
|
|
135
|
+
internal const int HTTOPRIGHT = 14;
|
|
136
|
+
internal const int HTBOTTOM = 15;
|
|
137
|
+
internal const int HTBOTTOMLEFT = 16;
|
|
138
|
+
internal const int HTBOTTOMRIGHT = 17;
|
|
139
|
+
|
|
140
|
+
internal const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
|
|
141
|
+
internal const int DWMWA_WINDOW_CORNER_PREFERENCE = 33;
|
|
142
|
+
internal const int DWMWCP_ROUND = 2;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
internal sealed class HudForm : Form
|
|
146
|
+
{
|
|
147
|
+
private readonly string startUrl;
|
|
148
|
+
private readonly string pendingPath;
|
|
149
|
+
private readonly string statePath;
|
|
150
|
+
private readonly Mutex mutex;
|
|
151
|
+
|
|
152
|
+
private WebView2 web;
|
|
153
|
+
private NotifyIcon tray;
|
|
154
|
+
private System.Windows.Forms.Timer pendingTimer;
|
|
155
|
+
private SelectionWatcher watcher;
|
|
156
|
+
private ToolStripMenuItem askMenuItem;
|
|
157
|
+
private string pendingQuote;
|
|
158
|
+
private string pendingCmd;
|
|
159
|
+
private HudConfig config;
|
|
160
|
+
private long configStamp;
|
|
161
|
+
private string askFlagPath;
|
|
162
|
+
private Panel titleBar;
|
|
163
|
+
private Label titleLabel;
|
|
164
|
+
private Label pinButton;
|
|
165
|
+
private readonly List<Label> iconButtons = new List<Label>();
|
|
166
|
+
private readonly List<int> iconWidths = new List<int>();
|
|
167
|
+
private Panel gapPanel;
|
|
168
|
+
private System.Windows.Forms.Timer themeTimer;
|
|
169
|
+
private Color chromeBg = Color.FromArgb(247, 247, 248);
|
|
170
|
+
private Color chromeFg = Color.FromArgb(32, 32, 36);
|
|
171
|
+
private bool pinned = true;
|
|
172
|
+
private bool ready;
|
|
173
|
+
private float scale = 1f;
|
|
174
|
+
|
|
175
|
+
internal HudForm(string url, string title, Mutex mutex)
|
|
176
|
+
{
|
|
177
|
+
this.startUrl = url;
|
|
178
|
+
this.mutex = mutex;
|
|
179
|
+
|
|
180
|
+
string dir = Path.Combine(
|
|
181
|
+
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "dsh-hud");
|
|
182
|
+
Directory.CreateDirectory(dir);
|
|
183
|
+
pendingPath = Path.Combine(dir, "pending.txt");
|
|
184
|
+
statePath = Path.Combine(dir, "window.txt");
|
|
185
|
+
askFlagPath = Path.Combine(dir, "ask.txt");
|
|
186
|
+
|
|
187
|
+
scale = Native.EffectiveScale();
|
|
188
|
+
|
|
189
|
+
SuspendLayout();
|
|
190
|
+
FormBorderStyle = FormBorderStyle.None;
|
|
191
|
+
StartPosition = FormStartPosition.Manual;
|
|
192
|
+
AutoScaleMode = AutoScaleMode.None;
|
|
193
|
+
Text = title;
|
|
194
|
+
TopMost = true;
|
|
195
|
+
ShowInTaskbar = true;
|
|
196
|
+
BackColor = Color.FromArgb(247, 247, 248);
|
|
197
|
+
DoubleBuffered = true;
|
|
198
|
+
KeyPreview = true;
|
|
199
|
+
Padding = new Padding(Unit(5));
|
|
200
|
+
MinimumSize = Logical(300, 340);
|
|
201
|
+
Size = Logical(460, 560);
|
|
202
|
+
RestoreSavedBounds();
|
|
203
|
+
|
|
204
|
+
BuildTitleBar(title);
|
|
205
|
+
BuildWebView();
|
|
206
|
+
BuildTray();
|
|
207
|
+
ResumeLayout(false);
|
|
208
|
+
|
|
209
|
+
pendingTimer = new System.Windows.Forms.Timer();
|
|
210
|
+
pendingTimer.Interval = 700;
|
|
211
|
+
pendingTimer.Tick += OnPendingTick;
|
|
212
|
+
pendingTimer.Start();
|
|
213
|
+
|
|
214
|
+
// The page can repaint itself long after load (theme switch, session switch,
|
|
215
|
+
// moving between monitors with different DPI). Keep the chrome in step.
|
|
216
|
+
themeTimer = new System.Windows.Forms.Timer();
|
|
217
|
+
themeTimer.Interval = 6000;
|
|
218
|
+
themeTimer.Tick += delegate
|
|
219
|
+
{
|
|
220
|
+
if (!ready || !Visible) return;
|
|
221
|
+
SyncChromeWithPageAsync(0);
|
|
222
|
+
};
|
|
223
|
+
themeTimer.Start();
|
|
224
|
+
|
|
225
|
+
watcher = new SelectionWatcher(this);
|
|
226
|
+
watcher.QuoteRequested += AskAboutSelection;
|
|
227
|
+
watcher.Enabled = ReadAskFlag();
|
|
228
|
+
|
|
229
|
+
config = HudConfig.Load();
|
|
230
|
+
configStamp = HudConfig.Stamp();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private Size Logical(int w, int h)
|
|
234
|
+
{
|
|
235
|
+
return new Size((int)Math.Round(w * scale), (int)Math.Round(h * scale));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private int Unit(int v)
|
|
239
|
+
{
|
|
240
|
+
return (int)Math.Round(v * scale);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------- chrome ----------
|
|
244
|
+
|
|
245
|
+
private void BuildTitleBar(string title)
|
|
246
|
+
{
|
|
247
|
+
titleBar = new Panel();
|
|
248
|
+
titleBar.Dock = DockStyle.Top;
|
|
249
|
+
titleBar.Height = Unit(36);
|
|
250
|
+
titleBar.BackColor = Blend(chromeBg, chromeFg, 0.04);
|
|
251
|
+
Controls.Add(titleBar);
|
|
252
|
+
|
|
253
|
+
titleLabel = new Label();
|
|
254
|
+
titleLabel.AutoSize = false;
|
|
255
|
+
titleLabel.Dock = DockStyle.Fill;
|
|
256
|
+
titleLabel.TextAlign = ContentAlignment.MiddleLeft;
|
|
257
|
+
titleLabel.Padding = new Padding(Unit(12), 0, 0, 0);
|
|
258
|
+
titleLabel.Font = new Font("Microsoft YaHei UI", 9.5f, FontStyle.Regular);
|
|
259
|
+
titleLabel.ForeColor = chromeFg;
|
|
260
|
+
titleLabel.Text = title;
|
|
261
|
+
titleLabel.MouseDown += OnDragAreaMouseDown;
|
|
262
|
+
titleLabel.MouseDoubleClick += OnTitleDoubleClick;
|
|
263
|
+
titleBar.Controls.Add(titleLabel);
|
|
264
|
+
|
|
265
|
+
Label pin = MakeIcon("\uE718", "\u7F6E\u9876", 44);
|
|
266
|
+
pin.Click += delegate { TogglePin(); };
|
|
267
|
+
pinButton = pin;
|
|
268
|
+
|
|
269
|
+
Label back = MakeIcon("\uE72B", "\u4E3B", 40);
|
|
270
|
+
back.Click += delegate { BackToMain(); };
|
|
271
|
+
|
|
272
|
+
Label shot = MakeIcon("\uE722", "\u622A", 40);
|
|
273
|
+
shot.Click += delegate { DoScreenshot(); };
|
|
274
|
+
|
|
275
|
+
// Region / window picker. Segoe MDL2 "Crop" = E7A8.
|
|
276
|
+
Label crop = MakeIcon("\uE7A8", "\u6846", 40);
|
|
277
|
+
crop.Click += delegate { DoRegionCapture(); };
|
|
278
|
+
|
|
279
|
+
// The close glyph is drawn noticeably larger than the others at the same
|
|
280
|
+
// point size, so it gets its own smaller face and a hover highlight.
|
|
281
|
+
Label close = MakeIcon("\uE8BB", "\u00D7", 40);
|
|
282
|
+
close.Font = new Font("Segoe MDL2 Assets", 8.5f, FontStyle.Regular);
|
|
283
|
+
close.ForeColor = IconTint();
|
|
284
|
+
close.MouseEnter += delegate { close.BackColor = Color.FromArgb(232, 17, 35); close.ForeColor = Color.White; };
|
|
285
|
+
close.MouseLeave += delegate { close.BackColor = Color.Transparent; close.ForeColor = IconTint(); };
|
|
286
|
+
close.Click += delegate { HideToTray(); };
|
|
287
|
+
|
|
288
|
+
// Small spacer so the last key does not sit on the window border. Right
|
|
289
|
+
// docking stacks leftwards, so the spacer is added last to stay rightmost.
|
|
290
|
+
Panel gap = new Panel();
|
|
291
|
+
gap.Dock = DockStyle.Right;
|
|
292
|
+
gap.Width = Unit(6);
|
|
293
|
+
gapPanel = gap;
|
|
294
|
+
|
|
295
|
+
titleBar.Controls.Add(close);
|
|
296
|
+
titleBar.Controls.Add(pin);
|
|
297
|
+
titleBar.Controls.Add(back);
|
|
298
|
+
titleBar.Controls.Add(shot);
|
|
299
|
+
titleBar.Controls.Add(crop);
|
|
300
|
+
titleBar.Controls.Add(gap);
|
|
301
|
+
|
|
302
|
+
titleBar.MouseDown += OnDragAreaMouseDown;
|
|
303
|
+
titleBar.MouseDoubleClick += OnTitleDoubleClick;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private Label MakeIcon(string glyph, string fallback, int width)
|
|
307
|
+
{
|
|
308
|
+
Label b = new Label();
|
|
309
|
+
b.Dock = DockStyle.Right;
|
|
310
|
+
b.Width = Unit(width);
|
|
311
|
+
iconWidths.Add(width);
|
|
312
|
+
b.TextAlign = ContentAlignment.MiddleCenter;
|
|
313
|
+
b.Font = new Font("Segoe MDL2 Assets", 10f, FontStyle.Regular);
|
|
314
|
+
b.ForeColor = IconTint();
|
|
315
|
+
b.Cursor = Cursors.Hand;
|
|
316
|
+
b.Text = glyph;
|
|
317
|
+
b.MouseEnter += delegate { b.BackColor = HoverTint(); };
|
|
318
|
+
b.MouseLeave += delegate { b.BackColor = Color.Transparent; };
|
|
319
|
+
b.Tag = fallback;
|
|
320
|
+
iconButtons.Add(b);
|
|
321
|
+
return b;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/// <summary>
|
|
325
|
+
/// The window frame and title bar follow the page's own colours. Before the
|
|
326
|
+
/// page has reported in, a neutral light grey stands in for it.
|
|
327
|
+
/// </summary>
|
|
328
|
+
private Color Blend(Color a, Color b, double amount)
|
|
329
|
+
{
|
|
330
|
+
int r = (int)Math.Round(a.R + (b.R - a.R) * amount);
|
|
331
|
+
int g = (int)Math.Round(a.G + (b.G - a.G) * amount);
|
|
332
|
+
int bl = (int)Math.Round(a.B + (b.B - a.B) * amount);
|
|
333
|
+
return Color.FromArgb(r, g, bl);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private Color IconTint()
|
|
337
|
+
{
|
|
338
|
+
return Blend(chromeFg, chromeBg, 0.25);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private Color HoverTint()
|
|
342
|
+
{
|
|
343
|
+
return Blend(chromeBg, chromeFg, 0.12);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private void OnDragAreaMouseDown(object sender, MouseEventArgs e)
|
|
347
|
+
{
|
|
348
|
+
if (e.Button != MouseButtons.Left) return;
|
|
349
|
+
Native.ReleaseCapture();
|
|
350
|
+
Native.SendMessage(Handle, Native.WM_NCLBUTTONDOWN, (IntPtr)Native.HTCAPTION, IntPtr.Zero);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private void OnTitleDoubleClick(object sender, MouseEventArgs e)
|
|
354
|
+
{
|
|
355
|
+
Size = (WindowState == FormWindowState.Maximized) ? Logical(460, 920) : Screen.FromControl(this).WorkingArea.Size;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private void TogglePin()
|
|
359
|
+
{
|
|
360
|
+
pinned = !pinned;
|
|
361
|
+
TopMost = pinned;
|
|
362
|
+
pinButton.ForeColor = pinned ? Color.FromArgb(110, 190, 255) : Color.FromArgb(140, 140, 148);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private void BuildWebView()
|
|
366
|
+
{
|
|
367
|
+
web = new WebView2();
|
|
368
|
+
web.Dock = DockStyle.Fill;
|
|
369
|
+
web.DefaultBackgroundColor = Color.FromArgb(24, 24, 27);
|
|
370
|
+
web.AllowExternalDrop = false;
|
|
371
|
+
Controls.Add(web);
|
|
372
|
+
web.BringToFront();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private void BuildTray()
|
|
376
|
+
{
|
|
377
|
+
tray = new NotifyIcon();
|
|
378
|
+
tray.Icon = SystemIcons.Application;
|
|
379
|
+
tray.Text = "DSH \u5C0F\u7A97";
|
|
380
|
+
tray.Visible = true;
|
|
381
|
+
tray.DoubleClick += delegate { RestoreFromTray(); };
|
|
382
|
+
|
|
383
|
+
ContextMenuStrip menu = new ContextMenuStrip();
|
|
384
|
+
menu.Items.Add("\u663E\u793A\u5C0F\u7A97", null, delegate { RestoreFromTray(); });
|
|
385
|
+
|
|
386
|
+
askMenuItem = new ToolStripMenuItem("\u5212\u8BCD\u95EE\u5927\u80A5\u9C7C");
|
|
387
|
+
askMenuItem.CheckOnClick = true;
|
|
388
|
+
askMenuItem.Checked = true;
|
|
389
|
+
askMenuItem.CheckedChanged += delegate
|
|
390
|
+
{
|
|
391
|
+
if (watcher == null) return;
|
|
392
|
+
watcher.Enabled = askMenuItem.Checked;
|
|
393
|
+
WriteAskFlag(askMenuItem.Checked);
|
|
394
|
+
};
|
|
395
|
+
menu.Items.Add(askMenuItem);
|
|
396
|
+
|
|
397
|
+
menu.Items.Add(new ToolStripSeparator());
|
|
398
|
+
menu.Items.Add("\u9000\u51FA", null, delegate { Close(); });
|
|
399
|
+
tray.ContextMenuStrip = menu;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private void RestoreFromTray()
|
|
403
|
+
{
|
|
404
|
+
Show();
|
|
405
|
+
WindowState = FormWindowState.Normal;
|
|
406
|
+
Activate();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/// <summary>
|
|
410
|
+
/// The close key parks the window in the tray instead of exiting: the selection
|
|
411
|
+
/// watcher lives in this process, so the "问大肥鱼" pill would die with it.
|
|
412
|
+
/// </summary>
|
|
413
|
+
private void HideToTray()
|
|
414
|
+
{
|
|
415
|
+
Hide();
|
|
416
|
+
if (tray != null && !tray.Visible) tray.Visible = true;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private bool ReadAskFlag()
|
|
420
|
+
{
|
|
421
|
+
try
|
|
422
|
+
{
|
|
423
|
+
if (!File.Exists(askFlagPath)) return true;
|
|
424
|
+
return File.ReadAllText(askFlagPath).Trim() != "0";
|
|
425
|
+
}
|
|
426
|
+
catch
|
|
427
|
+
{
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private void WriteAskFlag(bool on)
|
|
433
|
+
{
|
|
434
|
+
try
|
|
435
|
+
{
|
|
436
|
+
File.WriteAllText(askFlagPath, on ? "1" : "0", Encoding.UTF8);
|
|
437
|
+
}
|
|
438
|
+
catch
|
|
439
|
+
{
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ---------- lifecycle ----------
|
|
444
|
+
|
|
445
|
+
protected override void OnHandleCreated(EventArgs e)
|
|
446
|
+
{
|
|
447
|
+
base.OnHandleCreated(e);
|
|
448
|
+
try
|
|
449
|
+
{
|
|
450
|
+
int round = Native.DWMWCP_ROUND;
|
|
451
|
+
Native.DwmSetWindowAttribute(Handle, Native.DWMWA_WINDOW_CORNER_PREFERENCE, ref round, sizeof(int));
|
|
452
|
+
int dark = 1;
|
|
453
|
+
Native.DwmSetWindowAttribute(Handle, Native.DWMWA_USE_IMMERSIVE_DARK_MODE, ref dark, sizeof(int));
|
|
454
|
+
}
|
|
455
|
+
catch
|
|
456
|
+
{
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
protected override void OnShown(EventArgs e)
|
|
461
|
+
{
|
|
462
|
+
base.OnShown(e);
|
|
463
|
+
InitWebViewAsync();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private async void InitWebViewAsync()
|
|
467
|
+
{
|
|
468
|
+
try
|
|
469
|
+
{
|
|
470
|
+
string udf = Path.Combine(
|
|
471
|
+
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "dsh-hud", "webview2");
|
|
472
|
+
Directory.CreateDirectory(udf);
|
|
473
|
+
CoreWebView2Environment env = await CoreWebView2Environment.CreateAsync(null, udf, null);
|
|
474
|
+
await web.EnsureCoreWebView2Async(env);
|
|
475
|
+
|
|
476
|
+
web.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false;
|
|
477
|
+
web.CoreWebView2.Settings.IsStatusBarEnabled = false;
|
|
478
|
+
web.CoreWebView2.Settings.AreDevToolsEnabled = false;
|
|
479
|
+
web.CoreWebView2.Settings.IsZoomControlEnabled = false;
|
|
480
|
+
web.CoreWebView2.WebMessageReceived += OnWebMessage;
|
|
481
|
+
web.CoreWebView2.NavigationCompleted += OnNavigationCompleted;
|
|
482
|
+
|
|
483
|
+
// A real browser zoom: the layout viewport becomes 460/0.9 CSS px, so
|
|
484
|
+
// the document still fills the window edge to edge. CSS `zoom` on the
|
|
485
|
+
// body only shrank the used height, which left blank bands.
|
|
486
|
+
web.ZoomFactor = 0.9;
|
|
487
|
+
|
|
488
|
+
try
|
|
489
|
+
{
|
|
490
|
+
SyncHintCookies(startUrl);
|
|
491
|
+
}
|
|
492
|
+
catch
|
|
493
|
+
{
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
web.CoreWebView2.Navigate(startUrl);
|
|
497
|
+
ready = true;
|
|
498
|
+
}
|
|
499
|
+
catch (Exception ex)
|
|
500
|
+
{
|
|
501
|
+
MessageBox.Show("WebView2 \u521D\u59CB\u5316\u5931\u8D25\uFF1A" + ex.Message,
|
|
502
|
+
"DSH \u5C0F\u7A97", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private void OnPendingTick(object sender, EventArgs e)
|
|
507
|
+
{
|
|
508
|
+
try
|
|
509
|
+
{
|
|
510
|
+
if (File.Exists(pendingPath))
|
|
511
|
+
{
|
|
512
|
+
string url = File.ReadAllText(pendingPath, Encoding.UTF8).Trim();
|
|
513
|
+
File.Delete(pendingPath);
|
|
514
|
+
if (url.Length > 0 && ready)
|
|
515
|
+
{
|
|
516
|
+
SyncHintCookies(url);
|
|
517
|
+
web.CoreWebView2.Navigate(url);
|
|
518
|
+
RestoreFromTray();
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
catch
|
|
523
|
+
{
|
|
524
|
+
}
|
|
525
|
+
PumpEval();
|
|
526
|
+
PumpRequests();
|
|
527
|
+
|
|
528
|
+
try
|
|
529
|
+
{
|
|
530
|
+
string pillPath = Path.Combine(Path.GetDirectoryName(pendingPath), "showpill.txt");
|
|
531
|
+
if (File.Exists(pillPath))
|
|
532
|
+
{
|
|
533
|
+
File.Delete(pillPath);
|
|
534
|
+
if (watcher != null) watcher.DebugPill();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
string regionPath = Path.Combine(Path.GetDirectoryName(pendingPath), "regionshot.txt");
|
|
538
|
+
if (File.Exists(regionPath))
|
|
539
|
+
{
|
|
540
|
+
File.Delete(regionPath);
|
|
541
|
+
DoRegionCapture();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
string testPath = Path.Combine(Path.GetDirectoryName(pendingPath), "seltest.txt");
|
|
545
|
+
if (File.Exists(testPath))
|
|
546
|
+
{
|
|
547
|
+
File.Delete(testPath);
|
|
548
|
+
if (watcher != null) watcher.TestProbe();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
catch
|
|
552
|
+
{
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/// <summary>
|
|
557
|
+
/// Work handed over by the background service: a quote to insert, or a screenshot
|
|
558
|
+
/// command from a global hotkey. Both arrive as files so the two processes never
|
|
559
|
+
/// need to agree on IPC.
|
|
560
|
+
/// </summary>
|
|
561
|
+
private void PumpRequests()
|
|
562
|
+
{
|
|
563
|
+
try
|
|
564
|
+
{
|
|
565
|
+
long stamp = HudConfig.Stamp();
|
|
566
|
+
if (stamp != configStamp)
|
|
567
|
+
{
|
|
568
|
+
configStamp = stamp;
|
|
569
|
+
config = HudConfig.Load();
|
|
570
|
+
Log("config reloaded copyShot=" + config.CopyShot);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
string quote = HudConfig.QuotePath;
|
|
574
|
+
if (File.Exists(quote))
|
|
575
|
+
{
|
|
576
|
+
string text = File.ReadAllText(quote, Encoding.UTF8);
|
|
577
|
+
File.Delete(quote);
|
|
578
|
+
if (!string.IsNullOrEmpty(text))
|
|
579
|
+
{
|
|
580
|
+
Log("quote from service " + text.Length + " chars");
|
|
581
|
+
pendingQuote = text;
|
|
582
|
+
RestoreFromTray();
|
|
583
|
+
if (ready && web != null && web.CoreWebView2 != null) FlushPendingQuote();
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
string cmd = HudConfig.CmdPath;
|
|
588
|
+
if (File.Exists(cmd))
|
|
589
|
+
{
|
|
590
|
+
string kind = File.ReadAllText(cmd, Encoding.UTF8).Trim();
|
|
591
|
+
File.Delete(cmd);
|
|
592
|
+
if (kind.Length > 0)
|
|
593
|
+
{
|
|
594
|
+
Log("cmd from service: " + kind);
|
|
595
|
+
if (!ready) pendingCmd = kind;
|
|
596
|
+
else RunCmd(kind);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
else if (pendingCmd != null && ready)
|
|
600
|
+
{
|
|
601
|
+
string kind = pendingCmd;
|
|
602
|
+
pendingCmd = null;
|
|
603
|
+
RunCmd(kind);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
catch
|
|
607
|
+
{
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private void RunCmd(string kind)
|
|
612
|
+
{
|
|
613
|
+
if (kind == "region") DoRegionCapture();
|
|
614
|
+
else DoScreenshot();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/// <summary>
|
|
618
|
+
/// The GUI authenticates a fresh profile by redirecting to a cleaned URL, which
|
|
619
|
+
/// drops our query flags. Cookies survive that redirect, so the hand-off values
|
|
620
|
+
/// live there where the page can still read them.
|
|
621
|
+
/// </summary>
|
|
622
|
+
private void SyncHintCookies(string url)
|
|
623
|
+
{
|
|
624
|
+
CoreWebView2Cookie hud = web.CoreWebView2.CookieManager.CreateCookie("dsh_hud", "1", "127.0.0.1", "/");
|
|
625
|
+
hud.Expires = DateTime.Now.AddHours(12);
|
|
626
|
+
web.CoreWebView2.CookieManager.AddOrUpdateCookie(hud);
|
|
627
|
+
|
|
628
|
+
string session = QueryValue(url, "session");
|
|
629
|
+
CoreWebView2Cookie handoff = web.CoreWebView2.CookieManager.CreateCookie(
|
|
630
|
+
"dsh_hud_session", session, "127.0.0.1", "/");
|
|
631
|
+
handoff.Expires = session.Length == 0 ? DateTime.Now.AddSeconds(-1) : DateTime.Now.AddHours(12);
|
|
632
|
+
web.CoreWebView2.CookieManager.AddOrUpdateCookie(handoff);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private static string QueryValue(string url, string key)
|
|
636
|
+
{
|
|
637
|
+
int question = url.IndexOf('?');
|
|
638
|
+
if (question < 0) return "";
|
|
639
|
+
string query = url.Substring(question + 1);
|
|
640
|
+
int hash = query.IndexOf('#');
|
|
641
|
+
if (hash >= 0) query = query.Substring(0, hash);
|
|
642
|
+
string[] parts = query.Split('&');
|
|
643
|
+
for (int i = 0; i < parts.Length; i++)
|
|
644
|
+
{
|
|
645
|
+
int equals = parts[i].IndexOf('=');
|
|
646
|
+
if (equals <= 0) continue;
|
|
647
|
+
if (parts[i].Substring(0, equals) == key)
|
|
648
|
+
{
|
|
649
|
+
return Uri.UnescapeDataString(parts[i].Substring(equals + 1));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return "";
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/// <summary>Dev loop: drop JS into %LOCALAPPDATA%\dsh-hud\eval.js, result lands in hud.log.</summary>
|
|
656
|
+
private void PumpEval()
|
|
657
|
+
{
|
|
658
|
+
try
|
|
659
|
+
{
|
|
660
|
+
if (!ready) return;
|
|
661
|
+
string dir = Path.GetDirectoryName(pendingPath);
|
|
662
|
+
string evalPath = Path.Combine(dir, "eval.js");
|
|
663
|
+
if (!File.Exists(evalPath)) return;
|
|
664
|
+
string script = File.ReadAllText(evalPath, Encoding.UTF8);
|
|
665
|
+
File.Delete(evalPath);
|
|
666
|
+
RunEval(script);
|
|
667
|
+
}
|
|
668
|
+
catch
|
|
669
|
+
{
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
private async void RunEval(string script)
|
|
674
|
+
{
|
|
675
|
+
string dir = Path.GetDirectoryName(pendingPath);
|
|
676
|
+
string logPath = Path.Combine(dir, "hud.log");
|
|
677
|
+
try
|
|
678
|
+
{
|
|
679
|
+
string result = await web.CoreWebView2.ExecuteScriptAsync(script);
|
|
680
|
+
File.WriteAllText(logPath, result, Encoding.UTF8);
|
|
681
|
+
}
|
|
682
|
+
catch (Exception ex)
|
|
683
|
+
{
|
|
684
|
+
try { File.WriteAllText(logPath, "eval failed: " + ex.Message, Encoding.UTF8); }
|
|
685
|
+
catch { }
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// ---------- page bridge ----------
|
|
690
|
+
|
|
691
|
+
private void OnNavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e)
|
|
692
|
+
{
|
|
693
|
+
if (!e.IsSuccess) return;
|
|
694
|
+
try
|
|
695
|
+
{
|
|
696
|
+
web.CoreWebView2.ExecuteScriptAsync(HudPage.Script);
|
|
697
|
+
}
|
|
698
|
+
catch
|
|
699
|
+
{
|
|
700
|
+
}
|
|
701
|
+
// The app paints itself a moment after load, so sample the theme twice.
|
|
702
|
+
SyncChromeWithPageAsync(400);
|
|
703
|
+
SyncChromeWithPageAsync(1800);
|
|
704
|
+
if (Environment.GetEnvironmentVariable("DSH_HUD_DEBUG") == "1")
|
|
705
|
+
{
|
|
706
|
+
RunProbe();
|
|
707
|
+
}
|
|
708
|
+
if (pendingQuote != null && web != null && web.CoreWebView2 != null)
|
|
709
|
+
{
|
|
710
|
+
QuoteAsync(pendingQuote);
|
|
711
|
+
pendingQuote = null;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/// <summary>
|
|
716
|
+
/// Paint the frame and title bar in the page's own colours, so a light document
|
|
717
|
+
/// does not sit inside a black border. The page reports the two computed values
|
|
718
|
+
/// (`background-color|color`) and the shell recolours its chrome from them.
|
|
719
|
+
/// </summary>
|
|
720
|
+
private async void SyncChromeWithPageAsync(int delay)
|
|
721
|
+
{
|
|
722
|
+
if (delay > 0)
|
|
723
|
+
{
|
|
724
|
+
try { await System.Threading.Tasks.Task.Delay(delay); }
|
|
725
|
+
catch { return; }
|
|
726
|
+
}
|
|
727
|
+
try
|
|
728
|
+
{
|
|
729
|
+
if (!ready || web.CoreWebView2 == null) return;
|
|
730
|
+
string raw = await web.CoreWebView2.ExecuteScriptAsync(
|
|
731
|
+
"(function(){var s=getComputedStyle(document.body);" +
|
|
732
|
+
"return s.backgroundColor+'|'+s.color;})()");
|
|
733
|
+
string text = Unquote(raw);
|
|
734
|
+
string[] parts = text.Split('|');
|
|
735
|
+
if (parts.Length < 2) return;
|
|
736
|
+
Color bg = ParseCssColor(parts[0], Color.Empty);
|
|
737
|
+
Color fg = ParseCssColor(parts[1], Color.Empty);
|
|
738
|
+
if (bg.IsEmpty || bg.A == 0) return;
|
|
739
|
+
if (fg.IsEmpty) fg = Luminance(bg) > 0.55 ? Color.FromArgb(32, 32, 36) : Color.FromArgb(235, 235, 240);
|
|
740
|
+
ApplyChrome(bg, fg);
|
|
741
|
+
}
|
|
742
|
+
catch
|
|
743
|
+
{
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
private void ApplyChrome(Color bg, Color fg)
|
|
748
|
+
{
|
|
749
|
+
chromeBg = bg;
|
|
750
|
+
chromeFg = fg;
|
|
751
|
+
try
|
|
752
|
+
{
|
|
753
|
+
BackColor = bg;
|
|
754
|
+
if (titleBar != null) titleBar.BackColor = Blend(bg, fg, 0.04);
|
|
755
|
+
if (titleLabel != null) titleLabel.ForeColor = fg;
|
|
756
|
+
for (int i = 0; i < iconButtons.Count; i++) iconButtons[i].ForeColor = IconTint();
|
|
757
|
+
if (pinButton != null) pinButton.ForeColor = IconTint();
|
|
758
|
+
|
|
759
|
+
int dark = Luminance(bg) < 0.45 ? 1 : 0;
|
|
760
|
+
if (IsHandleCreated)
|
|
761
|
+
{
|
|
762
|
+
Native.DwmSetWindowAttribute(Handle, Native.DWMWA_USE_IMMERSIVE_DARK_MODE, ref dark, sizeof(int));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
catch
|
|
766
|
+
{
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
private static double Luminance(Color c)
|
|
771
|
+
{
|
|
772
|
+
return (0.2126 * c.R + 0.7152 * c.G + 0.0722 * c.B) / 255.0;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/// <summary>ExecuteScriptAsync answers with a JSON literal; unwrap it.</summary>
|
|
776
|
+
private static string Unquote(string raw)
|
|
777
|
+
{
|
|
778
|
+
if (raw == null) return "";
|
|
779
|
+
string text = raw.Trim();
|
|
780
|
+
if (text.Length >= 2 && text[0] == '"' && text[text.Length - 1] == '"')
|
|
781
|
+
{
|
|
782
|
+
text = text.Substring(1, text.Length - 2);
|
|
783
|
+
}
|
|
784
|
+
return text.Replace("\\\"", "\"").Replace("\\u0026", "&").Replace("\\/", "/");
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
private static Color ParseCssColor(string value, Color fallback)
|
|
788
|
+
{
|
|
789
|
+
if (value == null) return fallback;
|
|
790
|
+
string text = value.Trim();
|
|
791
|
+
if (text.Length == 0) return fallback;
|
|
792
|
+
int open = text.IndexOf('(');
|
|
793
|
+
if (open < 0 || text.IndexOf("rgb", StringComparison.OrdinalIgnoreCase) != 0) return fallback;
|
|
794
|
+
int close = text.IndexOf(')', open);
|
|
795
|
+
if (close < 0) return fallback;
|
|
796
|
+
string[] parts = text.Substring(open + 1, close - open - 1).Split(',');
|
|
797
|
+
if (parts.Length < 3) return fallback;
|
|
798
|
+
int r, g, b;
|
|
799
|
+
if (!int.TryParse(parts[0].Trim(), out r)) return fallback;
|
|
800
|
+
if (!int.TryParse(parts[1].Trim(), out g)) return fallback;
|
|
801
|
+
if (!int.TryParse(parts[2].Trim(), out b)) return fallback;
|
|
802
|
+
int a = 255;
|
|
803
|
+
if (parts.Length >= 4)
|
|
804
|
+
{
|
|
805
|
+
double alpha;
|
|
806
|
+
if (double.TryParse(parts[3].Trim(), System.Globalization.NumberStyles.Float,
|
|
807
|
+
System.Globalization.CultureInfo.InvariantCulture, out alpha))
|
|
808
|
+
{
|
|
809
|
+
a = (int)Math.Round(alpha * 255);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return Color.FromArgb(a, Math.Min(255, r), Math.Min(255, g), Math.Min(255, b));
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
private async void RunProbe()
|
|
816
|
+
{
|
|
817
|
+
string logPath = Path.Combine(Path.GetDirectoryName(pendingPath), "hud.log");
|
|
818
|
+
try
|
|
819
|
+
{
|
|
820
|
+
await System.Threading.Tasks.Task.Delay(3000);
|
|
821
|
+
string raw = await web.CoreWebView2.ExecuteScriptAsync(
|
|
822
|
+
"JSON.stringify(window.__dshHud ? window.__dshHud.probe() : {err:'bridge missing'})");
|
|
823
|
+
File.WriteAllText(logPath, raw, Encoding.UTF8);
|
|
824
|
+
}
|
|
825
|
+
catch (Exception ex)
|
|
826
|
+
{
|
|
827
|
+
try
|
|
828
|
+
{
|
|
829
|
+
File.WriteAllText(logPath, "probe failed: " + ex.Message, Encoding.UTF8);
|
|
830
|
+
}
|
|
831
|
+
catch
|
|
832
|
+
{
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
private void OnWebMessage(object sender, CoreWebView2WebMessageReceivedEventArgs e)
|
|
838
|
+
{
|
|
839
|
+
string text;
|
|
840
|
+
try
|
|
841
|
+
{
|
|
842
|
+
text = e.TryGetWebMessageAsString();
|
|
843
|
+
}
|
|
844
|
+
catch
|
|
845
|
+
{
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if (text == null) return;
|
|
849
|
+
|
|
850
|
+
if (text.IndexOf("\"shot\"", StringComparison.Ordinal) >= 0) { DoScreenshot(); return; }
|
|
851
|
+
if (text.IndexOf("\"shot-region\"", StringComparison.Ordinal) >= 0) { DoRegionCapture(); return; }
|
|
852
|
+
if (text.IndexOf("\"pin\"", StringComparison.Ordinal) >= 0) { TogglePin(); return; }
|
|
853
|
+
if (text.IndexOf("\"close\"", StringComparison.Ordinal) >= 0) { Close(); return; }
|
|
854
|
+
if (text.IndexOf("\"hide\"", StringComparison.Ordinal) >= 0) { Hide(); return; }
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
private void BackToMain()
|
|
858
|
+
{
|
|
859
|
+
try
|
|
860
|
+
{
|
|
861
|
+
Process self = Process.GetCurrentProcess();
|
|
862
|
+
foreach (Process p in Process.GetProcessesByName("DeepSeek Harness"))
|
|
863
|
+
{
|
|
864
|
+
if (p.Id == self.Id) continue;
|
|
865
|
+
IntPtr h = p.MainWindowHandle;
|
|
866
|
+
if (h == IntPtr.Zero) continue;
|
|
867
|
+
Native.ShowWindow(h, Native.SW_RESTORE);
|
|
868
|
+
Native.SetForegroundWindow(h);
|
|
869
|
+
break;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
catch
|
|
873
|
+
{
|
|
874
|
+
}
|
|
875
|
+
Hide();
|
|
876
|
+
}
|
|
877
|
+
private void SendToPage(string json)
|
|
878
|
+
{
|
|
879
|
+
if (!ready) return;
|
|
880
|
+
try
|
|
881
|
+
{
|
|
882
|
+
web.CoreWebView2.PostWebMessageAsJson(json);
|
|
883
|
+
}
|
|
884
|
+
catch
|
|
885
|
+
{
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// ---------- 划词问大肥鱼 ----------
|
|
890
|
+
|
|
891
|
+
/// <summary>
|
|
892
|
+
/// Called when the user clicks the pill next to a selection. Brings the window up
|
|
893
|
+
/// and quotes the captured text into the composer.
|
|
894
|
+
/// </summary>
|
|
895
|
+
private void AskAboutSelection(string text)
|
|
896
|
+
{
|
|
897
|
+
if (string.IsNullOrEmpty(text)) return;
|
|
898
|
+
pendingQuote = text;
|
|
899
|
+
RestoreFromTray();
|
|
900
|
+
if (ready && web != null && web.CoreWebView2 != null) FlushPendingQuote();
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
private void FlushPendingQuote()
|
|
904
|
+
{
|
|
905
|
+
if (pendingQuote == null) return;
|
|
906
|
+
string text = pendingQuote;
|
|
907
|
+
pendingQuote = null;
|
|
908
|
+
QuoteAsync(text);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
private async void QuoteAsync(string text)
|
|
912
|
+
{
|
|
913
|
+
try
|
|
914
|
+
{
|
|
915
|
+
string script = "(function(){try{return window.__dshHud && window.__dshHud.quote("
|
|
916
|
+
+ "\"" + Escape(text) + "\") ? 'ok' : 'fail';}catch(e){return 'err:' + e.message;}})()";
|
|
917
|
+
string result = await web.CoreWebView2.ExecuteScriptAsync(script);
|
|
918
|
+
Log("quote " + text.Length + " chars -> " + result);
|
|
919
|
+
}
|
|
920
|
+
catch (Exception ex)
|
|
921
|
+
{
|
|
922
|
+
Log("quote failed: " + ex.Message);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
private void Log(string line)
|
|
927
|
+
{
|
|
928
|
+
try
|
|
929
|
+
{
|
|
930
|
+
string logPath = Path.Combine(Path.GetDirectoryName(pendingPath), "hud.log");
|
|
931
|
+
File.AppendAllText(logPath, DateTime.Now.ToString("HH:mm:ss") + " " + line + "\r\n", Encoding.UTF8);
|
|
932
|
+
}
|
|
933
|
+
catch
|
|
934
|
+
{
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
private static string Escape(string s)
|
|
939
|
+
{
|
|
940
|
+
if (s == null) return "";
|
|
941
|
+
StringBuilder sb = new StringBuilder(s.Length + 8);
|
|
942
|
+
foreach (char c in s)
|
|
943
|
+
{
|
|
944
|
+
if (c == '"' || c == '\\') sb.Append('\\').Append(c);
|
|
945
|
+
else if (c == '\n') sb.Append("\\n");
|
|
946
|
+
else if (c == '\r') sb.Append("\\r");
|
|
947
|
+
else if (c < ' ') sb.Append(' ');
|
|
948
|
+
else sb.Append(c);
|
|
949
|
+
}
|
|
950
|
+
return sb.ToString();
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
private void CopyToClipboard(Image img)
|
|
954
|
+
{
|
|
955
|
+
try
|
|
956
|
+
{
|
|
957
|
+
Clipboard.SetImage(img);
|
|
958
|
+
Log("copied to clipboard " + img.Width + "x" + img.Height);
|
|
959
|
+
}
|
|
960
|
+
catch (Exception ex)
|
|
961
|
+
{
|
|
962
|
+
Log("clipboard failed: " + ex.Message);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
private void DoScreenshot()
|
|
967
|
+
{
|
|
968
|
+
if (!ready) return;
|
|
969
|
+
Hide();
|
|
970
|
+
Application.DoEvents();
|
|
971
|
+
Thread.Sleep(320);
|
|
972
|
+
try
|
|
973
|
+
{
|
|
974
|
+
Rectangle vs = RegionCaptureForm.PhysicalVirtualScreen();
|
|
975
|
+
using (Bitmap bmp = new Bitmap(vs.Width, vs.Height))
|
|
976
|
+
{
|
|
977
|
+
using (Graphics g = Graphics.FromImage(bmp))
|
|
978
|
+
{
|
|
979
|
+
g.CopyFromScreen(vs.X, vs.Y, 0, 0, new Size(vs.Width, vs.Height), CopyPixelOperation.SourceCopy);
|
|
980
|
+
}
|
|
981
|
+
byte[] bytes;
|
|
982
|
+
using (MemoryStream ms = new MemoryStream())
|
|
983
|
+
{
|
|
984
|
+
bmp.Save(ms, ImageFormat.Png);
|
|
985
|
+
bytes = ms.ToArray();
|
|
986
|
+
}
|
|
987
|
+
string b64 = Convert.ToBase64String(bytes);
|
|
988
|
+
if (config != null && config.CopyShot) CopyToClipboard(bmp);
|
|
989
|
+
SendToPage("{\"type\":\"shot-result\",\"dataUrl\":\"data:image/png;base64," + b64 +
|
|
990
|
+
"\",\"width\":" + vs.Width + ",\"height\":" + vs.Height + "}");
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
catch (Exception ex)
|
|
994
|
+
{
|
|
995
|
+
SendToPage("{\"type\":\"shot-error\",\"error\":\"" + Escape(ex.Message) + "\"}");
|
|
996
|
+
}
|
|
997
|
+
finally
|
|
998
|
+
{
|
|
999
|
+
Show();
|
|
1000
|
+
Activate();
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/// <summary>
|
|
1005
|
+
/// Freeze the desktop, let the user pick a window (click) or a rectangle (drag),
|
|
1006
|
+
/// then crop that out of the frozen frame. Cropping the frozen bitmap instead of
|
|
1007
|
+
/// re-capturing means the overlay can never end up in its own picture.
|
|
1008
|
+
/// </summary>
|
|
1009
|
+
private void DoRegionCapture()
|
|
1010
|
+
{
|
|
1011
|
+
if (!ready) return;
|
|
1012
|
+
|
|
1013
|
+
Rectangle vs = RegionCaptureForm.PhysicalVirtualScreen();
|
|
1014
|
+
Bitmap frozen = null;
|
|
1015
|
+
Bitmap crop = null;
|
|
1016
|
+
try
|
|
1017
|
+
{
|
|
1018
|
+
Hide();
|
|
1019
|
+
Application.DoEvents();
|
|
1020
|
+
Thread.Sleep(240);
|
|
1021
|
+
|
|
1022
|
+
List<CaptureTarget> targets = RegionCaptureForm.CollectTargets();
|
|
1023
|
+
frozen = RegionCaptureForm.GrabDesktop(vs);
|
|
1024
|
+
bool picked = false;
|
|
1025
|
+
Rectangle local = Rectangle.Empty;
|
|
1026
|
+
using (RegionCaptureForm picker = new RegionCaptureForm(frozen, vs, targets))
|
|
1027
|
+
{
|
|
1028
|
+
if (picker.ShowDialog() == DialogResult.OK)
|
|
1029
|
+
{
|
|
1030
|
+
Rectangle r = picker.SelectedRect;
|
|
1031
|
+
local = new Rectangle(r.X - vs.X, r.Y - vs.Y, r.Width, r.Height);
|
|
1032
|
+
picked = local.Width > 1 && local.Height > 1;
|
|
1033
|
+
if (picked)
|
|
1034
|
+
{
|
|
1035
|
+
// Clone before the picker disposes the frozen backdrop.
|
|
1036
|
+
crop = frozen.Clone(local, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
frozen = null; // owned and disposed by the picker
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
Log("region pick " + (picked ? (local.Width + "x" + local.Height) : "none") +
|
|
1043
|
+
" at " + local.X + "," + local.Y);
|
|
1044
|
+
if (!picked || crop == null) return;
|
|
1045
|
+
|
|
1046
|
+
byte[] bytes;
|
|
1047
|
+
using (MemoryStream ms = new MemoryStream())
|
|
1048
|
+
{
|
|
1049
|
+
crop.Save(ms, ImageFormat.Png);
|
|
1050
|
+
bytes = ms.ToArray();
|
|
1051
|
+
}
|
|
1052
|
+
string b64 = Convert.ToBase64String(bytes);
|
|
1053
|
+
if (config != null && config.CopyShot) CopyToClipboard(crop);
|
|
1054
|
+
SendToPage("{\"type\":\"shot-result\",\"dataUrl\":\"data:image/png;base64," + b64 +
|
|
1055
|
+
"\",\"width\":" + crop.Width + ",\"height\":" + crop.Height + "}");
|
|
1056
|
+
}
|
|
1057
|
+
catch (Exception ex)
|
|
1058
|
+
{
|
|
1059
|
+
SendToPage("{\"type\":\"shot-error\",\"error\":\"" + Escape(ex.Message) + "\"}");
|
|
1060
|
+
}
|
|
1061
|
+
finally
|
|
1062
|
+
{
|
|
1063
|
+
if (crop != null) crop.Dispose();
|
|
1064
|
+
Show();
|
|
1065
|
+
Activate();
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// ---------- window plumbing ----------
|
|
1070
|
+
|
|
1071
|
+
/// <summary>
|
|
1072
|
+
/// Re-derive every hard-coded pixel size from a new DPI factor. Called when the
|
|
1073
|
+
/// window is dragged to a monitor with a different scale (WM_DPICHANGED), which
|
|
1074
|
+
/// is why the sizes are kept in logical units in the first place.
|
|
1075
|
+
/// </summary>
|
|
1076
|
+
private void ApplyScale(float next)
|
|
1077
|
+
{
|
|
1078
|
+
if (next <= 0f) return;
|
|
1079
|
+
Size logical = new Size(
|
|
1080
|
+
(int)Math.Round(Width / (double)scale),
|
|
1081
|
+
(int)Math.Round(Height / (double)scale));
|
|
1082
|
+
scale = next;
|
|
1083
|
+
Padding = new Padding(Unit(5));
|
|
1084
|
+
MinimumSize = Logical(300, 340);
|
|
1085
|
+
if (titleBar != null) titleBar.Height = Unit(36);
|
|
1086
|
+
if (titleLabel != null) titleLabel.Padding = new Padding(Unit(12), 0, 0, 0);
|
|
1087
|
+
if (gapPanel != null) gapPanel.Width = Unit(6);
|
|
1088
|
+
for (int i = 0; i < iconButtons.Count && i < iconWidths.Count; i++)
|
|
1089
|
+
{
|
|
1090
|
+
iconButtons[i].Width = Unit(iconWidths[i]);
|
|
1091
|
+
}
|
|
1092
|
+
Size = Logical(logical.Width, logical.Height);
|
|
1093
|
+
Log("dpi scale " + next.ToString("0.##"));
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
protected override void WndProc(ref Message m)
|
|
1097
|
+
{
|
|
1098
|
+
if (m.Msg == Native.WM_DPICHANGED)
|
|
1099
|
+
{
|
|
1100
|
+
int dpi = (int)(m.WParam.ToInt64() & 0xFFFF);
|
|
1101
|
+
if (dpi > 0)
|
|
1102
|
+
{
|
|
1103
|
+
float next = dpi / 96f;
|
|
1104
|
+
if (Math.Abs(next - scale) > 0.01f) ApplyScale(next);
|
|
1105
|
+
}
|
|
1106
|
+
SyncChromeWithPageAsync(0);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (m.Msg == Native.WM_NCHITTEST)
|
|
1110
|
+
{
|
|
1111
|
+
int edge = Unit(7);
|
|
1112
|
+
int screenX = (short)(m.LParam.ToInt64() & 0xFFFF);
|
|
1113
|
+
int screenY = (short)((m.LParam.ToInt64() >> 16) & 0xFFFF);
|
|
1114
|
+
Point p = PointToClient(new Point(screenX, screenY));
|
|
1115
|
+
bool left = p.X <= edge;
|
|
1116
|
+
bool right = p.X >= ClientSize.Width - edge;
|
|
1117
|
+
bool top = p.Y <= edge;
|
|
1118
|
+
bool bottom = p.Y >= ClientSize.Height - edge;
|
|
1119
|
+
if (left && top) { m.Result = (IntPtr)Native.HTTOPLEFT; return; }
|
|
1120
|
+
if (right && top) { m.Result = (IntPtr)Native.HTTOPRIGHT; return; }
|
|
1121
|
+
if (left && bottom) { m.Result = (IntPtr)Native.HTBOTTOMLEFT; return; }
|
|
1122
|
+
if (right && bottom) { m.Result = (IntPtr)Native.HTBOTTOMRIGHT; return; }
|
|
1123
|
+
if (left) { m.Result = (IntPtr)Native.HTLEFT; return; }
|
|
1124
|
+
if (right) { m.Result = (IntPtr)Native.HTRIGHT; return; }
|
|
1125
|
+
if (top) { m.Result = (IntPtr)Native.HTTOP; return; }
|
|
1126
|
+
if (bottom) { m.Result = (IntPtr)Native.HTBOTTOM; return; }
|
|
1127
|
+
}
|
|
1128
|
+
base.WndProc(ref m);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
private void RestoreSavedBounds()
|
|
1132
|
+
{
|
|
1133
|
+
Rectangle wa = Native.WorkArea();
|
|
1134
|
+
int x = wa.Right - Width - Unit(16);
|
|
1135
|
+
int y = wa.Top + Unit(16);
|
|
1136
|
+
if (File.Exists(statePath))
|
|
1137
|
+
{
|
|
1138
|
+
try
|
|
1139
|
+
{
|
|
1140
|
+
string[] parts = File.ReadAllText(statePath).Split(',');
|
|
1141
|
+
if (parts.Length == 4)
|
|
1142
|
+
{
|
|
1143
|
+
int sx, sy, sw, sh;
|
|
1144
|
+
if (int.TryParse(parts[0], out sx) && int.TryParse(parts[1], out sy) &&
|
|
1145
|
+
int.TryParse(parts[2], out sw) && int.TryParse(parts[3], out sh) && sw > 200 && sh > 200)
|
|
1146
|
+
{
|
|
1147
|
+
Rectangle saved = new Rectangle(sx, sy, sw, sh);
|
|
1148
|
+
bool visible = false;
|
|
1149
|
+
foreach (Screen s in Screen.AllScreens)
|
|
1150
|
+
{
|
|
1151
|
+
if (Rectangle.Intersect(s.WorkingArea, saved).Width > 80) { visible = true; break; }
|
|
1152
|
+
}
|
|
1153
|
+
if (visible) { Location = new Point(sx, sy); Size = new Size(sw, sh); return; }
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
catch
|
|
1158
|
+
{
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
Location = new Point(x, y);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
protected override void OnFormClosing(FormClosingEventArgs e)
|
|
1165
|
+
{
|
|
1166
|
+
base.OnFormClosing(e);
|
|
1167
|
+
try
|
|
1168
|
+
{
|
|
1169
|
+
Rectangle r = WindowState == FormWindowState.Normal ? Bounds : RestoreBounds;
|
|
1170
|
+
File.WriteAllText(statePath, r.X + "," + r.Y + "," + r.Width + "," + r.Height);
|
|
1171
|
+
}
|
|
1172
|
+
catch
|
|
1173
|
+
{
|
|
1174
|
+
}
|
|
1175
|
+
if (tray != null) { tray.Visible = false; tray.Dispose(); tray = null; }
|
|
1176
|
+
if (watcher != null) { watcher.Dispose(); watcher = null; }
|
|
1177
|
+
if (pendingTimer != null) { pendingTimer.Stop(); pendingTimer.Dispose(); pendingTimer = null; }
|
|
1178
|
+
if (mutex != null)
|
|
1179
|
+
{
|
|
1180
|
+
try { mutex.ReleaseMutex(); } catch { }
|
|
1181
|
+
mutex.Dispose();
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
|
1186
|
+
{
|
|
1187
|
+
if (keyData == Keys.Escape) { Hide(); return true; }
|
|
1188
|
+
return base.ProcessCmdKey(ref msg, keyData);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
internal static class Program
|
|
1193
|
+
{
|
|
1194
|
+
[STAThread]
|
|
1195
|
+
private static void Main(string[] argv)
|
|
1196
|
+
{
|
|
1197
|
+
try { Native.SetProcessDpiAwarenessContext(new IntPtr(-4)); }
|
|
1198
|
+
catch { }
|
|
1199
|
+
|
|
1200
|
+
Application.EnableVisualStyles();
|
|
1201
|
+
Application.SetCompatibleTextRenderingDefault(false);
|
|
1202
|
+
|
|
1203
|
+
string url = null;
|
|
1204
|
+
string title = "DSH \u5C0F\u7A97";
|
|
1205
|
+
bool service = false;
|
|
1206
|
+
bool probe = false;
|
|
1207
|
+
long probeHwnd = 0;
|
|
1208
|
+
for (int i = 0; i < argv.Length; i++)
|
|
1209
|
+
{
|
|
1210
|
+
if (argv[i] == "--url" && i + 1 < argv.Length) url = argv[i + 1];
|
|
1211
|
+
else if (argv[i] == "--title" && i + 1 < argv.Length) title = argv[i + 1];
|
|
1212
|
+
else if (argv[i] == "--service") service = true;
|
|
1213
|
+
else if (argv[i] == "--probe") probe = true;
|
|
1214
|
+
else if (argv[i] == "--hwnd" && i + 1 < argv.Length)
|
|
1215
|
+
{
|
|
1216
|
+
long parsed;
|
|
1217
|
+
if (long.TryParse(argv[i + 1], out parsed)) probeHwnd = parsed;
|
|
1218
|
+
}
|
|
1219
|
+
else if (argv[i] == "--debug")
|
|
1220
|
+
{
|
|
1221
|
+
// expose the WebView2 page over CDP so we can inspect/patch the DOM from outside
|
|
1222
|
+
Environment.SetEnvironmentVariable("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS",
|
|
1223
|
+
"--remote-debugging-port=9333");
|
|
1224
|
+
Environment.SetEnvironmentVariable("DSH_HUD_DEBUG", "1");
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
if (probe)
|
|
1229
|
+
{
|
|
1230
|
+
// Headless self-test: read the selection once and report it, no window at all.
|
|
1231
|
+
SelectionHit hit = SelectionReader.ReadHit(new IntPtr(probeHwnd));
|
|
1232
|
+
string head = hit.Text ?? "";
|
|
1233
|
+
if (head.Length > 60) head = head.Substring(0, 60);
|
|
1234
|
+
SelectionWatcher.Log("probe " + (hit.Text == null ? 0 : hit.Text.Length) + " chars" +
|
|
1235
|
+
(hit.HasRect ? " rect=" + hit.Rect.ToString() : " rect=none") +
|
|
1236
|
+
" head=" + head.Replace("\n", " ").Replace("\r", " "));
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
if (service)
|
|
1240
|
+
{
|
|
1241
|
+
// One background helper: it owns the selection pill and the global hotkeys,
|
|
1242
|
+
// so "问大肥鱼" works whether or not this window has ever been opened.
|
|
1243
|
+
bool serviceCreated;
|
|
1244
|
+
Mutex serviceMutex = new Mutex(true, "Local\\DshHudService", out serviceCreated);
|
|
1245
|
+
if (!serviceCreated)
|
|
1246
|
+
{
|
|
1247
|
+
serviceMutex.Dispose();
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
Application.Run(new SelectionService(url));
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
if (string.IsNullOrEmpty(url))
|
|
1255
|
+
{
|
|
1256
|
+
MessageBox.Show("\u7F3A\u5C11 --url \u53C2\u6570\u3002", "DSH \u5C0F\u7A97",
|
|
1257
|
+
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
string dir = Path.Combine(
|
|
1262
|
+
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "dsh-hud");
|
|
1263
|
+
Directory.CreateDirectory(dir);
|
|
1264
|
+
string pending = Path.Combine(dir, "pending.txt");
|
|
1265
|
+
|
|
1266
|
+
bool created;
|
|
1267
|
+
Mutex mutex = new Mutex(true, "Local\\DshHudWindow", out created);
|
|
1268
|
+
if (!created)
|
|
1269
|
+
{
|
|
1270
|
+
try { File.WriteAllText(pending, url, Encoding.UTF8); }
|
|
1271
|
+
catch { }
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
Application.Run(new HudForm(url, title, mutex));
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|