bridgerator 1.0.8 → 1.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 +76 -14
- package/assets/bridgerator.ico +0 -0
- package/assets/icon-16.png +0 -0
- package/assets/icon-20.png +0 -0
- package/assets/icon-24.png +0 -0
- package/assets/icon-256.png +0 -0
- package/assets/icon-32.png +0 -0
- package/assets/icon-48.png +0 -0
- package/assets/icon-64.png +0 -0
- package/assets/logo.svg +1 -0
- package/install.ps1 +455 -56
- package/npm-shrinkwrap.json +8621 -0
- package/package.json +55 -49
- package/recipes/backup-folder.json +31 -0
- package/recipes/clean-downloads.json +48 -0
- package/recipes/disk-report.json +66 -0
- package/recipes/sort-folder.json +49 -0
- package/recipes/update-apps.json +25 -0
- package/src/ask-lib.ps1 +230 -0
- package/src/ask.mjs +51 -0
- package/src/ask.ps1 +17 -0
- package/src/automations.mjs +673 -0
- package/src/batch.mjs +221 -0
- package/src/bridge.mjs +720 -29
- package/src/cli.mjs +529 -74
- package/src/confirm.mjs +418 -0
- package/src/dc-config-hooks.mjs +18 -8
- package/src/dc-isolate.mjs +5 -0
- package/src/delegate.mjs +555 -0
- package/src/docs-tool.mjs +494 -0
- package/src/doctor.mjs +200 -0
- package/src/events.mjs +21 -0
- package/src/extra-tools.mjs +122 -0
- package/src/guard.mjs +34 -0
- package/src/notice.mjs +84 -23
- package/src/pair.mjs +27 -4
- package/src/pc-check.mjs +112 -0
- package/src/pc-check.ps1 +103 -0
- package/src/pc-text.mjs +94 -0
- package/src/pdf-font.mjs +245 -0
- package/src/photos-tool.mjs +187 -0
- package/src/photos.ps1 +145 -0
- package/src/plan.mjs +90 -0
- package/src/policy.mjs +293 -124
- package/src/queue.mjs +194 -0
- package/src/recent.mjs +48 -0
- package/src/recipes.mjs +108 -0
- package/src/recycle.mjs +185 -0
- package/src/recycle.ps1 +110 -0
- package/src/screen-tool.mjs +185 -0
- package/src/screen.ps1 +285 -0
- package/src/search-tool.mjs +189 -0
- package/src/search.ps1 +129 -0
- package/src/settings-tool.mjs +186 -0
- package/src/setup-tool.mjs +129 -0
- package/src/software-tool.mjs +185 -0
- package/src/telemetry.mjs +86 -0
- package/src/tool-lib.mjs +177 -0
- package/src/tool-text.mjs +45 -0
- package/src/tray.mjs +97 -0
- package/src/tray.ps1 +359 -0
- package/src/undo.mjs +313 -0
- package/src/win.mjs +17 -0
- package/ui/app.css +388 -0
- package/ui/app.js +868 -0
- package/ui/i18n/en.json +598 -0
- package/ui/i18n/ru.json +598 -0
- package/ui/index.html +26 -0
- package/vendor/webview2/LICENSE.txt +27 -0
- package/vendor/webview2/Microsoft.Web.WebView2.Core.dll +0 -0
- package/vendor/webview2/Microsoft.Web.WebView2.WinForms.dll +0 -0
- package/vendor/webview2/NOTICE.txt +85 -0
- package/vendor/webview2/SHA256SUMS.txt +4 -0
- package/vendor/webview2/SOURCE.txt +1 -0
- package/vendor/webview2/arm64/WebView2Loader.dll +0 -0
- package/vendor/webview2/x64/WebView2Loader.dll +0 -0
package/src/tray.ps1
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# Bridgerator tray helper: started hidden by the bridge (one per Windows user), talks to it over
|
|
2
|
+
# stdin/stdout JSON lines, exits when the bridge process is gone. Loaded after ask-lib.ps1 (the
|
|
3
|
+
# toast / fallback window). No network, no listening socket.
|
|
4
|
+
# bridge -> helper: {t:'state', state:'online'|'offline'|'paused', name, todo, confirm:'on'|'off', theme, lang:'en'|'ru', plan:{label,url,badge}|null, awake:bool}
|
|
5
|
+
# {t:'ask', id, head, detail, attr, host, auto, secs, theme, silent, style, lang, buttons:[[key,label,kind]...]}
|
|
6
|
+
# {t:'rpc-result', id, result | error} (answers to the window) {t:'open'}
|
|
7
|
+
# helper -> bridge: {t:'ready'} {t:'answer', id, key} {t:'log', msg} {t:'power', on, ac} (keep-awake as applied)
|
|
8
|
+
# {t:'menu', action:'pause'(minutes?)|'resume'|'quit'|'confirm'(on)}
|
|
9
|
+
# {t:'rpc', id, method, params, paths} (from the window)
|
|
10
|
+
$ErrorActionPreference = 'Stop'
|
|
11
|
+
[Console]::InputEncoding = [Text.UTF8Encoding]::new($false)
|
|
12
|
+
[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
|
|
13
|
+
Add-Type -AssemblyName System.Windows.Forms, System.Drawing
|
|
14
|
+
Add-CachedType -refs System.Windows.Forms -code @'
|
|
15
|
+
using System; using System.Collections.Concurrent; using System.Threading; using System.Runtime.InteropServices;
|
|
16
|
+
public static class BrIn {
|
|
17
|
+
public static readonly ConcurrentQueue<string> Lines = new ConcurrentQueue<string>();
|
|
18
|
+
public static volatile bool Closed;
|
|
19
|
+
public static void Start() {
|
|
20
|
+
var t = new Thread(() => { try { string l; while ((l = Console.In.ReadLine()) != null) Lines.Enqueue(l); } catch {} Closed = true; });
|
|
21
|
+
t.IsBackground = true; t.Start();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
public static class BrWin {
|
|
25
|
+
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
|
|
26
|
+
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int c);
|
|
27
|
+
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
|
|
28
|
+
// GetHicon makes a handle that Icon.Dispose / FromHandle never free: destroyed here after the icon is replaced.
|
|
29
|
+
[DllImport("user32.dll")] public static extern bool DestroyIcon(IntPtr h);
|
|
30
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern bool SetWindowText(IntPtr h, string s);
|
|
31
|
+
[DllImport("user32.dll")] static extern int GetWindowLong(IntPtr h, int i);
|
|
32
|
+
[DllImport("user32.dll")] static extern int SetWindowLong(IntPtr h, int i, int v);
|
|
33
|
+
[DllImport("user32.dll")] static extern bool SetWindowPos(IntPtr h, IntPtr a, int x, int y, int w, int hh, uint f);
|
|
34
|
+
// Resizable frame, no caption (the page has its own title bar); the title stays for the taskbar.
|
|
35
|
+
public static void NoCaption(IntPtr h) {
|
|
36
|
+
int st = GetWindowLong(h, -16); st &= ~0x00C00000; st |= 0x00040000 | 0x00020000 | 0x00080000; SetWindowLong(h, -16, st);
|
|
37
|
+
SetWindowPos(h, IntPtr.Zero, 0, 0, 0, 0, 0x0027);
|
|
38
|
+
}
|
|
39
|
+
[DllImport("dwmapi.dll")] public static extern int DwmSetWindowAttribute(IntPtr h, int a, ref int v, int s);
|
|
40
|
+
// The page's title bar moves the window: the native move loop, started as if the caption was pressed.
|
|
41
|
+
[DllImport("user32.dll")] static extern bool ReleaseCapture();
|
|
42
|
+
[DllImport("user32.dll")] static extern IntPtr SendMessage(IntPtr h, int m, IntPtr w, IntPtr l);
|
|
43
|
+
public static void Drag(IntPtr h) { ReleaseCapture(); SendMessage(h, 0xA1, (IntPtr)2, IntPtr.Zero); }
|
|
44
|
+
// The window for the page's requests: a nested .GetNewClosure() handler does not see $f (it was $null → "Minimize" did nothing).
|
|
45
|
+
public static object MainForm, Frame, Post;
|
|
46
|
+
// Keep-awake: ES_CONTINUOUS (| ES_SYSTEM_REQUIRED) on the UI thread, which lives as long as the helper.
|
|
47
|
+
[DllImport("kernel32.dll")] static extern uint SetThreadExecutionState(uint f);
|
|
48
|
+
public static void Awake(bool on) { SetThreadExecutionState(on ? 0x80000001u : 0x80000000u); }
|
|
49
|
+
}
|
|
50
|
+
// Without a caption Windows still draws the top resize border as a dark / light strip above the page:
|
|
51
|
+
// WM_NCCALCSIZE keeps the client top where the window top is (sides and bottom still resize).
|
|
52
|
+
public class BrFrame : System.Windows.Forms.NativeWindow {
|
|
53
|
+
[DllImport("user32.dll")] static extern bool IsZoomed(IntPtr h);
|
|
54
|
+
[DllImport("user32.dll")] static extern int GetSystemMetrics(int i);
|
|
55
|
+
public BrFrame(IntPtr h) { AssignHandle(h); }
|
|
56
|
+
protected override void WndProc(ref System.Windows.Forms.Message m) {
|
|
57
|
+
if (m.Msg == 0x83 && m.WParam != IntPtr.Zero) {
|
|
58
|
+
int top = Marshal.ReadInt32(m.LParam, 4);
|
|
59
|
+
base.WndProc(ref m);
|
|
60
|
+
Marshal.WriteInt32(m.LParam, 4, top + (IsZoomed(m.HWnd) ? GetSystemMetrics(33) + GetSystemMetrics(92) : 0));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
base.WndProc(ref m);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
'@
|
|
67
|
+
[void][BrWin]::SetProcessDPIAware()
|
|
68
|
+
[Windows.Forms.Application]::EnableVisualStyles()
|
|
69
|
+
Initialize-Windows
|
|
70
|
+
$parent = [int]$env:BR_PARENT
|
|
71
|
+
# The account page of the site this PC is linked to (BR_ACCOUNT from tray.mjs: staging opens staging), else the main site.
|
|
72
|
+
$account = if ($env:BR_ACCOUNT -match '^https://[a-z0-9.-]+(:\d+)?/app$') { $env:BR_ACCOUNT } else { 'https://bridgerator.recondeck.com/app' }
|
|
73
|
+
$siteHost = ([Uri]$account).Host
|
|
74
|
+
$send = { param($o) [BrIO]::Send(($o | ConvertTo-Json -Compress -Depth 6)) }
|
|
75
|
+
# Links from the window / menu: only our own site (the main one, or the linked one), opened in the default browser.
|
|
76
|
+
$openUrl = { param($u) try { $x = [Uri]$u; if ($x.Scheme -eq 'https' -and ($x.Host -eq 'bridgerator.recondeck.com' -or $x.Host -eq $siteHost) -and -not $x.UserInfo) { Start-Process $x.AbsoluteUri } } catch {} }
|
|
77
|
+
$openAccount = { & $openUrl $account }
|
|
78
|
+
$g0 = [Drawing.Graphics]::FromHwnd([IntPtr]::Zero); $script:dpi = $g0.DpiX / 96.0; $g0.Dispose()
|
|
79
|
+
if ($env:BR_DPI) { $script:dpi = [double]$env:BR_DPI } # test harness: simulate scaling
|
|
80
|
+
|
|
81
|
+
# --- icon: the logo with a status dot (and a plan badge) ------------------------------------------
|
|
82
|
+
function New-TrayIcon([string]$state, [string]$badge) {
|
|
83
|
+
# Drawn at the tray's own icon size (16 at 100 %, 20 / 24 at 125 / 150 %): crisp, no downscaling.
|
|
84
|
+
$n = [Math]::Max(16, [Windows.Forms.SystemInformation]::SmallIconSize.Width)
|
|
85
|
+
$bm = New-LogoBitmap $n
|
|
86
|
+
$g = [Drawing.Graphics]::FromImage($bm); $g.SmoothingMode = 'AntiAlias'
|
|
87
|
+
$ring = [Drawing.ColorTranslator]::FromHtml('#0c0d0f')
|
|
88
|
+
$d = [Math]::Max(4, [Math]::Round($n * 0.34)); $rw = 1.5
|
|
89
|
+
$dot = { param($hex, $cx, $cy)
|
|
90
|
+
$g.FillEllipse((New-Object Drawing.SolidBrush $ring), ($cx - $d / 2 - $rw), ($cy - $d / 2 - $rw), ($d + 2 * $rw), ($d + 2 * $rw))
|
|
91
|
+
$g.FillEllipse((New-Object Drawing.SolidBrush ([Drawing.ColorTranslator]::FromHtml($hex))), ($cx - $d / 2), ($cy - $d / 2), $d, $d) }
|
|
92
|
+
# Status dot bottom-right with a 1.5 px ring (covers the logo's cyan dot, so they never clash).
|
|
93
|
+
& $dot $(switch ($state) { 'online' { '#32d74b' } 'paused' { '#ffb900' } default { '#8e9098' } }) ($n - $d / 2 - 0.5) ($n - $d / 2 - 0.5)
|
|
94
|
+
if ($badge) { & $dot $(if ($badge -eq 'red') { '#ff453a' } else { '#ffd60a' }) ($n - $d / 2 - 0.5) ($d / 2 + 0.5) }
|
|
95
|
+
$g.Dispose()
|
|
96
|
+
$h = $bm.GetHicon(); $bm.Dispose()
|
|
97
|
+
return @{ icon = [Drawing.Icon]::FromHandle($h); handle = $h }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# --- tray menu (as in the mockup: status line, open, queue, confirm, pause, account, help, quit) ----
|
|
101
|
+
# Labels in the PC's language (config.json language, sent with every state; V-3).
|
|
102
|
+
$TXT = @{
|
|
103
|
+
en = @{ open = 'Open Bridgerator'; queue = 'Task queue'; queueN = 'Task queue — {0} to do'; confirm = 'Confirm on this PC'; on = 'On'; off = 'Off'; pause = 'Pause'; p15 = 'For 15 minutes'; p60 = 'For 1 hour'; pInf = 'Until I resume'; resume = 'Resume'; account = 'Open account ↗'; help = 'Help'; quit = 'Quit Bridgerator'; online = 'Online'; paused = 'Paused'; offline = 'Offline'; renew = 'Renew Pro…'; upgrade = 'Upgrade to Pro…' }
|
|
104
|
+
ru = @{ open = 'Открыть Bridgerator'; queue = 'Очередь задач'; queueN = 'Очередь задач — {0} к выполнению'; confirm = 'Подтверждения на этом ПК'; on = 'Вкл.'; off = 'Выкл.'; pause = 'Пауза'; p15 = 'На 15 минут'; p60 = 'На 1 час'; pInf = 'Пока не возобновлю'; resume = 'Возобновить'; account = 'Открыть кабинет ↗'; help = 'Справка'; quit = 'Выйти из Bridgerator'; online = 'В сети'; paused = 'Пауза'; offline = 'Не в сети'; renew = 'Продлить Pro…'; upgrade = 'Перейти на Pro…' }
|
|
105
|
+
}
|
|
106
|
+
$S = @{ state = 'offline'; name = $env:COMPUTERNAME; todo = 0; confirm = 'off'; theme = 'system'; lang = 'en'; plan = $null }
|
|
107
|
+
function L([string]$k) { $t = $TXT[$(if ($S.lang -eq 'ru') { 'ru' } else { 'en' })]; [string]$t[$k] }
|
|
108
|
+
$ni = New-Object Windows.Forms.NotifyIcon
|
|
109
|
+
$script:iconKey = 'offline|'; $ti = New-TrayIcon 'offline' $null; $script:iconH = $ti.handle; $ni.Icon = $ti.icon; $ni.Text = 'Bridgerator'; $ni.Visible = $true
|
|
110
|
+
$menu = New-Object Windows.Forms.ContextMenuStrip
|
|
111
|
+
$menu.ShowImageMargin = $true; $menu.Font = New-Object Drawing.Font('Segoe UI', 9)
|
|
112
|
+
$script:dotImgs = @{}
|
|
113
|
+
function Get-Dot([string]$hex) { if (-not $script:dotImgs[$hex]) { $script:dotImgs[$hex] = New-Dot $hex }; $script:dotImgs[$hex] }
|
|
114
|
+
function New-Dot([string]$hex) { $b = New-Object Drawing.Bitmap 16, 16; $g = [Drawing.Graphics]::FromImage($b); $g.SmoothingMode = 'AntiAlias'; $g.FillEllipse((New-Object Drawing.SolidBrush ([Drawing.ColorTranslator]::FromHtml($hex))), 4, 4, 8, 8); $g.Dispose(); $b }
|
|
115
|
+
function Add-Item([string]$text, $onClick) { $i = New-Object Windows.Forms.ToolStripMenuItem $text; if ($onClick) { $i.Add_Click($onClick) }; [void]$menu.Items.Add($i); $i }
|
|
116
|
+
$miPlan = Add-Item 'Renew Pro…' { if ($S.plan) { & $openUrl $S.plan.url } }
|
|
117
|
+
$sepPlan = New-Object Windows.Forms.ToolStripSeparator; [void]$menu.Items.Add($sepPlan)
|
|
118
|
+
$miHead = Add-Item 'Offline' $null; $miHead.Enabled = $false
|
|
119
|
+
[void]$menu.Items.Add((New-Object Windows.Forms.ToolStripSeparator))
|
|
120
|
+
$miOpen = Add-Item 'Open Bridgerator' { Open-Window 'home' }; $miOpen.Font = New-Object Drawing.Font('Segoe UI', 9, [Drawing.FontStyle]::Bold)
|
|
121
|
+
$miQueue = Add-Item 'Task queue' { Open-Window 'queue' }
|
|
122
|
+
$miConfirm = Add-Item 'Confirm on this PC' $null
|
|
123
|
+
$miConfOn = New-Object Windows.Forms.ToolStripMenuItem 'On'; $miConfOn.Add_Click({ & $send @{ t = 'menu'; action = 'confirm'; on = $true } })
|
|
124
|
+
$miConfOff = New-Object Windows.Forms.ToolStripMenuItem 'Off'; $miConfOff.Add_Click({ & $send @{ t = 'menu'; action = 'confirm'; on = $false } })
|
|
125
|
+
[void]$miConfirm.DropDownItems.AddRange(@($miConfOn, $miConfOff))
|
|
126
|
+
$miPause = Add-Item 'Pause' $null
|
|
127
|
+
$p15 = New-Object Windows.Forms.ToolStripMenuItem 'For 15 minutes'; $p15.Add_Click({ & $send @{ t = 'menu'; action = 'pause'; minutes = 15 } })
|
|
128
|
+
$p60 = New-Object Windows.Forms.ToolStripMenuItem 'For 1 hour'; $p60.Add_Click({ & $send @{ t = 'menu'; action = 'pause'; minutes = 60 } })
|
|
129
|
+
$pInf = New-Object Windows.Forms.ToolStripMenuItem 'Until I resume'; $pInf.Add_Click({ & $send @{ t = 'menu'; action = 'pause' } })
|
|
130
|
+
[void]$miPause.DropDownItems.AddRange(@($p15, $p60, $pInf))
|
|
131
|
+
$miResume = Add-Item 'Resume' { & $send @{ t = 'menu'; action = 'resume' } }
|
|
132
|
+
[void]$menu.Items.Add((New-Object Windows.Forms.ToolStripSeparator))
|
|
133
|
+
$miAcc = Add-Item 'Open account ↗' $openAccount
|
|
134
|
+
$miHelp = Add-Item 'Help' { Open-Window 'help' }
|
|
135
|
+
[void]$menu.Items.Add((New-Object Windows.Forms.ToolStripSeparator))
|
|
136
|
+
$miQuit = Add-Item 'Quit Bridgerator' { & $send @{ t = 'menu'; action = 'quit' } }
|
|
137
|
+
$ni.ContextMenuStrip = $menu
|
|
138
|
+
# Every exit path (parent gone, stop, error) hides + disposes the icon, or a ghost stays in the tray until the mouse passes it.
|
|
139
|
+
# The window's WebView2 is disposed too: its msedgewebview2 processes end with it (V-8: an update left 12 behind).
|
|
140
|
+
function Remove-Tray {
|
|
141
|
+
try { $ni.Visible = $false; $ni.Dispose() } catch {}; if ($script:iconH) { [void][BrWin]::DestroyIcon($script:iconH); $script:iconH = $null }
|
|
142
|
+
try { if ($script:wv) { $script:wv.Dispose(); $script:wv = $null } } catch {}
|
|
143
|
+
try { if ($script:win -and -not $script:win.IsDisposed) { $W.forceClose = $true; $script:win.Close(); $script:win.Dispose() } } catch {}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# The bridge says when ChatGPT is working (awake); the PC is held awake only on mains power (a desktop
|
|
147
|
+
# reports Online or Unknown, never Offline). Display may still turn off; a sleep the person starts still works.
|
|
148
|
+
$script:awakeOn = $false; $script:acOn = $true
|
|
149
|
+
function Set-Awake($want) {
|
|
150
|
+
$ac = $true; try { $ac = [Windows.Forms.SystemInformation]::PowerStatus.PowerLineStatus -ne 'Offline' } catch {}
|
|
151
|
+
$on = [bool]$want -and $ac
|
|
152
|
+
if ($on -ne $script:awakeOn -or $ac -ne $script:acOn) {
|
|
153
|
+
[BrWin]::Awake($on); $script:awakeOn = $on; $script:acOn = $ac
|
|
154
|
+
& $send @{ t = 'power'; on = $on; ac = $ac }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function Set-TrayState($m) {
|
|
158
|
+
foreach ($k in 'state', 'name', 'todo', 'confirm', 'theme', 'lang') { if ($null -ne $m.$k -and "$($m.$k)" -ne '') { $S[$k] = $m.$k } }
|
|
159
|
+
if ($null -ne $m.awake) { try { Set-Awake $m.awake } catch { & $send @{ t = 'log'; msg = "keep awake: $_" } } }
|
|
160
|
+
$S.plan = $m.plan
|
|
161
|
+
$badge = if ($S.plan) { [string]$S.plan.badge } else { $null }
|
|
162
|
+
# Redrawn only when the state / badge changed (the bridge pushes the state every 5 s); the old HICON is destroyed.
|
|
163
|
+
if ($script:iconKey -ne "$($S.state)|$badge") {
|
|
164
|
+
$ti = New-TrayIcon $S.state $badge; $old = $ni.Icon; $oldH = $script:iconH
|
|
165
|
+
$ni.Icon = $ti.icon; $script:iconH = $ti.handle; $script:iconKey = "$($S.state)|$badge"
|
|
166
|
+
if ($old) { $old.Dispose() }; if ($oldH) { [void][BrWin]::DestroyIcon($oldH) }
|
|
167
|
+
}
|
|
168
|
+
$word = switch ($S.state) { 'online' { L 'online' } 'paused' { L 'paused' } default { L 'offline' } }
|
|
169
|
+
$t = "Bridgerator — $($word.ToLower()) ($($S.name))"; if ($t.Length -gt 63) { $t = $t.Substring(0, 63) }
|
|
170
|
+
if ($ni.Text -ne $t) { $ni.Text = $t }
|
|
171
|
+
$miHead.Text = "$word · $($S.name)"
|
|
172
|
+
$miHead.Image = Get-Dot $(switch ($S.state) { 'online' { '#32d74b' } 'paused' { '#ffb900' } default { '#8e9098' } })
|
|
173
|
+
$miQueue.Text = if ([int]$S.todo -gt 0) { (L 'queueN') -f $S.todo } else { L 'queue' }
|
|
174
|
+
$miConfirm.Text = "$(L 'confirm') — $(if ($S.confirm -eq 'on') { L 'on' } else { L 'off' })"
|
|
175
|
+
$miOpen.Text = L 'open'; $miConfOn.Text = L 'on'; $miConfOff.Text = L 'off'; $miPause.Text = L 'pause'; $p15.Text = L 'p15'; $p60.Text = L 'p60'; $pInf.Text = L 'pInf'
|
|
176
|
+
$miResume.Text = L 'resume'; $miAcc.Text = L 'account'; $miHelp.Text = L 'help'; $miQuit.Text = L 'quit'
|
|
177
|
+
$miConfOn.Checked = $S.confirm -eq 'on'; $miConfOff.Checked = $S.confirm -ne 'on'
|
|
178
|
+
$miPause.Visible = $S.state -ne 'paused'; $miResume.Visible = $S.state -eq 'paused'
|
|
179
|
+
$miPlan.Visible = [bool]$S.plan; $sepPlan.Visible = [bool]$S.plan
|
|
180
|
+
if ($S.plan) { $miPlan.Text = $(switch ([string]$S.plan.label) { 'Renew Pro…' { L 'renew' } 'Upgrade to Pro…' { L 'upgrade' } default { [string]$S.plan.label } }); $miPlan.Image = Get-Dot $(if ($badge -eq 'red') { '#ff453a' } else { '#ffd60a' }) }
|
|
181
|
+
}
|
|
182
|
+
Set-TrayState @{ state = 'offline' }
|
|
183
|
+
|
|
184
|
+
# --- the window (WebView2): 1040 x 900 (every page without scrollbars, owner 21.10; never larger than the screen), min 760 x 520 ---
|
|
185
|
+
# Created once, hidden, soon after the helper starts (WebView2 warm), shown only after the page has
|
|
186
|
+
# loaded; closing hides it — reopening is instant and never flashes.
|
|
187
|
+
$W = @{ win = $null; wv = $null; ready = $false; want = $null; shown = $false; t0 = $null; forceClose = $false }
|
|
188
|
+
$script:win = $null; $script:wv = $null
|
|
189
|
+
function Show-Win([string]$page) {
|
|
190
|
+
$f = $W.win
|
|
191
|
+
if ($W.wv.CoreWebView2) { $W.wv.CoreWebView2.PostWebMessageAsJson((@{ t = 'nav'; page = $page } | ConvertTo-Json -Compress)) }
|
|
192
|
+
$f.Show(); if ($f.WindowState -eq 'Minimized') { $f.WindowState = 'Normal' }
|
|
193
|
+
if (-not $W.shown) {
|
|
194
|
+
$W.shown = $true; [BrWin]::NoCaption($f.Handle)
|
|
195
|
+
# Physical pixels (the process is DPI-aware): never larger than the work area of the monitor, title bar always on screen.
|
|
196
|
+
$wa = [Windows.Forms.Screen]::FromHandle($f.Handle).WorkingArea
|
|
197
|
+
$f.MinimumSize = New-Object Drawing.Size([Math]::Min($f.MinimumSize.Width, $wa.Width), [Math]::Min($f.MinimumSize.Height, $wa.Height))
|
|
198
|
+
$f.ClientSize = New-Object Drawing.Size([int](1040 * $script:dpi), [int](900 * $script:dpi))
|
|
199
|
+
$f.Size = New-Object Drawing.Size([Math]::Min($f.Width, $wa.Width), [Math]::Min($f.Height, $wa.Height))
|
|
200
|
+
$f.Location = New-Object Drawing.Point(($wa.Left + [int](($wa.Width - $f.Width) / 2)), ($wa.Top + [int](($wa.Height - $f.Height) / 2)))
|
|
201
|
+
}
|
|
202
|
+
[void][BrWin]::ShowWindow($f.Handle, 5); [void][BrWin]::SetForegroundWindow($f.Handle); $f.Activate()
|
|
203
|
+
if ($W.t0) { & $send @{ t = 'log'; msg = "tray window shown in $([int]((Get-Date) - $W.t0).TotalMilliseconds) ms" }; $W.t0 = $null }
|
|
204
|
+
}
|
|
205
|
+
function Get-Dark { $th = [string]$S.theme; if ($th -eq 'dark') { return $true }; if ($th -eq 'light') { return $false }; try { return (Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize' -ErrorAction Stop).AppsUseLightTheme -eq 0 } catch { return $true } }
|
|
206
|
+
function Open-Window([string]$page = 'home', [switch]$Prewarm) {
|
|
207
|
+
if (-not $Prewarm) { $W.t0 = Get-Date }
|
|
208
|
+
$ui = if ($env:BR_UI_DIR) { $env:BR_UI_DIR } else { Join-Path $env:BR_PKG 'ui' }
|
|
209
|
+
$vendor = Join-Path $env:BR_PKG 'vendor\webview2'
|
|
210
|
+
if (-not (Test-Path (Join-Path $ui 'index.html'))) { if (-not $Prewarm) { & $openAccount }; return }
|
|
211
|
+
if ($W.win -and -not $W.win.IsDisposed) {
|
|
212
|
+
if ($Prewarm) { return }
|
|
213
|
+
if ($W.ready) { Show-Win $page } else { $W.want = $page }
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
$arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'x64' }
|
|
218
|
+
$env:PATH = (Join-Path $vendor $arch) + ';' + $env:PATH # WebView2Loader.dll
|
|
219
|
+
Add-Type -Path (Join-Path $vendor 'Microsoft.Web.WebView2.Core.dll'), (Join-Path $vendor 'Microsoft.Web.WebView2.WinForms.dll')
|
|
220
|
+
[void][Microsoft.Web.WebView2.Core.CoreWebView2Environment]::GetAvailableBrowserVersionString()
|
|
221
|
+
} catch { & $send @{ t = 'log'; msg = "tray: WebView2 not available ($_)$(if ($Prewarm) { ' - prewarm skipped, tried again when the window is opened' } else { ', opening the account page' })" }; if (-not $Prewarm) { & $openAccount }; return }
|
|
222
|
+
$dark = Get-Dark
|
|
223
|
+
$bg = [Drawing.ColorTranslator]::FromHtml($(if ($dark) { '#141518' } else { '#f6f7f9' }))
|
|
224
|
+
$k = $script:dpi
|
|
225
|
+
$f = New-Object Windows.Forms.Form
|
|
226
|
+
# Sizable frame without a caption: the page draws its own title bar (drag region via app-region).
|
|
227
|
+
$f.FormBorderStyle = 'Sizable'; $f.ControlBox = $false; $f.Text = ''; $f.ShowIcon = $true
|
|
228
|
+
$f.Icon = New-Object Drawing.Icon (Join-Path $env:BR_PKG 'assets\bridgerator.ico'); $f.StartPosition = 'CenterScreen'; $f.AutoScaleMode = 'None'; $f.BackColor = $bg
|
|
229
|
+
$f.ClientSize = New-Object Drawing.Size([int](1040 * $k), [int](900 * $k))
|
|
230
|
+
$f.MinimumSize = New-Object Drawing.Size([int](760 * $k), [int](520 * $k))
|
|
231
|
+
$wv = New-Object Microsoft.Web.WebView2.WinForms.WebView2; $wv.Dock = 'Fill'; $wv.DefaultBackgroundColor = $bg
|
|
232
|
+
$cp = New-Object Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties
|
|
233
|
+
$cp.UserDataFolder = Join-Path $env:LOCALAPPDATA 'Bridgerator\webview'; $wv.CreationProperties = $cp
|
|
234
|
+
if ($env:BR_ZOOM) { $wv.ZoomFactor = [double]$env:BR_ZOOM }
|
|
235
|
+
$f.Controls.Add($wv)
|
|
236
|
+
$post = { param($o) if ($wv.CoreWebView2) { $wv.CoreWebView2.PostWebMessageAsJson(($o | ConvertTo-Json -Compress -Depth 4)) } }.GetNewClosure()
|
|
237
|
+
[BrWin]::Post = $post
|
|
238
|
+
$wv.Add_CoreWebView2InitializationCompleted({
|
|
239
|
+
param($s, $e)
|
|
240
|
+
if (-not $e.IsSuccess) {
|
|
241
|
+
# Close() is a UserClosing (hidden by the handler below): force it (the flag lives in $W: each .GetNewClosure() handler has its own $script: scope); only an explicit click falls back to the browser.
|
|
242
|
+
$wanted = [bool]$W.want; $W.forceClose = $true; try { $f.Close() } finally { $W.forceClose = $false }
|
|
243
|
+
if ($wanted) { & $openAccount }
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
$c = $wv.CoreWebView2
|
|
247
|
+
$c.Settings.AreDevToolsEnabled = [bool]$env:BR_DEVTOOLS; $c.Settings.IsStatusBarEnabled = $false; $c.Settings.AreDefaultContextMenusEnabled = [bool]$env:BR_DEVTOOLS
|
|
248
|
+
$c.Settings.IsZoomControlEnabled = $false
|
|
249
|
+
$c.SetVirtualHostNameToFolderMapping('bridgerator.local', $ui, 'Deny')
|
|
250
|
+
# Only our own page: anything else opens in the default browser (our site only) or is refused.
|
|
251
|
+
$c.Add_NavigationStarting({ param($s2, $n) if ($n.Uri -notlike 'https://bridgerator.local/*') { $n.Cancel = $true; & $openUrl $n.Uri } }.GetNewClosure())
|
|
252
|
+
$c.Add_NewWindowRequested({ param($s2, $n) $n.Handled = $true; & $openUrl $n.Uri }.GetNewClosure())
|
|
253
|
+
$c.Add_WebMessageReceived({
|
|
254
|
+
param($s2, $m)
|
|
255
|
+
try {
|
|
256
|
+
# Only our own page may talk to the helper.
|
|
257
|
+
if ([string]$m.Source -notlike 'https://bridgerator.local/*') { return }
|
|
258
|
+
$f = [BrWin]::MainForm; $post = [BrWin]::Post
|
|
259
|
+
$msg = $m.WebMessageAsJson | ConvertFrom-Json
|
|
260
|
+
$paths = @(); try { foreach ($o in $m.AdditionalObjects) { if ($o.Path) { $paths += [string]$o.Path } } } catch {}
|
|
261
|
+
switch ($msg.method) {
|
|
262
|
+
'openUrl' { & $openUrl ([string]$msg.params.url); & $post @{ id = $msg.id; result = $true } }
|
|
263
|
+
'minimize' { $f.WindowState = 'Minimized'; & $post @{ id = $msg.id; result = $true } }
|
|
264
|
+
'drag' { [BrWin]::Drag($f.Handle); & $post @{ id = $msg.id; result = $true } }
|
|
265
|
+
'hide' { $f.Hide(); & $post @{ id = $msg.id; result = $true } }
|
|
266
|
+
'droppedPaths' { & $post @{ id = $msg.id; result = @($paths) } }
|
|
267
|
+
'pickFolder' {
|
|
268
|
+
$d = New-Object Windows.Forms.FolderBrowserDialog
|
|
269
|
+
$r = if ($d.ShowDialog($f) -eq 'OK') { $d.SelectedPath } else { $null }
|
|
270
|
+
& $post @{ id = $msg.id; result = $r }
|
|
271
|
+
}
|
|
272
|
+
'pickFiles' {
|
|
273
|
+
$d = New-Object Windows.Forms.OpenFileDialog; $d.Multiselect = $true
|
|
274
|
+
$r = if ($d.ShowDialog($f) -eq 'OK') { @($d.FileNames) } else { @() }
|
|
275
|
+
& $post @{ id = $msg.id; result = $r }
|
|
276
|
+
}
|
|
277
|
+
default { & $send @{ t = 'rpc'; id = [string]$msg.id; method = [string]$msg.method; params = $msg.params; paths = $paths } }
|
|
278
|
+
}
|
|
279
|
+
} catch { & $send @{ t = 'log'; msg = "tray window: $_" } }
|
|
280
|
+
}.GetNewClosure())
|
|
281
|
+
# First page loaded: only now the window may appear (no white / black flash).
|
|
282
|
+
$c.Add_NavigationCompleted({ if (-not $W.ready) { $W.ready = $true; if ($W.want) { $p0 = $W.want; $W.want = $null; Show-Win $p0 } } }.GetNewClosure())
|
|
283
|
+
$c.Navigate("https://bridgerator.local/index.html#$page")
|
|
284
|
+
}.GetNewClosure())
|
|
285
|
+
$f.Add_HandleCreated({ [void][BrWin]::SetWindowText($f.Handle, 'Bridgerator'); [BrWin]::MainForm = $f; [BrWin]::Frame = New-Object BrFrame $f.Handle; [BrWin]::NoCaption($f.Handle); if ($dark) { $v = 1; [void][BrWin]::DwmSetWindowAttribute($f.Handle, 20, [ref]$v, 4) } }.GetNewClosure())
|
|
286
|
+
|
|
287
|
+
# Closing hides to the tray (the X in the page, Alt+F4); Quit is in the tray menu.
|
|
288
|
+
$f.Add_FormClosing({ param($s, $e) if ($e.CloseReason -eq 'UserClosing' -and -not $W.forceClose) { $e.Cancel = $true; $f.Hide() } }.GetNewClosure())
|
|
289
|
+
$f.Add_FormClosed({ $W.win = $null; $W.wv = $null; $W.ready = $false; $W.shown = $false; $script:win = $null; $script:wv = $null })
|
|
290
|
+
$W.win = $f; $W.wv = $wv; $W.ready = $false; $W.shown = $false; $W.want = $(if ($Prewarm) { $null } else { $page })
|
|
291
|
+
$script:win = $f; $script:wv = $wv
|
|
292
|
+
# Handles without showing; WebView2 starts loading the page in the background.
|
|
293
|
+
[void]$f.Handle; [void]$wv.Handle
|
|
294
|
+
[void]$wv.EnsureCoreWebView2Async($null)
|
|
295
|
+
}
|
|
296
|
+
$ni.Add_DoubleClick({ try { Open-Window 'home' } catch { & $send @{ t = 'log'; msg = "tray: $_" } } })
|
|
297
|
+
|
|
298
|
+
# --- questions (toast; fallback window when our notifications are off) ---------------------------
|
|
299
|
+
$pending = @{} # id -> @{ w = BrWait; tag; deadline }
|
|
300
|
+
# popupStyle = toast but the own window appeared (V-4): the reason goes to bridge.log once per reason
|
|
301
|
+
# (notifications for Bridgerator / all apps off = Setting not Enabled, the toast failed with its HRESULT, ...).
|
|
302
|
+
$script:fallbackWhy = @{}
|
|
303
|
+
function Write-FallbackWhy([string]$why) { if (-not $script:fallbackWhy[$why]) { $script:fallbackWhy[$why] = $true; & $send @{ t = 'log'; msg = "question shown in the Bridgerator window instead of a Windows notification: $why" } } }
|
|
304
|
+
function Start-Ask($m) {
|
|
305
|
+
$specs = @(); foreach ($b in $m.buttons) { $specs += ,@([string]$b[0], [string]$b[1], [string]$b[2]) }
|
|
306
|
+
$q = @{ specs = $specs; head = [string]$m.head; detail = [string]$m.detail; attr = [string]$m.attr; host = [string]$m.host; auto = [string]$m.auto; secs = [int]$m.secs; tag = "t$([string]$m.id)"; theme = [string]$m.theme; icon = (Join-Path $env:BR_PKG 'assets\icon-64.png'); silent = [bool]$m.silent; lang = [string]$m.lang }
|
|
307
|
+
if ($q.tag.Length -gt 60) { $q.tag = $q.tag.Substring(0, 60) }
|
|
308
|
+
$w = New-Object BrWait; $w.Id = [string]$m.id; $w.Report = $true; $w.Keys = [string[]]@($specs | ForEach-Object { $_[0] })
|
|
309
|
+
try {
|
|
310
|
+
if ($m.style -eq 'popup') { throw 'compact pop-up chosen' }
|
|
311
|
+
Show-AskToast $q $w
|
|
312
|
+
$pending[$w.Id] = @{ w = $w; q = $q; deadline = (Get-Date).AddSeconds($q.secs) }
|
|
313
|
+
} catch { if ("$_" -ne 'compact pop-up chosen') { Write-FallbackWhy "$_" }; Show-AskWindow $q $w.Id $w.Keys }
|
|
314
|
+
}
|
|
315
|
+
# Our notifications are off (or the toast failed): the small window, answer reported when it closes.
|
|
316
|
+
function Show-AskWindow($q, $id, $keys) {
|
|
317
|
+
$w = New-Object BrWait; $w.Id = $id; $w.Report = $true; $w.Keys = $keys
|
|
318
|
+
try {
|
|
319
|
+
$f = New-AskForm $q
|
|
320
|
+
$f.Add_FormClosed({ [void]$w.Finish($f.Tag.pick) }.GetNewClosure())
|
|
321
|
+
$f.Show(); [void][BrWin]::ShowWindow($f.Handle, 4) # SW_SHOWNOACTIVATE: no focus stealing
|
|
322
|
+
} catch { [void]$w.Finish('deny') }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# --- main loop on the UI thread ----------------------------------------------------------------
|
|
326
|
+
[BrIn]::Start()
|
|
327
|
+
& $send @{ t = 'ready' }
|
|
328
|
+
$warm = New-Object Windows.Forms.Timer; $warm.Interval = 200
|
|
329
|
+
$warm.Add_Tick({ $warm.Stop(); try { Open-Window 'home' -Prewarm } catch { & $send @{ t = 'log'; msg = "tray prewarm: $_" } } })
|
|
330
|
+
$warm.Start()
|
|
331
|
+
$script:lastParentCheck = Get-Date
|
|
332
|
+
$tm = New-Object Windows.Forms.Timer; $tm.Interval = 100
|
|
333
|
+
$tm.Add_Tick({
|
|
334
|
+
$line = $null
|
|
335
|
+
while ([BrIn]::Lines.TryDequeue([ref]$line)) {
|
|
336
|
+
try {
|
|
337
|
+
$m = $line | ConvertFrom-Json
|
|
338
|
+
switch ($m.t) {
|
|
339
|
+
'state' { Set-TrayState $m }
|
|
340
|
+
'ask' { Start-Ask $m }
|
|
341
|
+
'rpc-result' { if ($script:wv -and $script:wv.CoreWebView2) { $script:wv.CoreWebView2.PostWebMessageAsJson($line) } }
|
|
342
|
+
'exit' { Remove-Tray; [Windows.Forms.Application]::Exit() }
|
|
343
|
+
'open' { Open-Window $(if ($m.page) { [string]$m.page } else { 'home' }) }
|
|
344
|
+
}
|
|
345
|
+
} catch { & $send @{ t = 'log'; msg = "tray: $_ $($_.InvocationInfo.PositionMessage)" } }
|
|
346
|
+
}
|
|
347
|
+
foreach ($id in @($pending.Keys)) {
|
|
348
|
+
$p = $pending[$id]
|
|
349
|
+
if ($p.w.IsOver) { $pending.Remove($id); if ($p.w.Result -eq 'failed') { Write-FallbackWhy "Windows could not show the notification ($($p.w.FailCode))"; Show-AskWindow $p.q $id $p.w.Keys }; continue }
|
|
350
|
+
if ((Get-Date) -gt $p.deadline) { Remove-AskToast $p.q.tag; [void]$p.w.Finish('deny'); $pending.Remove($id) }
|
|
351
|
+
}
|
|
352
|
+
if ([BrIn]::Closed -or ((Get-Date) - $script:lastParentCheck).TotalSeconds -ge 2) {
|
|
353
|
+
$script:lastParentCheck = Get-Date
|
|
354
|
+
$alive = $false; try { $alive = -not [BrIn]::Closed -and [bool](Get-Process -Id $parent -ErrorAction Stop) } catch {}
|
|
355
|
+
if (-not $alive) { Remove-Tray; [Windows.Forms.Application]::Exit() }
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
$tm.Start()
|
|
359
|
+
try { [Windows.Forms.Application]::Run() } finally { Remove-Tray }
|