mouse-vm 1.0.2
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/bin/cli.js +4 -0
- package/package.json +48 -0
- package/server.js +716 -0
- package/static/app.js +1470 -0
- package/static/icons/icon-192.png +0 -0
- package/static/icons/icon-512.png +0 -0
- package/static/icons/icon.svg +38 -0
- package/static/index.html +738 -0
- package/static/manifest.json +46 -0
- package/static/style.css +2651 -0
- package/static/sw.js +85 -0
package/static/app.js
ADDED
|
@@ -0,0 +1,1470 @@
|
|
|
1
|
+
/* -------------------------------------------------------------
|
|
2
|
+
* VIRTUAL MOUSE & KEYBOARD - FRONTEND CONTROLLER
|
|
3
|
+
* Supports:
|
|
4
|
+
* 1. PWA Service Worker & Install Prompt
|
|
5
|
+
* 2. PIN Handshake Authentication & Network Discovery
|
|
6
|
+
* 3. Live Keystroke Auto-Typing & Desktop TextPad Transmitter
|
|
7
|
+
* 4. Keycode Buttons, Modifiers & PC Shortcuts
|
|
8
|
+
* 5. Touchpad gestures, Multi-touch, DPI sensitivity & Fallbacks
|
|
9
|
+
* ------------------------------------------------------------- */
|
|
10
|
+
|
|
11
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
12
|
+
// --- Application State ---
|
|
13
|
+
let socket = null;
|
|
14
|
+
let isConnected = false;
|
|
15
|
+
let isSimulatorMode = false;
|
|
16
|
+
let sensitivity = 1.1; // DPI multiplier (1600 = 1.1x)
|
|
17
|
+
let scrollSensitivity = 0.8;
|
|
18
|
+
let deferredPrompt = null;
|
|
19
|
+
let heartbeatTimer = null;
|
|
20
|
+
let autoReconnectTimer = null;
|
|
21
|
+
let isUserDisconnect = false;
|
|
22
|
+
|
|
23
|
+
// Trackpad gesture variables
|
|
24
|
+
let lastX = 0;
|
|
25
|
+
let lastY = 0;
|
|
26
|
+
let scrollLastY = 0;
|
|
27
|
+
let isMoving = false;
|
|
28
|
+
let isTwoFingerScrolling = false;
|
|
29
|
+
let touchStartTimestamp = 0;
|
|
30
|
+
const clickMovementThreshold = 3;
|
|
31
|
+
|
|
32
|
+
// --- DOM Elements ---
|
|
33
|
+
const screens = {
|
|
34
|
+
splash: document.getElementById("screen-splash"),
|
|
35
|
+
home: document.getElementById("screen-home"),
|
|
36
|
+
mouse: document.getElementById("screen-mouse"),
|
|
37
|
+
keyboard: document.getElementById("screen-keyboard"),
|
|
38
|
+
settings: document.getElementById("screen-settings")
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const bottomNav = document.getElementById("main-bottom-nav");
|
|
42
|
+
const navItems = document.querySelectorAll(".nav-item");
|
|
43
|
+
const tabBtns = document.querySelectorAll(".tab-btn");
|
|
44
|
+
const tabPanels = document.querySelectorAll(".tab-panel");
|
|
45
|
+
|
|
46
|
+
// WiFi Form Inputs & PIN
|
|
47
|
+
const inputIp = document.getElementById("ip-address");
|
|
48
|
+
const inputPort = document.getElementById("port-number");
|
|
49
|
+
const inputPin = document.getElementById("connect-pin");
|
|
50
|
+
const inputDevice = document.getElementById("device-name");
|
|
51
|
+
const statusDot = document.querySelector(".connection-status .status-indicator");
|
|
52
|
+
const statusText = document.getElementById("txt-status-detail");
|
|
53
|
+
const btnConnectWifi = document.getElementById("btn-connect-wifi");
|
|
54
|
+
const btnConnectUsb = document.getElementById("btn-connect-usb");
|
|
55
|
+
const btnInstallPwa = document.getElementById("btn-install-pwa");
|
|
56
|
+
|
|
57
|
+
// WiFi Accordion Elements
|
|
58
|
+
const btnToggleWifiOptions = document.getElementById("btn-toggle-wifi-options");
|
|
59
|
+
const wifiAdvancedOptions = document.getElementById("wifi-advanced-options");
|
|
60
|
+
|
|
61
|
+
// Keypad Accordion Elements
|
|
62
|
+
const btnTogglePrimaryActions = document.getElementById("btn-toggle-primary-actions");
|
|
63
|
+
const contentPrimaryActions = document.getElementById("content-primary-actions");
|
|
64
|
+
|
|
65
|
+
// Bluetooth Screen
|
|
66
|
+
const btnScanBluetooth = document.getElementById("btn-scan-bluetooth");
|
|
67
|
+
const btnConnectBtPan = document.getElementById("btn-connect-bt-pan");
|
|
68
|
+
const bluetoothDevices = document.querySelectorAll(".device-item");
|
|
69
|
+
|
|
70
|
+
// Mouse Controller UI
|
|
71
|
+
const trackpadArea = document.getElementById("trackpad-area");
|
|
72
|
+
const touchGlowCursor = document.getElementById("touch-glow-cursor");
|
|
73
|
+
const lblDeviceTitle = document.getElementById("lbl-device-title");
|
|
74
|
+
const lblDeviceAddress = document.getElementById("lbl-device-address");
|
|
75
|
+
const lblPillState = document.getElementById("lbl-pill-state");
|
|
76
|
+
const btnLeftClick = document.getElementById("btn-left-click");
|
|
77
|
+
const btnRightClick = document.getElementById("btn-right-click");
|
|
78
|
+
const btnScrollUp = document.getElementById("btn-scroll-up");
|
|
79
|
+
const btnScrollDown = document.getElementById("btn-scroll-down");
|
|
80
|
+
const scrollNotch = document.getElementById("scroll-wheel-notch");
|
|
81
|
+
const btnQuickKeyboard = document.getElementById("btn-quick-keyboard");
|
|
82
|
+
|
|
83
|
+
// Dedicated Keyboard & TextPad UI
|
|
84
|
+
const kbLiveInput = document.getElementById("kb-live-input");
|
|
85
|
+
const btnLiveBackspace = document.getElementById("btn-live-backspace");
|
|
86
|
+
const kbTextpadInput = document.getElementById("kb-textpad-input");
|
|
87
|
+
const btnSendTextpad = document.getElementById("btn-send-textpad");
|
|
88
|
+
const btnTextpadClear = document.getElementById("btn-textpad-clear");
|
|
89
|
+
const btnClearAllText = document.getElementById("btn-clear-all-text");
|
|
90
|
+
const lblKbStatus = document.getElementById("lbl-kb-status");
|
|
91
|
+
const pillLiveTransmitting = document.getElementById("pill-live-transmitting");
|
|
92
|
+
const keycodeBtns = document.querySelectorAll(".keycode-btn");
|
|
93
|
+
|
|
94
|
+
// Unified Keyboard Mode Toggles
|
|
95
|
+
const btnModeLive = document.getElementById("btn-mode-live");
|
|
96
|
+
const btnModeTextpad = document.getElementById("btn-mode-textpad");
|
|
97
|
+
const viewModeLive = document.getElementById("view-mode-live");
|
|
98
|
+
const viewModeTextpad = document.getElementById("view-mode-textpad");
|
|
99
|
+
|
|
100
|
+
// Settings UI
|
|
101
|
+
const sliderDpi = document.getElementById("slider-dpi");
|
|
102
|
+
const lblDpiValue = document.getElementById("lbl-dpi-value");
|
|
103
|
+
const selectSessionTimeout = document.getElementById("select-session-timeout");
|
|
104
|
+
const btnReconnect = document.getElementById("btn-menu-reconnect");
|
|
105
|
+
const btnDisconnect = document.getElementById("btn-action-disconnect");
|
|
106
|
+
let clientSessionTimer = null;
|
|
107
|
+
|
|
108
|
+
// File Transfer UI
|
|
109
|
+
const btnTriggerUpload = document.getElementById("btn-trigger-upload");
|
|
110
|
+
const fileUploadInput = document.getElementById("file-upload-input");
|
|
111
|
+
const uploadProgressContainer = document.getElementById("upload-progress-container");
|
|
112
|
+
const uploadFilename = document.getElementById("upload-filename");
|
|
113
|
+
const uploadPercent = document.getElementById("upload-percent");
|
|
114
|
+
const uploadProgressFill = document.getElementById("upload-progress-fill");
|
|
115
|
+
const btnRefreshFiles = document.getElementById("btn-refresh-files");
|
|
116
|
+
const fileListContainer = document.getElementById("file-list-container");
|
|
117
|
+
|
|
118
|
+
// --- PWA Service Worker Registration & Install Prompt ---
|
|
119
|
+
if ("serviceWorker" in navigator) {
|
|
120
|
+
window.addEventListener("load", () => {
|
|
121
|
+
navigator.serviceWorker.register("./sw.js")
|
|
122
|
+
.then((reg) => {
|
|
123
|
+
console.log("PWA Service Worker registered:", reg.scope);
|
|
124
|
+
})
|
|
125
|
+
.catch((err) => {
|
|
126
|
+
console.warn("Service Worker registration failed:", err);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
window.addEventListener("beforeinstallprompt", (e) => {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
deferredPrompt = e;
|
|
134
|
+
if (btnInstallPwa) {
|
|
135
|
+
btnInstallPwa.classList.remove("hidden");
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (btnInstallPwa) {
|
|
140
|
+
btnInstallPwa.addEventListener("click", async () => {
|
|
141
|
+
if (deferredPrompt) {
|
|
142
|
+
deferredPrompt.prompt();
|
|
143
|
+
const { outcome } = await deferredPrompt.userChoice;
|
|
144
|
+
if (outcome === "accepted") {
|
|
145
|
+
showToast("Installing Virtual Mouse App...");
|
|
146
|
+
}
|
|
147
|
+
deferredPrompt = null;
|
|
148
|
+
btnInstallPwa.classList.add("hidden");
|
|
149
|
+
} else {
|
|
150
|
+
showToast("App install prompt is ready in browser menu!");
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --- Read URL Parameters for 1-Click Connection & PIN ---
|
|
156
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
157
|
+
const paramIp = urlParams.get("ip");
|
|
158
|
+
const paramPort = urlParams.get("port");
|
|
159
|
+
const paramPin = urlParams.get("code") || urlParams.get("pin");
|
|
160
|
+
|
|
161
|
+
// Restore saved settings or detected values
|
|
162
|
+
const savedHost = window.localStorage.getItem("virtualMouse.lastConnectedIp") || "";
|
|
163
|
+
const savedPort = window.localStorage.getItem("virtualMouse.lastConnectedPort") || "5001";
|
|
164
|
+
const savedPin = window.localStorage.getItem("virtualMouse.lastConnectedPin") || "";
|
|
165
|
+
|
|
166
|
+
const detectedHost = window.location.hostname || "127.0.0.1";
|
|
167
|
+
const isActualNetworkIP = /^\d{1,3}(\.\d{1,3}){3}$/.test(detectedHost) && detectedHost !== "127.0.0.1";
|
|
168
|
+
|
|
169
|
+
let initialIp = paramIp;
|
|
170
|
+
if (!initialIp) {
|
|
171
|
+
if (isActualNetworkIP) {
|
|
172
|
+
// When opened on phone from PC server (e.g. http://10.145.195.86:5000), always use that IP!
|
|
173
|
+
initialIp = detectedHost;
|
|
174
|
+
} else if (savedHost && savedHost !== "127.0.0.1" && savedHost !== "localhost") {
|
|
175
|
+
initialIp = savedHost;
|
|
176
|
+
} else {
|
|
177
|
+
initialIp = detectedHost === "localhost" ? "127.0.0.1" : (detectedHost || "");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
inputIp.value = initialIp;
|
|
182
|
+
inputPort.value = paramPort || savedPort || "5001";
|
|
183
|
+
inputPin.value = paramPin || savedPin || "";
|
|
184
|
+
|
|
185
|
+
// Restore saved Connection Session Timeout (default 1 Hour / 60 minutes)
|
|
186
|
+
const savedTimeout = window.localStorage.getItem("virtualMouse.sessionTimeout") || "60";
|
|
187
|
+
if (selectSessionTimeout) {
|
|
188
|
+
selectSessionTimeout.value = savedTimeout;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function stopClientSessionTimer() {
|
|
192
|
+
if (clientSessionTimer) {
|
|
193
|
+
clearTimeout(clientSessionTimer);
|
|
194
|
+
clientSessionTimer = null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function startClientSessionTimer(timeoutMins) {
|
|
199
|
+
stopClientSessionTimer();
|
|
200
|
+
if (timeoutMins > 0) {
|
|
201
|
+
clientSessionTimer = setTimeout(() => {
|
|
202
|
+
console.warn(`Connection session timed out (${timeoutMins} mins)`);
|
|
203
|
+
isUserDisconnect = true;
|
|
204
|
+
if (socket) { try { socket.close(); } catch (e) {} }
|
|
205
|
+
updateConnectionUI("disconnected", "Session Timed Out");
|
|
206
|
+
const label = timeoutMins >= 60 ? `${timeoutMins / 60} hour(s)` : `${timeoutMins} mins`;
|
|
207
|
+
showToast(`Session timed out (${label} limit reached). Reconnect whenever ready!`);
|
|
208
|
+
navigateTo("screen-home");
|
|
209
|
+
}, timeoutMins * 60 * 1000);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function updateActiveSessionTimeout(timeoutMins) {
|
|
214
|
+
window.localStorage.setItem("virtualMouse.sessionTimeout", String(timeoutMins));
|
|
215
|
+
if (isConnected && socket && socket.readyState === WebSocket.OPEN) {
|
|
216
|
+
socket.send(JSON.stringify({
|
|
217
|
+
type: "set_session_timeout",
|
|
218
|
+
timeoutMins: timeoutMins
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
if (isConnected) {
|
|
222
|
+
startClientSessionTimer(timeoutMins);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (selectSessionTimeout) {
|
|
227
|
+
selectSessionTimeout.addEventListener("change", () => {
|
|
228
|
+
const mins = parseInt(selectSessionTimeout.value, 10) || 0;
|
|
229
|
+
updateActiveSessionTimeout(mins);
|
|
230
|
+
const label = mins === 0 ? "Disabled (Unlimited)" : (mins >= 60 ? `${mins / 60} Hour(s)` : `${mins} Mins`);
|
|
231
|
+
showToast(`Session Timeout set to: ${label}`);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Auto-connect on load if URL params or saved pairing credentials are present
|
|
236
|
+
const targetAutoIp = paramIp || savedHost;
|
|
237
|
+
const targetAutoPin = paramPin || savedPin;
|
|
238
|
+
const targetAutoPort = paramPort || savedPort || "5000";
|
|
239
|
+
|
|
240
|
+
if (targetAutoIp && targetAutoPin) {
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
if (!isConnected && !isUserDisconnect) {
|
|
243
|
+
connectToServer(targetAutoIp, targetAutoPort, targetAutoPin);
|
|
244
|
+
}
|
|
245
|
+
}, 800);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
// ================= Navigation & View Management =================
|
|
250
|
+
|
|
251
|
+
let splashTimeout = setTimeout(() => {
|
|
252
|
+
navigateTo("screen-home");
|
|
253
|
+
}, 2000);
|
|
254
|
+
|
|
255
|
+
screens.splash.addEventListener("click", () => {
|
|
256
|
+
clearTimeout(splashTimeout);
|
|
257
|
+
navigateTo("screen-home");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
function navigateTo(targetScreenId) {
|
|
261
|
+
if (targetScreenId === "screen-splash") {
|
|
262
|
+
bottomNav.classList.add("hidden");
|
|
263
|
+
} else {
|
|
264
|
+
bottomNav.classList.remove("hidden");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
Object.entries(screens).forEach(([key, screenEl]) => {
|
|
268
|
+
if (screenEl && screenEl.id === targetScreenId) {
|
|
269
|
+
screenEl.classList.add("active");
|
|
270
|
+
} else if (screenEl) {
|
|
271
|
+
screenEl.classList.remove("active");
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
navItems.forEach(item => {
|
|
276
|
+
const itemScreen = item.getAttribute("data-screen");
|
|
277
|
+
if (itemScreen === targetScreenId ||
|
|
278
|
+
((targetScreenId === "screen-how-to-connect" || targetScreenId === "screen-troubleshooting") && itemScreen === "screen-settings")) {
|
|
279
|
+
item.classList.add("active");
|
|
280
|
+
} else {
|
|
281
|
+
item.classList.remove("active");
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Bottom Navigation Bar
|
|
287
|
+
navItems.forEach(item => {
|
|
288
|
+
item.addEventListener("click", () => {
|
|
289
|
+
const target = item.getAttribute("data-screen");
|
|
290
|
+
navigateTo(target);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Quick switch from Mouse screen to Keyboard screen
|
|
295
|
+
if (btnQuickKeyboard) {
|
|
296
|
+
btnQuickKeyboard.addEventListener("click", () => {
|
|
297
|
+
navigateTo("screen-keyboard");
|
|
298
|
+
if (kbLiveInput) kbLiveInput.focus();
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Tab Switcher (WiFi / Bluetooth / USB)
|
|
303
|
+
tabBtns.forEach(btn => {
|
|
304
|
+
btn.addEventListener("click", () => {
|
|
305
|
+
tabBtns.forEach(b => b.classList.remove("active"));
|
|
306
|
+
tabPanels.forEach(p => p.classList.remove("active"));
|
|
307
|
+
|
|
308
|
+
btn.classList.add("active");
|
|
309
|
+
const tabId = btn.getAttribute("data-tab");
|
|
310
|
+
const panel = document.getElementById(`panel-${tabId}`);
|
|
311
|
+
if (panel) panel.classList.add("active");
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// ================= Connection Protocols & PIN Handshake =================
|
|
316
|
+
|
|
317
|
+
// --- Outside Expandable Card Details Sync & Accordion ---
|
|
318
|
+
// (Removed connected card sync logic)
|
|
319
|
+
|
|
320
|
+
function updateConnectionUI(state, customMessage) {
|
|
321
|
+
statusDot.className = "status-indicator";
|
|
322
|
+
|
|
323
|
+
if (state === "disconnected") {
|
|
324
|
+
isConnected = false;
|
|
325
|
+
isSimulatorMode = false;
|
|
326
|
+
statusDot.classList.add("disconnect-state");
|
|
327
|
+
statusText.textContent = customMessage || "Not Connected";
|
|
328
|
+
btnConnectWifi.innerHTML = `<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" class="btn-icon"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg> Start & Connect`;
|
|
329
|
+
btnConnectWifi.disabled = false;
|
|
330
|
+
if (lblPillState) lblPillState.textContent = "Offline";
|
|
331
|
+
if (lblKbStatus) lblKbStatus.textContent = "Offline (Interactive)";
|
|
332
|
+
} else if (state === "connecting") {
|
|
333
|
+
statusDot.classList.add("connecting-state");
|
|
334
|
+
statusText.textContent = "Authenticating & Connecting...";
|
|
335
|
+
btnConnectWifi.innerHTML = `<span class="status-indicator connecting-state" style="margin-right:8px;box-shadow:none;"></span> Connecting...`;
|
|
336
|
+
btnConnectWifi.disabled = true;
|
|
337
|
+
} else if (state === "connected") {
|
|
338
|
+
isConnected = true;
|
|
339
|
+
statusDot.classList.add("connect-state");
|
|
340
|
+
statusText.textContent = `Connected to ${lblDeviceTitle.textContent}`;
|
|
341
|
+
btnConnectWifi.innerHTML = `Connected`;
|
|
342
|
+
btnConnectWifi.disabled = false;
|
|
343
|
+
if (lblPillState) lblPillState.textContent = "Active";
|
|
344
|
+
if (lblKbStatus) lblKbStatus.textContent = "Live connected to PC";
|
|
345
|
+
} else if (state === "simulated") {
|
|
346
|
+
isConnected = false;
|
|
347
|
+
isSimulatorMode = true;
|
|
348
|
+
statusDot.className = "status-indicator connect-state";
|
|
349
|
+
statusText.textContent = "Interactive Mode";
|
|
350
|
+
btnConnectWifi.innerHTML = `Running Interactive`;
|
|
351
|
+
btnConnectWifi.disabled = false;
|
|
352
|
+
if (lblPillState) lblPillState.textContent = "Interactive";
|
|
353
|
+
if (lblKbStatus) lblKbStatus.textContent = "Interactive Mode";
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// --- Expandable Card & Accordion Event Handlers ---
|
|
358
|
+
if (btnToggleWifiOptions && wifiAdvancedOptions) {
|
|
359
|
+
btnToggleWifiOptions.addEventListener("click", () => {
|
|
360
|
+
btnToggleWifiOptions.classList.toggle("is-expanded");
|
|
361
|
+
wifiAdvancedOptions.classList.toggle("is-expanded");
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (btnTogglePrimaryActions && contentPrimaryActions) {
|
|
366
|
+
btnTogglePrimaryActions.addEventListener("click", () => {
|
|
367
|
+
btnTogglePrimaryActions.classList.toggle("is-expanded");
|
|
368
|
+
contentPrimaryActions.classList.toggle("is-expanded");
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// --- USB Connect Button Handler ---
|
|
373
|
+
if (btnConnectUsb) {
|
|
374
|
+
btnConnectUsb.addEventListener("click", () => {
|
|
375
|
+
const pin = inputPin.value.trim();
|
|
376
|
+
if (!pin) {
|
|
377
|
+
showToast("Please enter the Connect PIN in the WiFi tab first!");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
// If they are on USB Tethering, window.location.hostname is the PC's USB IP.
|
|
381
|
+
// If they are using ADB Reverse, they likely loaded the page via 127.0.0.1 or localhost.
|
|
382
|
+
let usbIp = window.location.hostname;
|
|
383
|
+
if (!usbIp || usbIp === "") {
|
|
384
|
+
usbIp = "127.0.0.1";
|
|
385
|
+
}
|
|
386
|
+
const port = inputPort.value.trim() || "5001";
|
|
387
|
+
|
|
388
|
+
showToast("Attempting USB Connection...");
|
|
389
|
+
connectToServer(usbIp, port, pin);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// --- Bluetooth Button Handlers ---
|
|
394
|
+
if (btnScanBluetooth) {
|
|
395
|
+
btnScanBluetooth.addEventListener("click", () => {
|
|
396
|
+
showToast("Web Browsers do not support TCP connections over standard Bluetooth! Please use 'Bluetooth Network (PAN)' below instead.", 4000);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (btnConnectBtPan) {
|
|
401
|
+
btnConnectBtPan.addEventListener("click", () => {
|
|
402
|
+
const pin = inputPin.value.trim();
|
|
403
|
+
if (!pin) {
|
|
404
|
+
showToast("Please enter the Connect PIN in the WiFi tab first!");
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
let btIp = window.location.hostname;
|
|
408
|
+
if (!btIp || btIp === "" || btIp === "localhost" || btIp === "127.0.0.1") {
|
|
409
|
+
showToast("Cannot auto-detect Bluetooth IP. Please enter it manually in the WiFi tab.");
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const port = inputPort.value.trim() || "5001";
|
|
413
|
+
|
|
414
|
+
showToast("Attempting Bluetooth PAN Connection...");
|
|
415
|
+
connectToServer(btIp, port, pin);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Initial sync on load
|
|
420
|
+
|
|
421
|
+
// --- Keep-Alive Ping & Auto-Disconnect Helpers ---
|
|
422
|
+
let lastPongReceivedTime = Date.now();
|
|
423
|
+
|
|
424
|
+
function startHeartbeat() {
|
|
425
|
+
stopHeartbeat();
|
|
426
|
+
lastPongReceivedTime = Date.now();
|
|
427
|
+
heartbeatTimer = setInterval(() => {
|
|
428
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
429
|
+
// If no pong or traffic received in 15s, auto-disconnect cleanly
|
|
430
|
+
if (Date.now() - lastPongReceivedTime > 15000) {
|
|
431
|
+
console.warn("Heartbeat timeout: Auto-disconnecting stale connection");
|
|
432
|
+
stopHeartbeat();
|
|
433
|
+
try { socket.close(); } catch (e) {}
|
|
434
|
+
updateConnectionUI("disconnected", "Auto-Disconnected (Lost Connection)");
|
|
435
|
+
showToast("Disconnected: PC server lost / timed out.");
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
socket.send(JSON.stringify({ type: "ping", timestamp: Date.now() }));
|
|
439
|
+
}
|
|
440
|
+
}, 5000); // Send ping every 5 seconds
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function stopHeartbeat() {
|
|
444
|
+
if (heartbeatTimer) {
|
|
445
|
+
clearInterval(heartbeatTimer);
|
|
446
|
+
heartbeatTimer = null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function scheduleAutoReconnect(ip, port, pin) {
|
|
451
|
+
if (autoReconnectTimer) clearTimeout(autoReconnectTimer);
|
|
452
|
+
autoReconnectTimer = setTimeout(() => {
|
|
453
|
+
if (!isConnected && !isUserDisconnect) {
|
|
454
|
+
connectToServer(ip, port, pin);
|
|
455
|
+
}
|
|
456
|
+
}, 3000);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function connectToServer(ip, port, pin) {
|
|
460
|
+
// Auto-fix: If opened on mobile and IP was set to 127.0.0.1, use the actual PC host IP
|
|
461
|
+
const currentHost = window.location.hostname;
|
|
462
|
+
if ((!ip || ip === "127.0.0.1" || ip === "localhost") && currentHost && currentHost !== "localhost" && currentHost !== "127.0.0.1") {
|
|
463
|
+
ip = currentHost;
|
|
464
|
+
if (inputIp) inputIp.value = ip;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (autoReconnectTimer) {
|
|
468
|
+
clearTimeout(autoReconnectTimer);
|
|
469
|
+
autoReconnectTimer = null;
|
|
470
|
+
}
|
|
471
|
+
stopHeartbeat();
|
|
472
|
+
if (socket) {
|
|
473
|
+
try { socket.close(); } catch (e) {}
|
|
474
|
+
socket = null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
updateConnectionUI("connecting");
|
|
478
|
+
|
|
479
|
+
const connectTimeout = setTimeout(() => {
|
|
480
|
+
if (!isConnected) {
|
|
481
|
+
if (socket) socket.close();
|
|
482
|
+
updateConnectionUI("disconnected", "Connection Timed Out");
|
|
483
|
+
showToast("Could not connect to PC server.");
|
|
484
|
+
}
|
|
485
|
+
}, 4000);
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
const wsPort = port || "5001";
|
|
489
|
+
const isHttps = window.location.protocol === "https:";
|
|
490
|
+
const wsProtocol = isHttps ? "wss" : "ws";
|
|
491
|
+
|
|
492
|
+
socket = new WebSocket(`${wsProtocol}://${ip}:${wsPort}`);
|
|
493
|
+
|
|
494
|
+
socket.onopen = () => {
|
|
495
|
+
const codeToSend = pin || inputPin.value.trim();
|
|
496
|
+
if (!codeToSend) {
|
|
497
|
+
showToast("Please enter the Connect PIN first.");
|
|
498
|
+
socket.close();
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
socket.send(JSON.stringify({
|
|
502
|
+
type: "auth",
|
|
503
|
+
code: codeToSend
|
|
504
|
+
}));
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
socket.onmessage = (event) => {
|
|
508
|
+
lastPongReceivedTime = Date.now();
|
|
509
|
+
try {
|
|
510
|
+
const data = JSON.parse(event.data);
|
|
511
|
+
if (data.type === "pong") {
|
|
512
|
+
// Heartbeat ack acknowledged
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (data.type === "session_timeout") {
|
|
516
|
+
isUserDisconnect = true;
|
|
517
|
+
stopHeartbeat();
|
|
518
|
+
stopClientSessionTimer();
|
|
519
|
+
if (socket) { try { socket.close(); } catch(e){} }
|
|
520
|
+
updateConnectionUI("disconnected", "Session Timed Out");
|
|
521
|
+
showToast(data.message || "Connection session timed out. Reconnect whenever ready!");
|
|
522
|
+
navigateTo("screen-home");
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (data.type === "auth_result") {
|
|
526
|
+
if (data.status === "success") {
|
|
527
|
+
clearTimeout(connectTimeout);
|
|
528
|
+
|
|
529
|
+
window.localStorage.setItem("virtualMouse.lastConnectedIp", ip);
|
|
530
|
+
window.localStorage.setItem("virtualMouse.lastConnectedPort", wsPort);
|
|
531
|
+
if (pin) window.localStorage.setItem("virtualMouse.lastConnectedPin", pin);
|
|
532
|
+
|
|
533
|
+
const devName = inputDevice.value || "My PC";
|
|
534
|
+
lblDeviceTitle.textContent = devName;
|
|
535
|
+
lblDeviceAddress.textContent = `${ip}:${wsPort}`;
|
|
536
|
+
|
|
537
|
+
updateConnectionUI("connected");
|
|
538
|
+
startHeartbeat();
|
|
539
|
+
|
|
540
|
+
// Start session timeout timer based on user configured setting (default 1 Hour / 60 mins)
|
|
541
|
+
const currentTimeoutMins = parseInt(selectSessionTimeout ? selectSessionTimeout.value : "60", 10) || 60;
|
|
542
|
+
updateActiveSessionTimeout(currentTimeoutMins);
|
|
543
|
+
|
|
544
|
+
isUserDisconnect = false;
|
|
545
|
+
showToast("Connected & Paired with PC successfully!");
|
|
546
|
+
|
|
547
|
+
setTimeout(() => {
|
|
548
|
+
navigateTo("screen-mouse");
|
|
549
|
+
}, 600);
|
|
550
|
+
} else {
|
|
551
|
+
clearTimeout(connectTimeout);
|
|
552
|
+
showToast(data.message || "Invalid Connect PIN!");
|
|
553
|
+
updateConnectionUI("disconnected", "Invalid PIN / Code");
|
|
554
|
+
if (socket) socket.close();
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
} catch (err) {
|
|
558
|
+
console.error("[WS] Failed to parse message:", err);
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
socket.onerror = () => {
|
|
564
|
+
// Error handled by timeout
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
socket.onclose = () => {
|
|
568
|
+
clearTimeout(connectTimeout);
|
|
569
|
+
stopHeartbeat();
|
|
570
|
+
stopClientSessionTimer();
|
|
571
|
+
if (isConnected) {
|
|
572
|
+
updateConnectionUI("disconnected");
|
|
573
|
+
if (!isUserDisconnect) {
|
|
574
|
+
showToast("Connection lost. Reconnecting...");
|
|
575
|
+
scheduleAutoReconnect(ip, wsPort, pin);
|
|
576
|
+
} else {
|
|
577
|
+
showToast("Connection to PC closed.");
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
} catch (e) {
|
|
582
|
+
clearTimeout(connectTimeout);
|
|
583
|
+
stopHeartbeat();
|
|
584
|
+
console.warn("[WS] WebSocket construction failed:", e);
|
|
585
|
+
startSimulatorFallback("Cannot connect directly. Running Interactive Mode.");
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function startSimulatorFallback(reason) {
|
|
590
|
+
lblDeviceTitle.textContent = `${inputDevice.value || "My PC"} (Interactive)`;
|
|
591
|
+
lblDeviceAddress.textContent = `${inputIp.value || "127.0.0.1"}:${inputPort.value || "5001"}`;
|
|
592
|
+
|
|
593
|
+
updateConnectionUI("simulated");
|
|
594
|
+
showToast(reason || "Opened in Interactive Mode!");
|
|
595
|
+
|
|
596
|
+
setTimeout(() => {
|
|
597
|
+
navigateTo("screen-mouse");
|
|
598
|
+
}, 600);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function sendMessage(msgObj) {
|
|
602
|
+
// Attach PIN if available
|
|
603
|
+
const currentPin = inputPin.value.trim();
|
|
604
|
+
if (currentPin) msgObj.code = currentPin;
|
|
605
|
+
|
|
606
|
+
if (isConnected && socket && socket.readyState === WebSocket.OPEN) {
|
|
607
|
+
socket.send(JSON.stringify(msgObj));
|
|
608
|
+
} else if (isSimulatorMode) {
|
|
609
|
+
// Visual tactile feedback in interactive mode
|
|
610
|
+
if (msgObj.type === "text") {
|
|
611
|
+
showToast(`Transmitted: "${msgObj.text}"`);
|
|
612
|
+
} else if (msgObj.type === "keycode" || msgObj.type === "key") {
|
|
613
|
+
showToast(`Keycode: [${msgObj.key.toUpperCase()}]`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Option 1: Connect button
|
|
619
|
+
btnConnectWifi.addEventListener("click", () => {
|
|
620
|
+
if (isConnected) {
|
|
621
|
+
disconnectDevice();
|
|
622
|
+
} else {
|
|
623
|
+
const ip = inputIp.value.trim() || "127.0.0.1";
|
|
624
|
+
const port = inputPort.value.trim() || "5001";
|
|
625
|
+
const pin = inputPin.value.trim();
|
|
626
|
+
connectToServer(ip, port, pin);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
if (btnConnectBtPan) {
|
|
631
|
+
btnConnectBtPan.addEventListener("click", () => {
|
|
632
|
+
// Connect to the PC's Bluetooth adapter IP
|
|
633
|
+
const btIp = window.location.hostname || inputIp.value.trim() || "127.0.0.1";
|
|
634
|
+
updateConnectionUI("connecting");
|
|
635
|
+
showToast("Connecting via Bluetooth Network...");
|
|
636
|
+
connectToServer(btIp, inputPort.value.trim() || "5001", inputPin.value.trim());
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Bluetooth devices list items
|
|
641
|
+
const btItems = document.querySelectorAll("#bt-device-list .device-item");
|
|
642
|
+
btItems.forEach(device => {
|
|
643
|
+
device.addEventListener("click", () => {
|
|
644
|
+
const name = device.getAttribute("data-name") || "Windows PC (Bluetooth)";
|
|
645
|
+
const addr = device.getAttribute("data-ip") || window.location.hostname || "127.0.0.1";
|
|
646
|
+
lblDeviceTitle.textContent = name;
|
|
647
|
+
lblDeviceAddress.textContent = addr;
|
|
648
|
+
connectToServer(addr, inputPort.value.trim() || "5001", inputPin.value.trim());
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
function disconnectDevice() {
|
|
653
|
+
isUserDisconnect = true;
|
|
654
|
+
if (autoReconnectTimer) {
|
|
655
|
+
clearTimeout(autoReconnectTimer);
|
|
656
|
+
autoReconnectTimer = null;
|
|
657
|
+
}
|
|
658
|
+
stopHeartbeat();
|
|
659
|
+
if (socket) {
|
|
660
|
+
try { socket.close(1000, "User disconnected"); } catch (e) {}
|
|
661
|
+
socket = null;
|
|
662
|
+
}
|
|
663
|
+
updateConnectionUI("disconnected");
|
|
664
|
+
showToast("Disconnected.");
|
|
665
|
+
navigateTo("screen-home");
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
btnDisconnect.addEventListener("click", disconnectDevice);
|
|
669
|
+
|
|
670
|
+
// --- Persistent Connection: Auto-Reconnect when returning to App / Tab ---
|
|
671
|
+
document.addEventListener("visibilitychange", () => {
|
|
672
|
+
if (document.visibilityState === "visible") {
|
|
673
|
+
if (!isConnected && !isUserDisconnect) {
|
|
674
|
+
const ip = window.localStorage.getItem("virtualMouse.lastConnectedIp") || inputIp.value.trim();
|
|
675
|
+
const port = window.localStorage.getItem("virtualMouse.lastConnectedPort") || inputPort.value.trim() || "5000";
|
|
676
|
+
const pin = window.localStorage.getItem("virtualMouse.lastConnectedPin") || inputPin.value.trim();
|
|
677
|
+
if (ip && pin) {
|
|
678
|
+
showToast("Restoring connection to PC...");
|
|
679
|
+
connectToServer(ip, port, pin);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
window.addEventListener("online", () => {
|
|
686
|
+
if (!isConnected && !isUserDisconnect) {
|
|
687
|
+
const ip = window.localStorage.getItem("virtualMouse.lastConnectedIp") || inputIp.value.trim();
|
|
688
|
+
const port = window.localStorage.getItem("virtualMouse.lastConnectedPort") || inputPort.value.trim() || "5000";
|
|
689
|
+
const pin = window.localStorage.getItem("virtualMouse.lastConnectedPin") || inputPin.value.trim();
|
|
690
|
+
if (ip && pin) {
|
|
691
|
+
connectToServer(ip, port, pin);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
// ================= Trackpad Gestures Handler =================
|
|
698
|
+
|
|
699
|
+
trackpadArea.addEventListener("touchstart", (e) => {
|
|
700
|
+
e.preventDefault();
|
|
701
|
+
touchStartTimestamp = Date.now();
|
|
702
|
+
|
|
703
|
+
if (e.touches.length === 1) {
|
|
704
|
+
lastX = e.touches[0].clientX;
|
|
705
|
+
lastY = e.touches[0].clientY;
|
|
706
|
+
isMoving = true;
|
|
707
|
+
isTwoFingerScrolling = false;
|
|
708
|
+
|
|
709
|
+
const rect = trackpadArea.getBoundingClientRect();
|
|
710
|
+
touchGlowCursor.style.left = `${lastX - rect.left}px`;
|
|
711
|
+
touchGlowCursor.style.top = `${lastY - rect.top}px`;
|
|
712
|
+
touchGlowCursor.classList.add("active");
|
|
713
|
+
} else if (e.touches.length === 2) {
|
|
714
|
+
isMoving = false;
|
|
715
|
+
isTwoFingerScrolling = true;
|
|
716
|
+
scrollLastY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
|
|
717
|
+
touchGlowCursor.classList.remove("active");
|
|
718
|
+
}
|
|
719
|
+
}, { passive: false });
|
|
720
|
+
|
|
721
|
+
trackpadArea.addEventListener("touchmove", (e) => {
|
|
722
|
+
e.preventDefault();
|
|
723
|
+
|
|
724
|
+
if (isMoving && e.touches.length === 1) {
|
|
725
|
+
const currentX = e.touches[0].clientX;
|
|
726
|
+
const currentY = e.touches[0].clientY;
|
|
727
|
+
|
|
728
|
+
let deltaX = (currentX - lastX) * sensitivity;
|
|
729
|
+
let deltaY = (currentY - lastY) * sensitivity;
|
|
730
|
+
|
|
731
|
+
sendMessage({
|
|
732
|
+
type: "move",
|
|
733
|
+
dx: deltaX,
|
|
734
|
+
dy: deltaY
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
lastX = currentX;
|
|
738
|
+
lastY = currentY;
|
|
739
|
+
|
|
740
|
+
const rect = trackpadArea.getBoundingClientRect();
|
|
741
|
+
touchGlowCursor.style.left = `${currentX - rect.left}px`;
|
|
742
|
+
touchGlowCursor.style.top = `${currentY - rect.top}px`;
|
|
743
|
+
} else if (isTwoFingerScrolling && e.touches.length === 2) {
|
|
744
|
+
const currentScrollY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
|
|
745
|
+
const deltaScrollY = (currentScrollY - scrollLastY) * scrollSensitivity;
|
|
746
|
+
|
|
747
|
+
if (Math.abs(deltaScrollY) > 1) {
|
|
748
|
+
sendMessage({
|
|
749
|
+
type: "scroll",
|
|
750
|
+
dy: deltaScrollY
|
|
751
|
+
});
|
|
752
|
+
scrollLastY = currentScrollY;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}, { passive: false });
|
|
756
|
+
|
|
757
|
+
trackpadArea.addEventListener("touchend", (e) => {
|
|
758
|
+
e.preventDefault();
|
|
759
|
+
touchGlowCursor.classList.remove("active");
|
|
760
|
+
|
|
761
|
+
const touchDuration = Date.now() - touchStartTimestamp;
|
|
762
|
+
if (touchDuration < 250 && !isTwoFingerScrolling && e.changedTouches.length === 1) {
|
|
763
|
+
sendMessage({
|
|
764
|
+
type: "click",
|
|
765
|
+
button: "left",
|
|
766
|
+
action: "click"
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// Touch ripple visual
|
|
770
|
+
touchGlowCursor.classList.add("active");
|
|
771
|
+
setTimeout(() => touchGlowCursor.classList.remove("active"), 150);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (e.touches.length === 0) {
|
|
775
|
+
isMoving = false;
|
|
776
|
+
isTwoFingerScrolling = false;
|
|
777
|
+
}
|
|
778
|
+
}, { passive: false });
|
|
779
|
+
|
|
780
|
+
// Click Buttons
|
|
781
|
+
function bindClickButton(btnEl, buttonName) {
|
|
782
|
+
btnEl.addEventListener("touchstart", (e) => {
|
|
783
|
+
e.preventDefault();
|
|
784
|
+
btnEl.classList.add("active");
|
|
785
|
+
sendMessage({
|
|
786
|
+
type: "click",
|
|
787
|
+
button: buttonName,
|
|
788
|
+
action: "down"
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
btnEl.addEventListener("touchend", (e) => {
|
|
793
|
+
e.preventDefault();
|
|
794
|
+
btnEl.classList.remove("active");
|
|
795
|
+
sendMessage({
|
|
796
|
+
type: "click",
|
|
797
|
+
button: buttonName,
|
|
798
|
+
action: "up"
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
btnEl.addEventListener("click", () => {
|
|
803
|
+
sendMessage({
|
|
804
|
+
type: "click",
|
|
805
|
+
button: buttonName,
|
|
806
|
+
action: "click"
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
bindClickButton(btnLeftClick, "left");
|
|
812
|
+
bindClickButton(btnRightClick, "right");
|
|
813
|
+
|
|
814
|
+
// ================= Auto-Scroll & Scroll Wheel Controller =================
|
|
815
|
+
let autoScrollInterval = null;
|
|
816
|
+
let holdScrollTimeout = null;
|
|
817
|
+
let isHandsFreeAutoScrolling = false;
|
|
818
|
+
let handsFreeDirection = 0; // 1 for Up, -1 for Down
|
|
819
|
+
|
|
820
|
+
function animateNotch(amt) {
|
|
821
|
+
if (!scrollNotch) return;
|
|
822
|
+
const offset = amt > 0 ? -12 : 12;
|
|
823
|
+
scrollNotch.style.transform = `translateY(calc(-50% + ${offset}px))`;
|
|
824
|
+
setTimeout(() => {
|
|
825
|
+
scrollNotch.style.transform = "translateY(-50%)";
|
|
826
|
+
}, 120);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function startHoldingScroll(direction) {
|
|
830
|
+
stopAutoScroll();
|
|
831
|
+
|
|
832
|
+
// Immediate first step
|
|
833
|
+
const dy = direction * 4;
|
|
834
|
+
sendMessage({ type: "scroll", dy: dy });
|
|
835
|
+
animateNotch(dy);
|
|
836
|
+
|
|
837
|
+
const btn = direction > 0 ? btnScrollUp : btnScrollDown;
|
|
838
|
+
if (btn) btn.classList.add("auto-scrolling");
|
|
839
|
+
|
|
840
|
+
// Start continuous scrolling loop after initial hold threshold
|
|
841
|
+
holdScrollTimeout = setTimeout(() => {
|
|
842
|
+
autoScrollInterval = setInterval(() => {
|
|
843
|
+
sendMessage({ type: "scroll", dy: dy });
|
|
844
|
+
animateNotch(dy);
|
|
845
|
+
}, 60);
|
|
846
|
+
}, 220);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function stopHoldingScroll() {
|
|
850
|
+
if (holdScrollTimeout) {
|
|
851
|
+
clearTimeout(holdScrollTimeout);
|
|
852
|
+
holdScrollTimeout = null;
|
|
853
|
+
}
|
|
854
|
+
if (!isHandsFreeAutoScrolling && autoScrollInterval) {
|
|
855
|
+
clearInterval(autoScrollInterval);
|
|
856
|
+
autoScrollInterval = null;
|
|
857
|
+
}
|
|
858
|
+
if (btnScrollUp) btnScrollUp.classList.remove("auto-scrolling");
|
|
859
|
+
if (btnScrollDown) btnScrollDown.classList.remove("auto-scrolling");
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function toggleHandsFreeAutoScroll(direction) {
|
|
863
|
+
if (isHandsFreeAutoScrolling && handsFreeDirection === direction) {
|
|
864
|
+
stopAutoScroll();
|
|
865
|
+
showToast("Auto-Scroll Stopped");
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
stopAutoScroll();
|
|
870
|
+
isHandsFreeAutoScrolling = true;
|
|
871
|
+
handsFreeDirection = direction;
|
|
872
|
+
|
|
873
|
+
const dy = direction * 3;
|
|
874
|
+
const modeText = direction > 0 ? "AUTO ▲" : "AUTO ▼";
|
|
875
|
+
const lblScroll = document.querySelector(".scroll-label");
|
|
876
|
+
if (lblScroll) lblScroll.textContent = modeText;
|
|
877
|
+
|
|
878
|
+
const activeBtn = direction > 0 ? btnScrollUp : btnScrollDown;
|
|
879
|
+
if (activeBtn) activeBtn.classList.add("hands-free-active");
|
|
880
|
+
|
|
881
|
+
showToast(`Hands-Free Auto-Scroll ${direction > 0 ? "Up" : "Down"} Active`);
|
|
882
|
+
|
|
883
|
+
autoScrollInterval = setInterval(() => {
|
|
884
|
+
sendMessage({ type: "scroll", dy: dy });
|
|
885
|
+
animateNotch(dy);
|
|
886
|
+
}, 70);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function stopAutoScroll() {
|
|
890
|
+
if (holdScrollTimeout) {
|
|
891
|
+
clearTimeout(holdScrollTimeout);
|
|
892
|
+
holdScrollTimeout = null;
|
|
893
|
+
}
|
|
894
|
+
if (autoScrollInterval) {
|
|
895
|
+
clearInterval(autoScrollInterval);
|
|
896
|
+
autoScrollInterval = null;
|
|
897
|
+
}
|
|
898
|
+
isHandsFreeAutoScrolling = false;
|
|
899
|
+
handsFreeDirection = 0;
|
|
900
|
+
|
|
901
|
+
const lblScroll = document.querySelector(".scroll-label");
|
|
902
|
+
if (lblScroll) lblScroll.textContent = "Scroll";
|
|
903
|
+
|
|
904
|
+
if (btnScrollUp) {
|
|
905
|
+
btnScrollUp.classList.remove("auto-scrolling", "hands-free-active");
|
|
906
|
+
}
|
|
907
|
+
if (btnScrollDown) {
|
|
908
|
+
btnScrollDown.classList.remove("auto-scrolling", "hands-free-active");
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function setupScrollArrow(btnEl, direction) {
|
|
913
|
+
if (!btnEl) return;
|
|
914
|
+
let lastTapTime = 0;
|
|
915
|
+
|
|
916
|
+
// Pointer press-and-hold + double tap toggle
|
|
917
|
+
btnEl.addEventListener("pointerdown", (e) => {
|
|
918
|
+
e.preventDefault();
|
|
919
|
+
const now = Date.now();
|
|
920
|
+
if (now - lastTapTime < 300) {
|
|
921
|
+
toggleHandsFreeAutoScroll(direction);
|
|
922
|
+
lastTapTime = 0;
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
lastTapTime = now;
|
|
926
|
+
|
|
927
|
+
if (isHandsFreeAutoScrolling) {
|
|
928
|
+
stopAutoScroll();
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
startHoldingScroll(direction);
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
btnEl.addEventListener("pointerup", (e) => {
|
|
936
|
+
e.preventDefault();
|
|
937
|
+
stopHoldingScroll();
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
btnEl.addEventListener("pointercancel", stopHoldingScroll);
|
|
941
|
+
btnEl.addEventListener("mouseleave", stopHoldingScroll);
|
|
942
|
+
|
|
943
|
+
// Standard click fallback
|
|
944
|
+
btnEl.addEventListener("click", (e) => {
|
|
945
|
+
if (!holdScrollTimeout && !autoScrollInterval) {
|
|
946
|
+
const dy = direction * 4;
|
|
947
|
+
sendMessage({ type: "scroll", dy: dy });
|
|
948
|
+
animateNotch(dy);
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
setupScrollArrow(btnScrollUp, 1);
|
|
954
|
+
setupScrollArrow(btnScrollDown, -1);
|
|
955
|
+
|
|
956
|
+
// Tap on central scroll slider toggles auto-scroll
|
|
957
|
+
const scrollSlider = document.querySelector(".scroll-indicator-slider");
|
|
958
|
+
if (scrollSlider) {
|
|
959
|
+
scrollSlider.addEventListener("click", () => {
|
|
960
|
+
if (isHandsFreeAutoScrolling) {
|
|
961
|
+
stopAutoScroll();
|
|
962
|
+
showToast("Auto-Scroll Stopped");
|
|
963
|
+
} else {
|
|
964
|
+
toggleHandsFreeAutoScroll(-1); // Default auto scroll down
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Stop hands-free auto scroll if user touches trackpad
|
|
970
|
+
trackpadArea.addEventListener("touchstart", () => {
|
|
971
|
+
if (isHandsFreeAutoScrolling) {
|
|
972
|
+
stopAutoScroll();
|
|
973
|
+
}
|
|
974
|
+
}, { passive: true });
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
// Prevent Backspace key from ever navigating back or closing browser tab
|
|
978
|
+
document.addEventListener("keydown", (e) => {
|
|
979
|
+
if (e.key === "Backspace" || e.keyCode === 8) {
|
|
980
|
+
const active = document.activeElement;
|
|
981
|
+
const isInputField = active && (active.tagName === "INPUT" || active.tagName === "TEXTAREA" || active.isContentEditable);
|
|
982
|
+
if (!isInputField) {
|
|
983
|
+
e.preventDefault();
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
// ================= Dedicated Keyboard & TextPad Tools =================
|
|
989
|
+
|
|
990
|
+
function triggerLivePulse() {
|
|
991
|
+
if (pillLiveTransmitting) {
|
|
992
|
+
pillLiveTransmitting.textContent = "Sent ⚡";
|
|
993
|
+
setTimeout(() => {
|
|
994
|
+
pillLiveTransmitting.textContent = "Live Stream ⚡";
|
|
995
|
+
}, 300);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Tool 1: Live Keystroke Auto-Typing (Supports Mobile Soft Keyboards & Backspace)
|
|
1000
|
+
kbLiveInput.addEventListener("keydown", (e) => {
|
|
1001
|
+
if (e.key === "Backspace" || e.keyCode === 8) {
|
|
1002
|
+
e.preventDefault();
|
|
1003
|
+
sendMessage({
|
|
1004
|
+
type: "keycode",
|
|
1005
|
+
key: "backspace"
|
|
1006
|
+
});
|
|
1007
|
+
kbLiveInput.value = "";
|
|
1008
|
+
triggerLivePulse();
|
|
1009
|
+
} else if (e.key === "Enter" || e.keyCode === 13) {
|
|
1010
|
+
e.preventDefault();
|
|
1011
|
+
sendMessage({
|
|
1012
|
+
type: "keycode",
|
|
1013
|
+
key: "enter"
|
|
1014
|
+
});
|
|
1015
|
+
kbLiveInput.value = "";
|
|
1016
|
+
triggerLivePulse();
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
kbLiveInput.addEventListener("beforeinput", (e) => {
|
|
1021
|
+
if (e.inputType === "deleteContentBackward" || e.inputType === "deleteContentForward") {
|
|
1022
|
+
e.preventDefault();
|
|
1023
|
+
sendMessage({
|
|
1024
|
+
type: "keycode",
|
|
1025
|
+
key: "backspace"
|
|
1026
|
+
});
|
|
1027
|
+
kbLiveInput.value = "";
|
|
1028
|
+
triggerLivePulse();
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
kbLiveInput.addEventListener("input", (e) => {
|
|
1033
|
+
if (e.inputType === "deleteContentBackward" || e.inputType === "deleteContentForward") {
|
|
1034
|
+
kbLiveInput.value = "";
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const typedVal = e.target.value;
|
|
1038
|
+
if (typedVal.length > 0) {
|
|
1039
|
+
sendMessage({
|
|
1040
|
+
type: "text",
|
|
1041
|
+
text: typedVal
|
|
1042
|
+
});
|
|
1043
|
+
kbLiveInput.value = "";
|
|
1044
|
+
triggerLivePulse();
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
const btnLiveEnter = document.getElementById("btn-live-enter");
|
|
1049
|
+
if (btnLiveEnter) {
|
|
1050
|
+
btnLiveEnter.addEventListener("click", () => {
|
|
1051
|
+
sendMessage({
|
|
1052
|
+
type: "keycode",
|
|
1053
|
+
key: "enter"
|
|
1054
|
+
});
|
|
1055
|
+
kbLiveInput.focus();
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (btnLiveBackspace) {
|
|
1060
|
+
btnLiveBackspace.addEventListener("click", () => {
|
|
1061
|
+
sendMessage({
|
|
1062
|
+
type: "keycode",
|
|
1063
|
+
key: "backspace"
|
|
1064
|
+
});
|
|
1065
|
+
kbLiveInput.focus();
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Unified Keyboard Mode Toggle Handlers
|
|
1070
|
+
if (btnModeLive && btnModeTextpad) {
|
|
1071
|
+
btnModeLive.addEventListener("click", () => {
|
|
1072
|
+
btnModeLive.classList.add("active");
|
|
1073
|
+
btnModeTextpad.classList.remove("active");
|
|
1074
|
+
viewModeLive.style.display = "block";
|
|
1075
|
+
viewModeTextpad.style.display = "none";
|
|
1076
|
+
if (kbLiveInput) kbLiveInput.focus();
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
btnModeTextpad.addEventListener("click", () => {
|
|
1080
|
+
btnModeTextpad.classList.add("active");
|
|
1081
|
+
btnModeLive.classList.remove("active");
|
|
1082
|
+
viewModeTextpad.style.display = "block";
|
|
1083
|
+
viewModeLive.style.display = "none";
|
|
1084
|
+
if (kbTextpadInput) kbTextpadInput.focus();
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Tool 2: Desktop TextPad (Multi-line block sender)
|
|
1089
|
+
btnSendTextpad.addEventListener("click", () => {
|
|
1090
|
+
const content = kbTextpadInput.value;
|
|
1091
|
+
if (!content || content.trim().length === 0) {
|
|
1092
|
+
showToast("Please enter some text in the TextPad first!");
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
sendMessage({
|
|
1097
|
+
type: "text",
|
|
1098
|
+
text: content
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
showToast(`Sent ${content.length} characters to Desktop TextPad!`);
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
btnTextpadClear.addEventListener("click", () => {
|
|
1105
|
+
kbTextpadInput.value = "";
|
|
1106
|
+
showToast("TextPad cleared");
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
if (btnClearAllText) {
|
|
1110
|
+
btnClearAllText.addEventListener("click", () => {
|
|
1111
|
+
kbLiveInput.value = "";
|
|
1112
|
+
kbTextpadInput.value = "";
|
|
1113
|
+
showToast("All text cleared");
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Tool 3: Keycode Buttons & Modifiers
|
|
1118
|
+
keycodeBtns.forEach(btn => {
|
|
1119
|
+
btn.addEventListener("click", () => {
|
|
1120
|
+
const keyName = btn.getAttribute("data-key");
|
|
1121
|
+
if (!keyName) return;
|
|
1122
|
+
|
|
1123
|
+
// Visual feedback
|
|
1124
|
+
btn.classList.add("pressed");
|
|
1125
|
+
setTimeout(() => btn.classList.remove("pressed"), 180);
|
|
1126
|
+
|
|
1127
|
+
if (navigator.vibrate) {
|
|
1128
|
+
navigator.vibrate(25);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
sendMessage({
|
|
1132
|
+
type: "keycode",
|
|
1133
|
+
key: keyName
|
|
1134
|
+
});
|
|
1135
|
+
});
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
// ================= Settings Screen Logic =================
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
sliderDpi.addEventListener("input", () => {
|
|
1143
|
+
const val = parseInt(sliderDpi.value);
|
|
1144
|
+
if (val === 800) {
|
|
1145
|
+
sensitivity = 0.55;
|
|
1146
|
+
lblDpiValue.textContent = "Slow (800)";
|
|
1147
|
+
} else if (val === 1200) {
|
|
1148
|
+
sensitivity = 0.8;
|
|
1149
|
+
lblDpiValue.textContent = "Low (1200)";
|
|
1150
|
+
} else if (val === 1600) {
|
|
1151
|
+
sensitivity = 1.1;
|
|
1152
|
+
lblDpiValue.textContent = "Medium (1600)";
|
|
1153
|
+
} else if (val === 2000) {
|
|
1154
|
+
sensitivity = 1.45;
|
|
1155
|
+
lblDpiValue.textContent = "Fast (2000)";
|
|
1156
|
+
} else if (val === 2400) {
|
|
1157
|
+
sensitivity = 1.8;
|
|
1158
|
+
lblDpiValue.textContent = "High (2400)";
|
|
1159
|
+
} else if (val === 2800) {
|
|
1160
|
+
sensitivity = 2.15;
|
|
1161
|
+
lblDpiValue.textContent = "Super (2800)";
|
|
1162
|
+
} else if (val === 3200) {
|
|
1163
|
+
sensitivity = 2.5;
|
|
1164
|
+
lblDpiValue.textContent = "Ultra (3200)";
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
btnReconnect.addEventListener("click", () => {
|
|
1169
|
+
const ip = window.localStorage.getItem("virtualMouse.lastConnectedIp") || inputIp.value.trim();
|
|
1170
|
+
const port = window.localStorage.getItem("virtualMouse.lastConnectedPort") || inputPort.value.trim();
|
|
1171
|
+
const pin = window.localStorage.getItem("virtualMouse.lastConnectedPin") || inputPin.value.trim();
|
|
1172
|
+
if (ip && pin) {
|
|
1173
|
+
connectToServer(ip, port, pin);
|
|
1174
|
+
} else {
|
|
1175
|
+
showToast("Please enter IP and Connect PIN on the home screen.");
|
|
1176
|
+
navigateTo("screen-home");
|
|
1177
|
+
}
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
// Dedicated Screen Navigation for How to Connect & Troubleshooting
|
|
1181
|
+
const btnSupportUse = document.getElementById("btn-support-use");
|
|
1182
|
+
if (btnSupportUse) {
|
|
1183
|
+
btnSupportUse.addEventListener("click", () => {
|
|
1184
|
+
navigateTo("screen-how-to-connect");
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const btnSupportFaq = document.getElementById("btn-support-faq");
|
|
1189
|
+
if (btnSupportFaq) {
|
|
1190
|
+
btnSupportFaq.addEventListener("click", () => {
|
|
1191
|
+
navigateTo("screen-troubleshooting");
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const btnBackHowToConnect = document.getElementById("btn-back-how-to-connect");
|
|
1196
|
+
if (btnBackHowToConnect) {
|
|
1197
|
+
btnBackHowToConnect.addEventListener("click", () => {
|
|
1198
|
+
navigateTo("screen-settings");
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
const btnBackTroubleshooting = document.getElementById("btn-back-troubleshooting");
|
|
1203
|
+
if (btnBackTroubleshooting) {
|
|
1204
|
+
btnBackTroubleshooting.addEventListener("click", () => {
|
|
1205
|
+
navigateTo("screen-settings");
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
const btnGuideGoConnect = document.getElementById("btn-guide-go-connect");
|
|
1210
|
+
if (btnGuideGoConnect) {
|
|
1211
|
+
btnGuideGoConnect.addEventListener("click", () => {
|
|
1212
|
+
navigateTo("screen-home");
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const btnTroubleshootReconnect = document.getElementById("btn-troubleshoot-reconnect");
|
|
1217
|
+
if (btnTroubleshootReconnect) {
|
|
1218
|
+
btnTroubleshootReconnect.addEventListener("click", () => {
|
|
1219
|
+
const ip = window.localStorage.getItem("virtualMouse.lastConnectedIp") || inputIp.value.trim();
|
|
1220
|
+
const port = window.localStorage.getItem("virtualMouse.lastConnectedPort") || inputPort.value.trim();
|
|
1221
|
+
const pin = window.localStorage.getItem("virtualMouse.lastConnectedPin") || inputPin.value.trim();
|
|
1222
|
+
if (ip && pin) {
|
|
1223
|
+
connectToServer(ip, port, pin);
|
|
1224
|
+
} else {
|
|
1225
|
+
navigateTo("screen-home");
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
// ================= Utility Toast Notification =================
|
|
1232
|
+
|
|
1233
|
+
function showToast(message) {
|
|
1234
|
+
const activeToast = document.querySelector(".app-toast");
|
|
1235
|
+
if (activeToast) activeToast.remove();
|
|
1236
|
+
|
|
1237
|
+
const toast = document.createElement("div");
|
|
1238
|
+
toast.className = "app-toast";
|
|
1239
|
+
toast.textContent = message;
|
|
1240
|
+
|
|
1241
|
+
toast.style.position = "absolute";
|
|
1242
|
+
toast.style.bottom = "84px";
|
|
1243
|
+
toast.style.left = "50%";
|
|
1244
|
+
toast.style.transform = "translateX(-50%) translateY(10px)";
|
|
1245
|
+
toast.style.backgroundColor = "rgba(14, 21, 36, 0.95)";
|
|
1246
|
+
toast.style.border = "1px solid var(--border-color)";
|
|
1247
|
+
toast.style.color = "var(--text-primary)";
|
|
1248
|
+
toast.style.padding = "10px 18px";
|
|
1249
|
+
toast.style.borderRadius = "20px";
|
|
1250
|
+
toast.style.fontSize = "0.8rem";
|
|
1251
|
+
toast.style.fontWeight = "500";
|
|
1252
|
+
toast.style.zIndex = "500";
|
|
1253
|
+
toast.style.opacity = "0";
|
|
1254
|
+
toast.style.transition = "opacity 0.25s ease, transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275)";
|
|
1255
|
+
toast.style.boxShadow = "0 8px 24px rgba(0,0,0,0.5), 0 0 1px 1px rgba(255,255,255,0.08)";
|
|
1256
|
+
toast.style.pointerEvents = "none";
|
|
1257
|
+
toast.style.textAlign = "center";
|
|
1258
|
+
toast.style.whiteSpace = "nowrap";
|
|
1259
|
+
|
|
1260
|
+
document.querySelector(".app-container").appendChild(toast);
|
|
1261
|
+
|
|
1262
|
+
requestAnimationFrame(() => {
|
|
1263
|
+
toast.style.opacity = "1";
|
|
1264
|
+
toast.style.transform = "translateX(-50%) translateY(0)";
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
setTimeout(() => {
|
|
1268
|
+
toast.style.opacity = "0";
|
|
1269
|
+
toast.style.transform = "translateX(-50%) translateY(10px)";
|
|
1270
|
+
setTimeout(() => toast.remove(), 250);
|
|
1271
|
+
}, 2500);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// --- File Transfer Logic ---
|
|
1275
|
+
if (btnTriggerUpload && fileUploadInput) {
|
|
1276
|
+
btnTriggerUpload.addEventListener("click", () => {
|
|
1277
|
+
fileUploadInput.click();
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
fileUploadInput.addEventListener("change", (e) => {
|
|
1281
|
+
const files = e.target.files;
|
|
1282
|
+
if (!files || files.length === 0) return;
|
|
1283
|
+
uploadFile(files[0]);
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
if (btnRefreshFiles) {
|
|
1288
|
+
btnRefreshFiles.addEventListener("click", fetchFilesList);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function uploadFile(file) {
|
|
1292
|
+
if (!uploadProgressContainer || !uploadFilename || !uploadPercent || !uploadProgressFill) return;
|
|
1293
|
+
|
|
1294
|
+
uploadProgressContainer.style.display = "block";
|
|
1295
|
+
uploadFilename.textContent = file.name;
|
|
1296
|
+
uploadPercent.textContent = "0%";
|
|
1297
|
+
uploadProgressFill.style.width = "0%";
|
|
1298
|
+
btnTriggerUpload.disabled = true;
|
|
1299
|
+
|
|
1300
|
+
const xhr = new XMLHttpRequest();
|
|
1301
|
+
xhr.open("POST", "/api/upload", true);
|
|
1302
|
+
xhr.setRequestHeader("x-pin", inputPin.value.trim());
|
|
1303
|
+
xhr.setRequestHeader("x-file-name", encodeURIComponent(file.name));
|
|
1304
|
+
xhr.setRequestHeader("Content-Type", "application/octet-stream");
|
|
1305
|
+
|
|
1306
|
+
xhr.upload.onprogress = (e) => {
|
|
1307
|
+
if (e.lengthComputable) {
|
|
1308
|
+
const percentComplete = Math.round((e.loaded / e.total) * 100);
|
|
1309
|
+
uploadPercent.textContent = percentComplete + "%";
|
|
1310
|
+
uploadProgressFill.style.width = percentComplete + "%";
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
|
|
1314
|
+
xhr.onload = () => {
|
|
1315
|
+
btnTriggerUpload.disabled = false;
|
|
1316
|
+
if (xhr.status === 200) {
|
|
1317
|
+
uploadPercent.textContent = "Done!";
|
|
1318
|
+
showToast("File uploaded successfully");
|
|
1319
|
+
setTimeout(() => {
|
|
1320
|
+
uploadProgressContainer.style.display = "none";
|
|
1321
|
+
fetchFilesList(); // refresh list automatically
|
|
1322
|
+
}, 1500);
|
|
1323
|
+
} else {
|
|
1324
|
+
// Fix 4: 413 is now returned for oversized files — show a clear message
|
|
1325
|
+
const friendly = xhr.status === 413
|
|
1326
|
+
? "File too large! Maximum size is 200 MB."
|
|
1327
|
+
: "Upload failed (" + xhr.status + ")";
|
|
1328
|
+
uploadPercent.textContent = "Error";
|
|
1329
|
+
uploadProgressFill.style.background = "#ef4444";
|
|
1330
|
+
showToast(friendly);
|
|
1331
|
+
setTimeout(() => {
|
|
1332
|
+
uploadProgressContainer.style.display = "none";
|
|
1333
|
+
uploadProgressFill.style.background = "var(--color-cyan)";
|
|
1334
|
+
}, 3000);
|
|
1335
|
+
}
|
|
1336
|
+
fileUploadInput.value = ""; // clear input
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
xhr.onerror = () => {
|
|
1340
|
+
btnTriggerUpload.disabled = false;
|
|
1341
|
+
uploadPercent.textContent = "Error";
|
|
1342
|
+
uploadProgressFill.style.background = "#ef4444";
|
|
1343
|
+
showToast("Upload error. Check connection.");
|
|
1344
|
+
setTimeout(() => {
|
|
1345
|
+
uploadProgressContainer.style.display = "none";
|
|
1346
|
+
uploadProgressFill.style.background = "var(--color-cyan)";
|
|
1347
|
+
}, 3000);
|
|
1348
|
+
fileUploadInput.value = "";
|
|
1349
|
+
};
|
|
1350
|
+
|
|
1351
|
+
xhr.send(file);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
function fetchFilesList() {
|
|
1355
|
+
if (!fileListContainer) return;
|
|
1356
|
+
|
|
1357
|
+
fetch(`/api/files?pin=${encodeURIComponent(inputPin.value.trim())}`)
|
|
1358
|
+
.then(res => res.json())
|
|
1359
|
+
.then(data => {
|
|
1360
|
+
if (data.error) {
|
|
1361
|
+
showToast(data.error);
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
renderFilesList(data);
|
|
1365
|
+
})
|
|
1366
|
+
.catch(err => {
|
|
1367
|
+
console.error("Failed to fetch files:", err);
|
|
1368
|
+
fileListContainer.innerHTML = '<p class="empty-state" style="color:#ef4444;">Failed to load files</p>';
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function renderFilesList(files) {
|
|
1373
|
+
if (!fileListContainer) return;
|
|
1374
|
+
fileListContainer.innerHTML = "";
|
|
1375
|
+
|
|
1376
|
+
if (files.length === 0) {
|
|
1377
|
+
fileListContainer.innerHTML = '<p class="empty-state">No files found on PC.</p>';
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
files.forEach(f => {
|
|
1382
|
+
const sizeKB = (f.size / 1024).toFixed(1);
|
|
1383
|
+
let sizeStr = sizeKB + " KB";
|
|
1384
|
+
if (sizeKB > 1024) {
|
|
1385
|
+
sizeStr = (sizeKB / 1024).toFixed(2) + " MB";
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
const item = document.createElement("div");
|
|
1389
|
+
item.className = "file-list-item";
|
|
1390
|
+
|
|
1391
|
+
const fileInfo = document.createElement("div");
|
|
1392
|
+
fileInfo.className = "file-info";
|
|
1393
|
+
|
|
1394
|
+
const fileNameSpan = document.createElement("span");
|
|
1395
|
+
fileNameSpan.className = "file-name";
|
|
1396
|
+
fileNameSpan.title = f.name;
|
|
1397
|
+
fileNameSpan.textContent = f.name;
|
|
1398
|
+
|
|
1399
|
+
const fileSizeSpan = document.createElement("span");
|
|
1400
|
+
fileSizeSpan.className = "file-size";
|
|
1401
|
+
fileSizeSpan.textContent = sizeStr;
|
|
1402
|
+
|
|
1403
|
+
fileInfo.appendChild(fileNameSpan);
|
|
1404
|
+
fileInfo.appendChild(fileSizeSpan);
|
|
1405
|
+
|
|
1406
|
+
const fileActions = document.createElement("div");
|
|
1407
|
+
fileActions.className = "file-actions";
|
|
1408
|
+
|
|
1409
|
+
const downloadAnchor = document.createElement("a");
|
|
1410
|
+
downloadAnchor.href = `/api/files/${encodeURIComponent(f.name)}?pin=${encodeURIComponent(inputPin.value.trim())}`;
|
|
1411
|
+
downloadAnchor.className = "file-action-btn";
|
|
1412
|
+
downloadAnchor.setAttribute("download", "");
|
|
1413
|
+
downloadAnchor.title = "Download";
|
|
1414
|
+
downloadAnchor.innerHTML = `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`;
|
|
1415
|
+
|
|
1416
|
+
const deleteBtn = document.createElement("button");
|
|
1417
|
+
deleteBtn.className = "file-action-btn del-btn";
|
|
1418
|
+
deleteBtn.setAttribute("data-filename", f.name);
|
|
1419
|
+
deleteBtn.title = "Delete from PC";
|
|
1420
|
+
deleteBtn.innerHTML = `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>`;
|
|
1421
|
+
|
|
1422
|
+
fileActions.appendChild(downloadAnchor);
|
|
1423
|
+
fileActions.appendChild(deleteBtn);
|
|
1424
|
+
|
|
1425
|
+
item.appendChild(fileInfo);
|
|
1426
|
+
item.appendChild(fileActions);
|
|
1427
|
+
fileListContainer.appendChild(item);
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
// Attach delete listeners
|
|
1431
|
+
const delBtns = fileListContainer.querySelectorAll(".del-btn");
|
|
1432
|
+
delBtns.forEach(btn => {
|
|
1433
|
+
btn.addEventListener("click", (e) => {
|
|
1434
|
+
const filename = e.currentTarget.getAttribute("data-filename");
|
|
1435
|
+
if (confirm(`Delete ${filename} from PC?`)) {
|
|
1436
|
+
deleteFile(filename);
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function deleteFile(filename) {
|
|
1443
|
+
fetch(`/api/files/${encodeURIComponent(filename)}?pin=${encodeURIComponent(inputPin.value.trim())}`, {
|
|
1444
|
+
method: 'DELETE'
|
|
1445
|
+
})
|
|
1446
|
+
.then(res => res.json())
|
|
1447
|
+
.then(data => {
|
|
1448
|
+
if (data.success) {
|
|
1449
|
+
showToast("Deleted " + filename);
|
|
1450
|
+
fetchFilesList();
|
|
1451
|
+
} else {
|
|
1452
|
+
showToast("Failed to delete");
|
|
1453
|
+
}
|
|
1454
|
+
})
|
|
1455
|
+
.catch(err => {
|
|
1456
|
+
console.error("Delete error:", err);
|
|
1457
|
+
showToast("Delete error");
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// Auto-fetch files if the settings screen is opened
|
|
1462
|
+
navItems.forEach(item => {
|
|
1463
|
+
item.addEventListener("click", () => {
|
|
1464
|
+
if (item.getAttribute("data-screen") === "screen-settings") {
|
|
1465
|
+
fetchFilesList();
|
|
1466
|
+
}
|
|
1467
|
+
});
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
});
|