forge-jsxy 1.0.69 → 1.0.71
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.
|
@@ -5,57 +5,219 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
6
|
<title>Forge Remote Control</title>
|
|
7
7
|
<style>
|
|
8
|
-
:root {
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
:root {
|
|
9
|
+
color-scheme: dark;
|
|
10
|
+
--vscode-editor-background: #1e1e1e;
|
|
11
|
+
--vscode-editor-foreground: #cccccc;
|
|
12
|
+
--vscode-sideBar-background: #252526;
|
|
13
|
+
--vscode-panel-border: #3e3e42;
|
|
14
|
+
--vscode-button-background: #0078d4;
|
|
15
|
+
--vscode-button-hoverBackground: #1a8cff;
|
|
16
|
+
--vscode-button-foreground: #ffffff;
|
|
17
|
+
--vscode-button-secondaryBackground: #3a3d41;
|
|
18
|
+
--vscode-button-secondaryHoverBackground: #4c4d51;
|
|
19
|
+
--vscode-button-secondaryForeground: #c5c5c5;
|
|
20
|
+
--vscode-input-background: #3c3c3c;
|
|
21
|
+
--vscode-input-foreground: #cccccc;
|
|
22
|
+
--vscode-input-border: #3c3c3c;
|
|
23
|
+
--vscode-focusBorder: #0078d4;
|
|
24
|
+
--vscode-errorForeground: #f48771;
|
|
25
|
+
}
|
|
26
|
+
* { box-sizing: border-box; }
|
|
27
|
+
html, body { height: 100%; }
|
|
28
|
+
body {
|
|
29
|
+
margin: 0;
|
|
30
|
+
font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
31
|
+
background: var(--vscode-editor-background);
|
|
32
|
+
color: var(--vscode-editor-foreground);
|
|
33
|
+
}
|
|
34
|
+
.bar {
|
|
35
|
+
position: fixed;
|
|
36
|
+
left: 0;
|
|
37
|
+
right: 0;
|
|
38
|
+
bottom: 0;
|
|
39
|
+
z-index: 10;
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-wrap: wrap;
|
|
42
|
+
gap: 6px 8px;
|
|
43
|
+
align-items: center;
|
|
44
|
+
min-height: 44px;
|
|
45
|
+
padding: 8px 12px;
|
|
46
|
+
background: #181818;
|
|
47
|
+
border-top: 1px solid var(--vscode-panel-border);
|
|
48
|
+
}
|
|
11
49
|
.bar input, .bar button { font: inherit; }
|
|
12
|
-
.bar input {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
50
|
+
.bar input {
|
|
51
|
+
background: var(--vscode-input-background);
|
|
52
|
+
color: var(--vscode-input-foreground);
|
|
53
|
+
border: 1px solid var(--vscode-input-border);
|
|
54
|
+
border-radius: 4px;
|
|
55
|
+
padding: 5px 8px;
|
|
56
|
+
}
|
|
57
|
+
.bar button {
|
|
58
|
+
background: var(--vscode-button-background);
|
|
59
|
+
color: var(--vscode-button-foreground);
|
|
60
|
+
border: 1px solid transparent;
|
|
61
|
+
border-radius: 4px;
|
|
62
|
+
min-height: 28px;
|
|
63
|
+
padding: 4px 12px;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
font-size: 12px;
|
|
66
|
+
font-weight: 500;
|
|
67
|
+
}
|
|
68
|
+
.bar button:hover:not(:disabled) { background: var(--vscode-button-hoverBackground); }
|
|
69
|
+
.bar button.alt {
|
|
70
|
+
background: var(--vscode-button-secondaryBackground);
|
|
71
|
+
color: var(--vscode-button-secondaryForeground);
|
|
72
|
+
border-color: #505050;
|
|
73
|
+
}
|
|
74
|
+
.bar button.alt:hover:not(:disabled) { background: var(--vscode-button-secondaryHoverBackground); }
|
|
75
|
+
.bar button.warn {
|
|
76
|
+
background: #5a1d1d;
|
|
77
|
+
color: #ffd9d9;
|
|
78
|
+
border-color: #7c2b2b;
|
|
79
|
+
}
|
|
80
|
+
.bar button.warn:hover:not(:disabled) { background: #6c2626; }
|
|
81
|
+
.bar button:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
82
|
+
.bar button:focus-visible, .bar input:focus-visible {
|
|
83
|
+
outline: none;
|
|
84
|
+
box-shadow: 0 0 0 1px var(--vscode-editor-background), 0 0 0 3px rgba(0, 120, 212, 0.45);
|
|
85
|
+
}
|
|
86
|
+
.spacer { flex: 1; min-width: 8px; }
|
|
87
|
+
.state { opacity: .92; font-size: 11px; color: #b7b7b7; }
|
|
88
|
+
.brand { font-weight: 600; letter-spacing: 0.02em; }
|
|
89
|
+
.hint { color: #9d9d9d; }
|
|
90
|
+
.screen-wrap {
|
|
91
|
+
position: fixed;
|
|
92
|
+
inset: 0 0 58px 0;
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
background: #111;
|
|
95
|
+
display: grid;
|
|
96
|
+
place-items: center;
|
|
97
|
+
}
|
|
98
|
+
.screen-stage {
|
|
99
|
+
width: 100%;
|
|
100
|
+
height: 100%;
|
|
101
|
+
display: grid;
|
|
102
|
+
place-items: center;
|
|
103
|
+
padding: 10px;
|
|
104
|
+
position: relative;
|
|
105
|
+
overflow: hidden;
|
|
106
|
+
}
|
|
19
107
|
.screen {
|
|
20
108
|
display: block;
|
|
21
|
-
max-width:
|
|
109
|
+
max-width: calc(100vw - 20px);
|
|
110
|
+
max-height: calc(100vh - 86px);
|
|
111
|
+
width: auto;
|
|
112
|
+
height: auto;
|
|
22
113
|
user-select: none;
|
|
23
114
|
cursor: default;
|
|
24
115
|
margin: 0;
|
|
25
116
|
}
|
|
26
117
|
.screen.write-enabled { cursor: crosshair; }
|
|
27
|
-
.
|
|
118
|
+
.empty-state {
|
|
119
|
+
position: absolute;
|
|
120
|
+
inset: 0;
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
justify-content: center;
|
|
124
|
+
padding: 16px;
|
|
125
|
+
pointer-events: none;
|
|
126
|
+
}
|
|
127
|
+
.empty-state-card {
|
|
128
|
+
max-width: min(680px, 92vw);
|
|
129
|
+
text-align: center;
|
|
130
|
+
background: rgba(24, 24, 24, 0.88);
|
|
131
|
+
border: 1px solid #3e3e42;
|
|
132
|
+
border-radius: 8px;
|
|
133
|
+
padding: 14px 16px;
|
|
134
|
+
color: #d4d4d4;
|
|
135
|
+
font-size: 13px;
|
|
136
|
+
line-height: 1.45;
|
|
137
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
|
138
|
+
}
|
|
139
|
+
.empty-state.error .empty-state-card {
|
|
140
|
+
border-color: #7c2b2b;
|
|
141
|
+
color: #ffd8d8;
|
|
142
|
+
background: rgba(44, 18, 18, 0.9);
|
|
143
|
+
}
|
|
144
|
+
.file-panel { position: fixed; right: 8px; bottom: 66px; width: 380px; max-height: 60vh; overflow: auto; background: #1b1b1d; border: 1px solid #3e3e42; border-radius: 6px; padding: 8px; display: none; z-index: 9; }
|
|
28
145
|
.file-panel.open { display: block; }
|
|
29
146
|
.file-panel .row { display: flex; gap: 6px; margin-bottom: 6px; }
|
|
30
147
|
.file-panel button { font: inherit; }
|
|
148
|
+
.file-panel input { flex: 1; min-width: 0; background: var(--vscode-input-background); color: var(--vscode-input-foreground); border: 1px solid var(--vscode-input-border); border-radius: 4px; padding: 5px 8px; }
|
|
31
149
|
.file-list { border: 1px solid #3e3e42; border-radius: 4px; padding: 4px; max-height: 35vh; overflow: auto; }
|
|
32
150
|
.file-item { display: block; width: 100%; text-align: left; background: #2a2a2d; color: #d4d4d4; border: 1px solid #3e3e42; border-radius: 4px; padding: 4px 6px; margin: 3px 0; cursor: pointer; }
|
|
33
151
|
.file-item.dir { background: #253045; }
|
|
34
152
|
.path-label { font-size: 12px; opacity: 0.85; word-break: break-all; }
|
|
153
|
+
.auth-modal {
|
|
154
|
+
position: fixed;
|
|
155
|
+
inset: 0;
|
|
156
|
+
z-index: 30;
|
|
157
|
+
display: none;
|
|
158
|
+
align-items: center;
|
|
159
|
+
justify-content: center;
|
|
160
|
+
background: rgba(0, 0, 0, 0.45);
|
|
161
|
+
}
|
|
162
|
+
.auth-modal.open { display: flex; }
|
|
163
|
+
.auth-card {
|
|
164
|
+
width: min(420px, 92vw);
|
|
165
|
+
background: #1f1f1f;
|
|
166
|
+
border: 1px solid #3e3e42;
|
|
167
|
+
border-radius: 8px;
|
|
168
|
+
padding: 12px;
|
|
169
|
+
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.45);
|
|
170
|
+
}
|
|
171
|
+
.auth-title { font-size: 13px; font-weight: 600; margin: 0 0 8px 0; }
|
|
172
|
+
.auth-hint { font-size: 12px; color: #b7b7b7; margin: 0 0 8px 0; }
|
|
173
|
+
.auth-row { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
|
|
174
|
+
.auth-row input[type="password"] {
|
|
175
|
+
flex: 1;
|
|
176
|
+
min-width: 0;
|
|
177
|
+
background: var(--vscode-input-background);
|
|
178
|
+
color: var(--vscode-input-foreground);
|
|
179
|
+
border: 1px solid var(--vscode-input-border);
|
|
180
|
+
border-radius: 4px;
|
|
181
|
+
padding: 6px 8px;
|
|
182
|
+
font: inherit;
|
|
183
|
+
}
|
|
184
|
+
.auth-row button {
|
|
185
|
+
background: var(--vscode-button-background);
|
|
186
|
+
color: var(--vscode-button-foreground);
|
|
187
|
+
border: 1px solid transparent;
|
|
188
|
+
border-radius: 4px;
|
|
189
|
+
padding: 6px 10px;
|
|
190
|
+
cursor: pointer;
|
|
191
|
+
font: inherit;
|
|
192
|
+
}
|
|
193
|
+
.auth-row button.sec {
|
|
194
|
+
background: var(--vscode-button-secondaryBackground);
|
|
195
|
+
color: var(--vscode-button-secondaryForeground);
|
|
196
|
+
border-color: #505050;
|
|
197
|
+
}
|
|
35
198
|
</style>
|
|
36
199
|
</head>
|
|
37
200
|
<body>
|
|
38
201
|
<div class="bar">
|
|
39
|
-
<strong>Remote</strong>
|
|
40
|
-
<label>Session <input id="session" placeholder="client_xxx" /></label>
|
|
41
|
-
<label>Password <input id="pwd" type="password" placeholder="@@PWD_HINT@@" /></label>
|
|
42
|
-
<button id="connectBtn">Connect</button>
|
|
202
|
+
<strong class="brand">Remote</strong>
|
|
43
203
|
<button id="modeBtn" class="alt">View Only</button>
|
|
44
204
|
<button id="refreshBtn" class="alt">Refresh</button>
|
|
45
|
-
<button id="
|
|
46
|
-
<button id="clipPushBtn" class="alt">Clipboard: Local -> PC</button>
|
|
47
|
-
<button id="browseBtn" class="alt">Browse PC Files</button>
|
|
48
|
-
<input id="filePullPath" placeholder="C:\\Users\\...\\file.txt" />
|
|
49
|
-
<button id="filePullBtn" class="alt">Fetch File <- PC</button>
|
|
50
|
-
<input id="filePushInput" type="file" />
|
|
51
|
-
<button id="filePushBtn" class="alt">Send File -> PC</button>
|
|
205
|
+
<button id="browseBtn" class="alt">Files</button>
|
|
52
206
|
<button id="disconnectBtn" class="warn">Disconnect</button>
|
|
53
207
|
<span class="spacer"></span>
|
|
208
|
+
<span class="state hint">Shortcuts: Ctrl/Cmd+C (PC -> Local), Ctrl/Cmd+V (Local -> PC)</span>
|
|
54
209
|
<span class="state" id="state">Idle</span>
|
|
55
210
|
<span class="state" id="modeState">Mode: View Only</span>
|
|
56
211
|
</div>
|
|
57
212
|
<div class="screen-wrap" id="screenWrap">
|
|
58
|
-
<
|
|
213
|
+
<div class="screen-stage" id="screenStage">
|
|
214
|
+
<img id="screen" class="screen" alt="Remote screen" />
|
|
215
|
+
<div id="emptyState" class="empty-state">
|
|
216
|
+
<div id="emptyStateCard" class="empty-state-card">
|
|
217
|
+
Waiting for remote session...
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
59
221
|
</div>
|
|
60
222
|
<div class="file-panel" id="filePanel">
|
|
61
223
|
<div class="row">
|
|
@@ -64,20 +226,39 @@
|
|
|
64
226
|
<button id="closePanelBtn" class="warn">Close</button>
|
|
65
227
|
</div>
|
|
66
228
|
<div class="path-label" id="pathLabel">Path: (none)</div>
|
|
229
|
+
<div class="row">
|
|
230
|
+
<input id="filePullPath" placeholder="Remote file path" />
|
|
231
|
+
<button id="filePullBtn" class="alt">Fetch <- PC</button>
|
|
232
|
+
</div>
|
|
233
|
+
<div class="row">
|
|
234
|
+
<input id="filePushInput" type="file" />
|
|
235
|
+
<button id="filePushBtn" class="alt">Send -> PC</button>
|
|
236
|
+
</div>
|
|
67
237
|
<div class="file-list" id="fileList"></div>
|
|
68
238
|
</div>
|
|
239
|
+
<div class="auth-modal" id="authModal">
|
|
240
|
+
<div class="auth-card">
|
|
241
|
+
<p class="auth-title">Remote session password required</p>
|
|
242
|
+
<p class="auth-hint" id="authHint">Enter the password for this remote session.</p>
|
|
243
|
+
<div class="auth-row">
|
|
244
|
+
<input id="authPasswordInput" type="password" placeholder="Session password" />
|
|
245
|
+
</div>
|
|
246
|
+
<div class="auth-row">
|
|
247
|
+
<button id="authSubmitBtn">Continue</button>
|
|
248
|
+
<button id="authCancelBtn" class="sec">Cancel</button>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
69
252
|
<script>
|
|
70
253
|
const relayFallback = @@RELAY_FALLBACK_JS@@ || "";
|
|
71
254
|
const pwdHint = @@PWD_JS@@ || "";
|
|
72
255
|
const stateEl = document.getElementById("state");
|
|
73
256
|
const modeStateEl = document.getElementById("modeState");
|
|
74
|
-
const sessionEl = document.getElementById("session");
|
|
75
|
-
const pwdEl = document.getElementById("pwd");
|
|
76
257
|
const screenEl = document.getElementById("screen");
|
|
258
|
+
const emptyStateEl = document.getElementById("emptyState");
|
|
259
|
+
const emptyStateCardEl = document.getElementById("emptyStateCard");
|
|
77
260
|
const modeBtn = document.getElementById("modeBtn");
|
|
78
261
|
const wrapEl = document.getElementById("screenWrap");
|
|
79
|
-
const clipPullBtn = document.getElementById("clipPullBtn");
|
|
80
|
-
const clipPushBtn = document.getElementById("clipPushBtn");
|
|
81
262
|
const browseBtn = document.getElementById("browseBtn");
|
|
82
263
|
const filePullPath = document.getElementById("filePullPath");
|
|
83
264
|
const filePullBtn = document.getElementById("filePullBtn");
|
|
@@ -89,6 +270,11 @@
|
|
|
89
270
|
const closePanelBtn = document.getElementById("closePanelBtn");
|
|
90
271
|
const pathLabel = document.getElementById("pathLabel");
|
|
91
272
|
const fileList = document.getElementById("fileList");
|
|
273
|
+
const authModal = document.getElementById("authModal");
|
|
274
|
+
const authHint = document.getElementById("authHint");
|
|
275
|
+
const authPasswordInput = document.getElementById("authPasswordInput");
|
|
276
|
+
const authSubmitBtn = document.getElementById("authSubmitBtn");
|
|
277
|
+
const authCancelBtn = document.getElementById("authCancelBtn");
|
|
92
278
|
let ws = null;
|
|
93
279
|
let authed = false;
|
|
94
280
|
let writeEnabled = false;
|
|
@@ -99,12 +285,119 @@
|
|
|
99
285
|
let remoteClipboardBusy = false;
|
|
100
286
|
let localClipboardBusy = false;
|
|
101
287
|
let currentBrowsePath = "";
|
|
288
|
+
let reconnectTimer = null;
|
|
289
|
+
let pendingPasswordPrompt = null;
|
|
290
|
+
let hasFrame = false;
|
|
291
|
+
let authWatchdogTimer = null;
|
|
292
|
+
let authChallengeSeen = false;
|
|
293
|
+
let dragActive = false;
|
|
294
|
+
let pointerDown = false;
|
|
295
|
+
let pointerButton = "left";
|
|
296
|
+
let pointerDownPoint = null;
|
|
297
|
+
let suppressClickUntil = 0;
|
|
298
|
+
let disablePressLifecycle = false;
|
|
299
|
+
let lastFrameMeta = null;
|
|
300
|
+
let sessionAgentVersion = "";
|
|
301
|
+
let sessionAgentOs = "";
|
|
102
302
|
|
|
103
303
|
function setState(t) { stateEl.textContent = t; }
|
|
304
|
+
function parseVersion(v) {
|
|
305
|
+
return String(v || "")
|
|
306
|
+
.split(".")
|
|
307
|
+
.map((n) => Number.parseInt(n, 10))
|
|
308
|
+
.filter((n) => Number.isFinite(n));
|
|
309
|
+
}
|
|
310
|
+
function versionLt(a, b) {
|
|
311
|
+
const av = parseVersion(a);
|
|
312
|
+
const bv = parseVersion(b);
|
|
313
|
+
const n = Math.max(av.length, bv.length);
|
|
314
|
+
for (let i = 0; i < n; i++) {
|
|
315
|
+
const ai = av[i] || 0;
|
|
316
|
+
const bi = bv[i] || 0;
|
|
317
|
+
if (ai < bi) return true;
|
|
318
|
+
if (ai > bi) return false;
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
async function refreshSessionAgentMeta(sid) {
|
|
323
|
+
try {
|
|
324
|
+
const r = await fetch("/api/sessions", { cache: "no-store" });
|
|
325
|
+
if (!r.ok) return;
|
|
326
|
+
const j = await r.json();
|
|
327
|
+
const list = Array.isArray(j && j.sessions) ? j.sessions : [];
|
|
328
|
+
const row = list.find((it) => String(it && it.session_id || "") === sid);
|
|
329
|
+
if (!row) return;
|
|
330
|
+
sessionAgentVersion = String(row.agent_version || "").trim();
|
|
331
|
+
sessionAgentOs = String(row.agent_os || "").trim().toLowerCase();
|
|
332
|
+
if (
|
|
333
|
+
sessionAgentVersion &&
|
|
334
|
+
sessionAgentOs.includes("windows") &&
|
|
335
|
+
versionLt(sessionAgentVersion, "1.0.71")
|
|
336
|
+
) {
|
|
337
|
+
setState("Agent v" + sessionAgentVersion + " detected. Upgrade agent from /files to enable reliable control.");
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
/* ignore */
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function sha256HexFallback(input) {
|
|
344
|
+
const msg = unescape(encodeURIComponent(String(input || "")));
|
|
345
|
+
const bytes = new Uint8Array(msg.length);
|
|
346
|
+
for (let i = 0; i < msg.length; i++) bytes[i] = msg.charCodeAt(i);
|
|
347
|
+
const K = new Uint32Array([
|
|
348
|
+
0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
|
|
349
|
+
0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
|
|
350
|
+
0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
|
|
351
|
+
0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
|
|
352
|
+
0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
|
|
353
|
+
0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
|
|
354
|
+
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
|
|
355
|
+
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2
|
|
356
|
+
]);
|
|
357
|
+
const H = new Uint32Array([0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]);
|
|
358
|
+
const bitLen = bytes.length * 8;
|
|
359
|
+
const totalLen = ((bytes.length + 9 + 63) >> 6) << 6;
|
|
360
|
+
const data = new Uint8Array(totalLen);
|
|
361
|
+
data.set(bytes);
|
|
362
|
+
data[bytes.length] = 0x80;
|
|
363
|
+
const view = new DataView(data.buffer);
|
|
364
|
+
view.setUint32(totalLen - 8, Math.floor(bitLen / 0x100000000), false);
|
|
365
|
+
view.setUint32(totalLen - 4, bitLen >>> 0, false);
|
|
366
|
+
const w = new Uint32Array(64);
|
|
367
|
+
const rr = (x, n) => (x >>> n) | (x << (32 - n));
|
|
368
|
+
for (let i = 0; i < totalLen; i += 64) {
|
|
369
|
+
for (let t = 0; t < 16; t++) w[t] = view.getUint32(i + t * 4, false);
|
|
370
|
+
for (let t = 16; t < 64; t++) {
|
|
371
|
+
const s0 = rr(w[t - 15], 7) ^ rr(w[t - 15], 18) ^ (w[t - 15] >>> 3);
|
|
372
|
+
const s1 = rr(w[t - 2], 17) ^ rr(w[t - 2], 19) ^ (w[t - 2] >>> 10);
|
|
373
|
+
w[t] = (((w[t - 16] + s0) >>> 0) + ((w[t - 7] + s1) >>> 0)) >>> 0;
|
|
374
|
+
}
|
|
375
|
+
let a = H[0], b = H[1], c = H[2], d = H[3], e = H[4], f = H[5], g = H[6], h = H[7];
|
|
376
|
+
for (let t = 0; t < 64; t++) {
|
|
377
|
+
const S1 = rr(e, 6) ^ rr(e, 11) ^ rr(e, 25);
|
|
378
|
+
const ch = (e & f) ^ (~e & g);
|
|
379
|
+
const t1 = (((((h + S1) >>> 0) + ((ch + K[t]) >>> 0)) >>> 0) + w[t]) >>> 0;
|
|
380
|
+
const S0 = rr(a, 2) ^ rr(a, 13) ^ rr(a, 22);
|
|
381
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
382
|
+
const t2 = (S0 + maj) >>> 0;
|
|
383
|
+
h = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0;
|
|
384
|
+
}
|
|
385
|
+
H[0] = (H[0] + a) >>> 0; H[1] = (H[1] + b) >>> 0; H[2] = (H[2] + c) >>> 0; H[3] = (H[3] + d) >>> 0;
|
|
386
|
+
H[4] = (H[4] + e) >>> 0; H[5] = (H[5] + f) >>> 0; H[6] = (H[6] + g) >>> 0; H[7] = (H[7] + h) >>> 0;
|
|
387
|
+
}
|
|
388
|
+
return Array.from(H).map((n) => n.toString(16).padStart(8, "0")).join("");
|
|
389
|
+
}
|
|
390
|
+
function showEmptyState(msg, isError) {
|
|
391
|
+
emptyStateCardEl.textContent = String(msg || "Waiting for screenshot...");
|
|
392
|
+
emptyStateEl.classList.toggle("error", Boolean(isError));
|
|
393
|
+
emptyStateEl.style.display = "flex";
|
|
394
|
+
}
|
|
395
|
+
function hideEmptyState() {
|
|
396
|
+
emptyStateEl.style.display = "none";
|
|
397
|
+
emptyStateEl.classList.remove("error");
|
|
398
|
+
}
|
|
104
399
|
function updateWriteControls() {
|
|
105
400
|
const ro = !writeEnabled;
|
|
106
|
-
clipPullBtn.disabled = ro;
|
|
107
|
-
clipPushBtn.disabled = ro;
|
|
108
401
|
filePullBtn.disabled = ro;
|
|
109
402
|
filePullPath.disabled = ro;
|
|
110
403
|
filePushBtn.disabled = ro;
|
|
@@ -116,9 +409,12 @@
|
|
|
116
409
|
if (ro) filePanel.classList.remove("open");
|
|
117
410
|
}
|
|
118
411
|
function hashHex(s) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
412
|
+
if (globalThis.crypto && globalThis.crypto.subtle && typeof TextEncoder !== "undefined") {
|
|
413
|
+
return crypto.subtle.digest("SHA-256", new TextEncoder().encode(s)).then((buf) =>
|
|
414
|
+
[...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("")
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
return Promise.resolve(sha256HexFallback(s));
|
|
122
418
|
}
|
|
123
419
|
function wsBaseUrl() {
|
|
124
420
|
if (location.protocol.startsWith("http")) {
|
|
@@ -127,23 +423,163 @@
|
|
|
127
423
|
}
|
|
128
424
|
return relayFallback || "ws://127.0.0.1:9877";
|
|
129
425
|
}
|
|
426
|
+
function currentSessionId() {
|
|
427
|
+
return String(new URLSearchParams(location.search).get("session") || "").trim();
|
|
428
|
+
}
|
|
429
|
+
function resolveSessionId() {
|
|
430
|
+
const sid = currentSessionId();
|
|
431
|
+
if (sid) return sid;
|
|
432
|
+
const entered = String(window.prompt("Enter remote session id", "") || "").trim();
|
|
433
|
+
return entered;
|
|
434
|
+
}
|
|
435
|
+
function sessionPwdKey(sid) {
|
|
436
|
+
return "forge_remote_pwd_" + sid;
|
|
437
|
+
}
|
|
438
|
+
function readRememberedPassword(sid) {
|
|
439
|
+
if (!sid) return "";
|
|
440
|
+
try { return String(localStorage.getItem(sessionPwdKey(sid)) || ""); } catch { return ""; }
|
|
441
|
+
}
|
|
442
|
+
function readDashboardPassword() {
|
|
443
|
+
try { return String(localStorage.getItem("forge_dash_pwd") || ""); } catch { return ""; }
|
|
444
|
+
}
|
|
445
|
+
function rememberPassword(sid, pw) {
|
|
446
|
+
if (!sid || !pw) return;
|
|
447
|
+
try { localStorage.setItem(sessionPwdKey(sid), pw); } catch {}
|
|
448
|
+
}
|
|
449
|
+
function forgetPassword(sid) {
|
|
450
|
+
if (!sid) return;
|
|
451
|
+
try { localStorage.removeItem(sessionPwdKey(sid)); } catch {}
|
|
452
|
+
}
|
|
453
|
+
function askPassword(reason) {
|
|
454
|
+
if (pendingPasswordPrompt) return pendingPasswordPrompt;
|
|
455
|
+
pendingPasswordPrompt = new Promise((resolve) => {
|
|
456
|
+
authHint.textContent = String(reason || "Enter remote session password");
|
|
457
|
+
authPasswordInput.value = "";
|
|
458
|
+
authModal.classList.add("open");
|
|
459
|
+
setTimeout(() => authPasswordInput.focus(), 0);
|
|
460
|
+
const close = (value) => {
|
|
461
|
+
authModal.classList.remove("open");
|
|
462
|
+
authSubmitBtn.onclick = null;
|
|
463
|
+
authCancelBtn.onclick = null;
|
|
464
|
+
authPasswordInput.onkeydown = null;
|
|
465
|
+
pendingPasswordPrompt = null;
|
|
466
|
+
resolve(String(value || "").trim());
|
|
467
|
+
};
|
|
468
|
+
authSubmitBtn.onclick = () => close(authPasswordInput.value);
|
|
469
|
+
authCancelBtn.onclick = () => close("");
|
|
470
|
+
authPasswordInput.onkeydown = (ev) => {
|
|
471
|
+
if (ev.key === "Enter") {
|
|
472
|
+
ev.preventDefault();
|
|
473
|
+
close(authPasswordInput.value);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (ev.key === "Escape") {
|
|
477
|
+
ev.preventDefault();
|
|
478
|
+
close("");
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
});
|
|
482
|
+
return pendingPasswordPrompt;
|
|
483
|
+
}
|
|
484
|
+
async function resolveSessionPassword(sid, forcePrompt) {
|
|
485
|
+
if (forcePrompt) {
|
|
486
|
+
const entered = await askPassword("Remote session password required");
|
|
487
|
+
if (entered) rememberPassword(sid, entered);
|
|
488
|
+
return entered;
|
|
489
|
+
}
|
|
490
|
+
const fromUrl = String(new URLSearchParams(location.search).get("password") || "").trim();
|
|
491
|
+
if (fromUrl) {
|
|
492
|
+
rememberPassword(sid, fromUrl);
|
|
493
|
+
return fromUrl;
|
|
494
|
+
}
|
|
495
|
+
const remembered = String(readRememberedPassword(sid) || "").trim();
|
|
496
|
+
if (remembered) return remembered;
|
|
497
|
+
const dashboardPw = String(readDashboardPassword() || "").trim();
|
|
498
|
+
if (dashboardPw) {
|
|
499
|
+
rememberPassword(sid, dashboardPw);
|
|
500
|
+
return dashboardPw;
|
|
501
|
+
}
|
|
502
|
+
const fallback = String(pwdHint || "").trim();
|
|
503
|
+
if (fallback) return fallback;
|
|
504
|
+
const entered = await askPassword("Remote session password required");
|
|
505
|
+
if (entered) rememberPassword(sid, entered);
|
|
506
|
+
return entered;
|
|
507
|
+
}
|
|
508
|
+
function scheduleReconnect() {
|
|
509
|
+
if (reconnectTimer) return;
|
|
510
|
+
reconnectTimer = setTimeout(() => {
|
|
511
|
+
reconnectTimer = null;
|
|
512
|
+
if (ws || authed) return;
|
|
513
|
+
if (!currentSessionId()) return;
|
|
514
|
+
connect();
|
|
515
|
+
}, 2500);
|
|
516
|
+
}
|
|
517
|
+
function clearAuthWatchdog() {
|
|
518
|
+
if (authWatchdogTimer) {
|
|
519
|
+
clearTimeout(authWatchdogTimer);
|
|
520
|
+
authWatchdogTimer = null;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function armAuthWatchdog() {
|
|
524
|
+
clearAuthWatchdog();
|
|
525
|
+
authWatchdogTimer = setTimeout(() => {
|
|
526
|
+
if (!ws || ws.readyState !== 1 || authed) return;
|
|
527
|
+
if (authChallengeSeen) return;
|
|
528
|
+
setState("Handshake stalled — reconnecting...");
|
|
529
|
+
if (!hasFrame) {
|
|
530
|
+
showEmptyState(
|
|
531
|
+
"Connected to relay, but the session handshake is stalled. Retrying automatically...",
|
|
532
|
+
true
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
disconnect();
|
|
536
|
+
setTimeout(connect, 200);
|
|
537
|
+
}, 6500);
|
|
538
|
+
}
|
|
130
539
|
function connect() {
|
|
131
|
-
const sid =
|
|
540
|
+
const sid = resolveSessionId();
|
|
132
541
|
if (!sid) { setState("Session required"); return; }
|
|
133
|
-
|
|
542
|
+
void refreshSessionAgentMeta(sid);
|
|
134
543
|
const url = wsBaseUrl().replace(/\/+$/, "") + "/ws/viewer/" + encodeURIComponent(sid);
|
|
135
544
|
disconnect();
|
|
545
|
+
hasFrame = false;
|
|
546
|
+
authChallengeSeen = false;
|
|
547
|
+
screenEl.removeAttribute("src");
|
|
136
548
|
setState("Connecting…");
|
|
549
|
+
showEmptyState("Connecting to remote session...", false);
|
|
137
550
|
ws = new WebSocket(url);
|
|
138
|
-
ws.onopen = () => {
|
|
139
|
-
|
|
140
|
-
|
|
551
|
+
ws.onopen = () => {
|
|
552
|
+
setState("Connected — waiting auth");
|
|
553
|
+
if (!hasFrame) showEmptyState("Connected. Waiting for auth challenge...", false);
|
|
554
|
+
ws.send(JSON.stringify({ type: "get_info" }));
|
|
555
|
+
armAuthWatchdog();
|
|
556
|
+
};
|
|
557
|
+
ws.onclose = () => {
|
|
558
|
+
authed = false;
|
|
559
|
+
setState("Disconnected");
|
|
560
|
+
stopShotLoop();
|
|
561
|
+
clearAuthWatchdog();
|
|
562
|
+
if (!hasFrame) showEmptyState("Disconnected. Reconnecting...", true);
|
|
563
|
+
scheduleReconnect();
|
|
564
|
+
};
|
|
565
|
+
ws.onerror = () => {
|
|
566
|
+
setState("Socket error");
|
|
567
|
+
clearAuthWatchdog();
|
|
568
|
+
if (!hasFrame) showEmptyState("Network/socket error while connecting to remote screen.", true);
|
|
569
|
+
};
|
|
141
570
|
ws.onmessage = async (ev) => {
|
|
142
571
|
let msg = null;
|
|
143
572
|
try { msg = JSON.parse(String(ev.data || "")); } catch { return; }
|
|
144
573
|
const t = String(msg && msg.type || "");
|
|
145
574
|
if (t === "auth_challenge") {
|
|
146
|
-
|
|
575
|
+
authChallengeSeen = true;
|
|
576
|
+
clearAuthWatchdog();
|
|
577
|
+
const pwd = await resolveSessionPassword(sid, false);
|
|
578
|
+
if (!pwd) {
|
|
579
|
+
setState("Missing session password");
|
|
580
|
+
if (!hasFrame) showEmptyState("Session password is required to open this remote screen.", true);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
147
583
|
const ph = await hashHex(pwd);
|
|
148
584
|
const nonce = String(msg.nonce || "");
|
|
149
585
|
const resp = await hashHex(ph + ":" + nonce);
|
|
@@ -152,8 +588,24 @@
|
|
|
152
588
|
}
|
|
153
589
|
if (t === "auth_result") {
|
|
154
590
|
authed = !!msg.ok;
|
|
155
|
-
|
|
156
|
-
|
|
591
|
+
if (authed) {
|
|
592
|
+
clearAuthWatchdog();
|
|
593
|
+
setState("Authenticated");
|
|
594
|
+
startShotLoop();
|
|
595
|
+
requestScreenshot();
|
|
596
|
+
if (!hasFrame) showEmptyState("Connected. Waiting for first screenshot frame...", false);
|
|
597
|
+
} else {
|
|
598
|
+
forgetPassword(sid);
|
|
599
|
+
const entered = await resolveSessionPassword(sid, true);
|
|
600
|
+
if (entered) {
|
|
601
|
+
setState("Retrying with updated password...");
|
|
602
|
+
disconnect();
|
|
603
|
+
setTimeout(connect, 120);
|
|
604
|
+
} else {
|
|
605
|
+
setState("Auth failed");
|
|
606
|
+
if (!hasFrame) showEmptyState("Authentication failed. Please enter the correct session password.", true);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
157
609
|
return;
|
|
158
610
|
}
|
|
159
611
|
if (t === "system_info") {
|
|
@@ -165,6 +617,47 @@
|
|
|
165
617
|
if (msg.ok && msg.b64) {
|
|
166
618
|
const mime = String(msg.mime || "image/png");
|
|
167
619
|
screenEl.src = "data:" + mime + ";base64," + String(msg.b64);
|
|
620
|
+
lastFrameMeta = {
|
|
621
|
+
imageWidth: Number.isFinite(Number(msg.width)) ? Math.max(1, Math.floor(Number(msg.width))) : 0,
|
|
622
|
+
imageHeight: Number.isFinite(Number(msg.height)) ? Math.max(1, Math.floor(Number(msg.height))) : 0,
|
|
623
|
+
virtualX: Number.isFinite(Number(msg.virtual_x)) ? Math.floor(Number(msg.virtual_x)) : 0,
|
|
624
|
+
virtualY: Number.isFinite(Number(msg.virtual_y)) ? Math.floor(Number(msg.virtual_y)) : 0,
|
|
625
|
+
virtualWidth: Number.isFinite(Number(msg.virtual_width)) ? Math.max(1, Math.floor(Number(msg.virtual_width))) : 0,
|
|
626
|
+
virtualHeight: Number.isFinite(Number(msg.virtual_height)) ? Math.max(1, Math.floor(Number(msg.virtual_height))) : 0,
|
|
627
|
+
};
|
|
628
|
+
hasFrame = true;
|
|
629
|
+
hideEmptyState();
|
|
630
|
+
} else if (!hasFrame) {
|
|
631
|
+
const em = String(msg.error || "").trim();
|
|
632
|
+
showEmptyState(em || "Remote session connected, but screenshot is not available yet.", true);
|
|
633
|
+
}
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (t === "rc_input_result") {
|
|
637
|
+
if (!msg.ok) {
|
|
638
|
+
const em = String(msg.error || "").trim();
|
|
639
|
+
const low = em.toLowerCase();
|
|
640
|
+
if (
|
|
641
|
+
low.includes("unsupported remote control action: mouse_down") ||
|
|
642
|
+
low.includes("unsupported remote control action: mouse_up")
|
|
643
|
+
) {
|
|
644
|
+
disablePressLifecycle = true;
|
|
645
|
+
setState("Drag control needs newer agent; click/scroll still work.");
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (low.includes("here-string") || low.includes("parsererror") || low.includes("add-type")) {
|
|
649
|
+
writeEnabled = false;
|
|
650
|
+
modeBtn.textContent = "View Only";
|
|
651
|
+
modeStateEl.textContent = "Mode: View Only";
|
|
652
|
+
modeBtn.className = "alt";
|
|
653
|
+
screenEl.classList.remove("write-enabled");
|
|
654
|
+
updateWriteControls();
|
|
655
|
+
const ver = sessionAgentVersion ? (" v" + sessionAgentVersion) : "";
|
|
656
|
+
setState("Remote agent" + ver + " needs upgrade for control input. Open /files and click Upgrade agent.");
|
|
657
|
+
showEmptyState("Remote input failed due to outdated agent runtime. Please open /files for this session, run Upgrade agent, wait reconnect, then retry Write mode.", true);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
setState(em ? ("Input failed: " + em) : "Input failed");
|
|
168
661
|
}
|
|
169
662
|
return;
|
|
170
663
|
}
|
|
@@ -179,15 +672,23 @@
|
|
|
179
672
|
}
|
|
180
673
|
function disconnect() {
|
|
181
674
|
stopShotLoop();
|
|
675
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
676
|
+
clearAuthWatchdog();
|
|
182
677
|
if (ws) { try { ws.close(); } catch {} ws = null; }
|
|
183
678
|
authed = false;
|
|
184
679
|
inflightShot = false;
|
|
680
|
+
dragActive = false;
|
|
681
|
+
pointerDown = false;
|
|
682
|
+
pointerButton = "left";
|
|
683
|
+
pointerDownPoint = null;
|
|
684
|
+
disablePressLifecycle = false;
|
|
185
685
|
pendingReqs.clear();
|
|
186
686
|
writeEnabled = false;
|
|
187
687
|
modeBtn.textContent = "View Only";
|
|
188
688
|
modeStateEl.textContent = "Mode: View Only";
|
|
189
689
|
modeBtn.className = "alt";
|
|
190
690
|
screenEl.classList.remove("write-enabled");
|
|
691
|
+
if (!hasFrame) showEmptyState("Disconnected from remote session.", true);
|
|
191
692
|
updateWriteControls();
|
|
192
693
|
}
|
|
193
694
|
function stopShotLoop() {
|
|
@@ -200,6 +701,7 @@
|
|
|
200
701
|
function requestScreenshot() {
|
|
201
702
|
if (!ws || ws.readyState !== 1 || !authed || inflightShot) return;
|
|
202
703
|
inflightShot = true;
|
|
704
|
+
if (!hasFrame) showEmptyState("Requesting screenshot frame...", false);
|
|
203
705
|
ws.send(JSON.stringify({ type: "fs_screenshot", request_id: "shot_" + (++reqSeq) }));
|
|
204
706
|
}
|
|
205
707
|
function wsRequest(type, payload) {
|
|
@@ -215,6 +717,31 @@
|
|
|
215
717
|
}, 8000);
|
|
216
718
|
});
|
|
217
719
|
}
|
|
720
|
+
async function pullClipboardToLocal() {
|
|
721
|
+
if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
|
|
722
|
+
const r = await wsRequest("rc_clipboard_get");
|
|
723
|
+
if (!r || !r.ok) { setState("Clipboard pull failed"); return; }
|
|
724
|
+
const text = String(r.text || "");
|
|
725
|
+
try {
|
|
726
|
+
await navigator.clipboard.writeText(text);
|
|
727
|
+
setState("Clipboard copied from PC to local");
|
|
728
|
+
} catch {
|
|
729
|
+
setState("Clipboard write blocked by browser");
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
async function pushLocalClipboardToRemote() {
|
|
733
|
+
if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
|
|
734
|
+
let text = "";
|
|
735
|
+
try {
|
|
736
|
+
text = await navigator.clipboard.readText();
|
|
737
|
+
} catch {
|
|
738
|
+
setState("Clipboard read blocked by browser");
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const r = await wsRequest("rc_clipboard_set", { text });
|
|
742
|
+
if (!r || !r.ok) { setState("Clipboard push failed"); return; }
|
|
743
|
+
setState("Clipboard sent from local to PC");
|
|
744
|
+
}
|
|
218
745
|
async function pullRemoteFileToLocal(remotePath) {
|
|
219
746
|
const p = String(remotePath || "").trim();
|
|
220
747
|
if (!p) return { ok: false, error: "remote path required" };
|
|
@@ -344,17 +871,35 @@
|
|
|
344
871
|
request_id: "rc_" + (++reqSeq),
|
|
345
872
|
}, payload || {})));
|
|
346
873
|
}
|
|
874
|
+
function isBrowserZoomHotkey(ev) {
|
|
875
|
+
if (!(ev.ctrlKey || ev.metaKey) || ev.altKey) return false;
|
|
876
|
+
const key = String(ev.key || "").toLowerCase();
|
|
877
|
+
return key === "+" || key === "-" || key === "=" || key === "_" || key === "0";
|
|
878
|
+
}
|
|
347
879
|
function imgPoint(ev) {
|
|
348
880
|
const r = screenEl.getBoundingClientRect();
|
|
349
881
|
if (!r.width || !r.height || !screenEl.naturalWidth || !screenEl.naturalHeight) return null;
|
|
350
|
-
const
|
|
351
|
-
const
|
|
882
|
+
const px = Math.max(0, Math.min(screenEl.naturalWidth - 1, Math.round((ev.clientX - r.left) * (screenEl.naturalWidth / r.width))));
|
|
883
|
+
const py = Math.max(0, Math.min(screenEl.naturalHeight - 1, Math.round((ev.clientY - r.top) * (screenEl.naturalHeight / r.height))));
|
|
884
|
+
const iw = Number(lastFrameMeta && lastFrameMeta.imageWidth) || Number(screenEl.naturalWidth) || 0;
|
|
885
|
+
const ih = Number(lastFrameMeta && lastFrameMeta.imageHeight) || Number(screenEl.naturalHeight) || 0;
|
|
886
|
+
const vw = Number(lastFrameMeta && lastFrameMeta.virtualWidth) || iw;
|
|
887
|
+
const vh = Number(lastFrameMeta && lastFrameMeta.virtualHeight) || ih;
|
|
888
|
+
const vx = Number(lastFrameMeta && lastFrameMeta.virtualX) || 0;
|
|
889
|
+
const vy = Number(lastFrameMeta && lastFrameMeta.virtualY) || 0;
|
|
890
|
+
const x = vx + Math.round(px * (vw / Math.max(1, iw)));
|
|
891
|
+
const y = vy + Math.round(py * (vh / Math.max(1, ih)));
|
|
352
892
|
return { x, y };
|
|
353
893
|
}
|
|
354
894
|
|
|
355
|
-
document.getElementById("connectBtn").addEventListener("click", connect);
|
|
356
895
|
document.getElementById("disconnectBtn").addEventListener("click", disconnect);
|
|
357
|
-
document.getElementById("refreshBtn").addEventListener("click",
|
|
896
|
+
document.getElementById("refreshBtn").addEventListener("click", () => {
|
|
897
|
+
if (!ws || ws.readyState !== 1) {
|
|
898
|
+
connect();
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
requestScreenshot();
|
|
902
|
+
});
|
|
358
903
|
modeBtn.addEventListener("click", () => {
|
|
359
904
|
writeEnabled = !writeEnabled;
|
|
360
905
|
modeBtn.textContent = writeEnabled ? "Write Only" : "View Only";
|
|
@@ -363,31 +908,6 @@
|
|
|
363
908
|
screenEl.classList.toggle("write-enabled", writeEnabled);
|
|
364
909
|
updateWriteControls();
|
|
365
910
|
});
|
|
366
|
-
clipPullBtn.addEventListener("click", async () => {
|
|
367
|
-
if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
|
|
368
|
-
const r = await wsRequest("rc_clipboard_get");
|
|
369
|
-
if (!r || !r.ok) { setState("Clipboard pull failed"); return; }
|
|
370
|
-
const text = String(r.text || "");
|
|
371
|
-
try {
|
|
372
|
-
await navigator.clipboard.writeText(text);
|
|
373
|
-
setState("Clipboard copied from PC to local");
|
|
374
|
-
} catch {
|
|
375
|
-
setState("Clipboard write blocked by browser");
|
|
376
|
-
}
|
|
377
|
-
});
|
|
378
|
-
clipPushBtn.addEventListener("click", async () => {
|
|
379
|
-
if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
|
|
380
|
-
let text = "";
|
|
381
|
-
try {
|
|
382
|
-
text = await navigator.clipboard.readText();
|
|
383
|
-
} catch {
|
|
384
|
-
setState("Clipboard read blocked by browser");
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
const r = await wsRequest("rc_clipboard_set", { text });
|
|
388
|
-
if (!r || !r.ok) { setState("Clipboard push failed"); return; }
|
|
389
|
-
setState("Clipboard sent from local to PC");
|
|
390
|
-
});
|
|
391
911
|
filePullBtn.addEventListener("click", async () => {
|
|
392
912
|
if (!writeEnabled) { setState("Enable Write Only mode for file fetch"); return; }
|
|
393
913
|
const p = String(filePullPath.value || "").trim();
|
|
@@ -446,7 +966,57 @@
|
|
|
446
966
|
filePushInput.value = "";
|
|
447
967
|
});
|
|
448
968
|
|
|
969
|
+
screenEl.addEventListener("mousedown", (ev) => {
|
|
970
|
+
if (!writeEnabled) return;
|
|
971
|
+
if (ev.button !== 0 && ev.button !== 1 && ev.button !== 2) return;
|
|
972
|
+
const p = imgPoint(ev); if (!p) return;
|
|
973
|
+
ev.preventDefault();
|
|
974
|
+
pointerDown = true;
|
|
975
|
+
pointerButton = ev.button === 2 ? "right" : (ev.button === 1 ? "middle" : "left");
|
|
976
|
+
pointerDownPoint = p;
|
|
977
|
+
dragActive = false;
|
|
978
|
+
});
|
|
979
|
+
window.addEventListener("mouseup", (ev) => {
|
|
980
|
+
if (!writeEnabled || !pointerDown) return;
|
|
981
|
+
if (ev.button !== 0 && ev.button !== 1 && ev.button !== 2) return;
|
|
982
|
+
const p = imgPoint(ev) || pointerDownPoint;
|
|
983
|
+
ev.preventDefault();
|
|
984
|
+
if (dragActive && p && !disablePressLifecycle) {
|
|
985
|
+
sendRemoteInput({ action: "mouse_up", button: pointerButton, x: p.x, y: p.y });
|
|
986
|
+
suppressClickUntil = Date.now() + 220;
|
|
987
|
+
requestScreenshot();
|
|
988
|
+
}
|
|
989
|
+
pointerDown = false;
|
|
990
|
+
pointerDownPoint = null;
|
|
991
|
+
dragActive = false;
|
|
992
|
+
});
|
|
993
|
+
screenEl.addEventListener("mousemove", (ev) => {
|
|
994
|
+
if (!writeEnabled) return;
|
|
995
|
+
const p = imgPoint(ev); if (!p) return;
|
|
996
|
+
if (pointerDown && !disablePressLifecycle) {
|
|
997
|
+
if (!dragActive && pointerDownPoint) {
|
|
998
|
+
const dx = Math.abs(p.x - pointerDownPoint.x);
|
|
999
|
+
const dy = Math.abs(p.y - pointerDownPoint.y);
|
|
1000
|
+
if (dx + dy >= 8) {
|
|
1001
|
+
dragActive = true;
|
|
1002
|
+
sendRemoteInput({ action: "mouse_down", button: pointerButton, x: pointerDownPoint.x, y: pointerDownPoint.y });
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
if (dragActive) {
|
|
1006
|
+
ev.preventDefault();
|
|
1007
|
+
sendRemoteInput({ action: "mouse_move", x: p.x, y: p.y });
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
sendRemoteInput({ action: "mouse_move", x: p.x, y: p.y });
|
|
1012
|
+
});
|
|
1013
|
+
screenEl.addEventListener("dragstart", (ev) => {
|
|
1014
|
+
if (!writeEnabled) return;
|
|
1015
|
+
ev.preventDefault();
|
|
1016
|
+
});
|
|
449
1017
|
screenEl.addEventListener("click", (ev) => {
|
|
1018
|
+
if (!writeEnabled) return;
|
|
1019
|
+
if (Date.now() < suppressClickUntil) return;
|
|
450
1020
|
const p = imgPoint(ev); if (!p) return;
|
|
451
1021
|
sendRemoteInput({ action: "mouse_click", button: ev.button === 2 ? "right" : "left", x: p.x, y: p.y, click_count: 1 });
|
|
452
1022
|
requestScreenshot();
|
|
@@ -458,39 +1028,41 @@
|
|
|
458
1028
|
});
|
|
459
1029
|
screenEl.addEventListener("contextmenu", (ev) => ev.preventDefault());
|
|
460
1030
|
wrapEl.addEventListener("wheel", (ev) => {
|
|
1031
|
+
if (ev.ctrlKey || ev.metaKey) return; // Keep browser zoom (ctrl/cmd + wheel) working.
|
|
461
1032
|
if (!writeEnabled) return;
|
|
462
1033
|
ev.preventDefault();
|
|
463
|
-
|
|
1034
|
+
const p = imgPoint(ev);
|
|
1035
|
+
sendRemoteInput({ action: "mouse_wheel", delta_y: Math.round(ev.deltaY), x: p ? p.x : undefined, y: p ? p.y : undefined });
|
|
464
1036
|
}, { passive: false });
|
|
465
1037
|
window.addEventListener("keydown", (ev) => {
|
|
466
1038
|
if (!writeEnabled) return;
|
|
467
|
-
if (
|
|
1039
|
+
if (["INPUT", "TEXTAREA"].includes(String(document.activeElement && document.activeElement.tagName || ""))) return;
|
|
1040
|
+
if (isBrowserZoomHotkey(ev)) return; // Let browser handle Ctrl/Cmd +/-/0 zoom.
|
|
1041
|
+
const ctrlOrMeta = ev.ctrlKey || ev.metaKey;
|
|
1042
|
+
if (ctrlOrMeta && !ev.shiftKey && !ev.altKey && String(ev.key || "").toLowerCase() === "c") {
|
|
468
1043
|
ev.preventDefault();
|
|
469
1044
|
if (!remoteClipboardBusy) {
|
|
470
1045
|
remoteClipboardBusy = true;
|
|
471
|
-
|
|
1046
|
+
void pullClipboardToLocal();
|
|
472
1047
|
setTimeout(() => { remoteClipboardBusy = false; }, 1200);
|
|
473
1048
|
}
|
|
474
1049
|
return;
|
|
475
1050
|
}
|
|
476
|
-
if (
|
|
1051
|
+
if (ctrlOrMeta && !ev.shiftKey && !ev.altKey && String(ev.key || "").toLowerCase() === "v") {
|
|
477
1052
|
ev.preventDefault();
|
|
478
1053
|
if (!localClipboardBusy) {
|
|
479
1054
|
localClipboardBusy = true;
|
|
480
|
-
|
|
1055
|
+
void pushLocalClipboardToRemote();
|
|
481
1056
|
setTimeout(() => { localClipboardBusy = false; }, 1200);
|
|
482
1057
|
}
|
|
483
1058
|
return;
|
|
484
1059
|
}
|
|
485
|
-
if (["INPUT", "TEXTAREA"].includes(String(document.activeElement && document.activeElement.tagName || ""))) return;
|
|
486
1060
|
ev.preventDefault();
|
|
487
1061
|
sendRemoteInput({ action: "key", key: ev.key, ctrl: ev.ctrlKey, alt: ev.altKey, shift: ev.shiftKey, meta: ev.metaKey });
|
|
488
1062
|
});
|
|
489
1063
|
|
|
490
|
-
const sid0 = new URLSearchParams(location.search).get("session");
|
|
491
|
-
if (sid0) sessionEl.value = sid0;
|
|
492
|
-
if (pwdHint) pwdEl.value = pwdHint;
|
|
493
1064
|
updateWriteControls();
|
|
1065
|
+
connect();
|
|
494
1066
|
</script>
|
|
495
1067
|
</body>
|
|
496
1068
|
</html>
|