@dyyz1993/agent-browser 0.9.2 → 0.11.1
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/dist/__tests__/utils/parseCli.d.ts +1 -0
- package/dist/__tests__/utils/parseCli.d.ts.map +1 -1
- package/dist/__tests__/utils/parseCli.js +18 -10
- package/dist/__tests__/utils/parseCli.js.map +1 -1
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +63 -3
- package/dist/actions.js.map +1 -1
- package/dist/browser.d.ts +46 -2
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +343 -13
- package/dist/browser.js.map +1 -1
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +8 -3
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/connection.d.ts.map +1 -1
- package/dist/cli/connection.js +39 -1
- package/dist/cli/connection.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +27 -20
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/output.d.ts.map +1 -1
- package/dist/cli/output.js +5 -0
- package/dist/cli/output.js.map +1 -1
- package/dist/cli.js +20 -0
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +147 -1
- package/dist/daemon.js.map +1 -1
- package/dist/message-bridge.d.ts.map +1 -1
- package/dist/message-bridge.js +22 -4
- package/dist/message-bridge.js.map +1 -1
- package/dist/openapi.d.ts +22 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +382 -0
- package/dist/openapi.js.map +1 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +18 -0
- package/dist/protocol.js.map +1 -1
- package/dist/recorder/inject.js +61 -134
- package/dist/stream-server-standalone.d.ts +10 -0
- package/dist/stream-server-standalone.d.ts.map +1 -1
- package/dist/stream-server-standalone.js +594 -74
- package/dist/stream-server-standalone.js.map +1 -1
- package/dist/stream-server.d.ts +67 -2
- package/dist/stream-server.d.ts.map +1 -1
- package/dist/stream-server.js +371 -51
- package/dist/stream-server.js.map +1 -1
- package/dist/swagger-ui.d.ts +6 -0
- package/dist/swagger-ui.d.ts.map +1 -0
- package/dist/swagger-ui.js +51 -0
- package/dist/swagger-ui.js.map +1 -0
- package/dist/test-live.d.ts +2 -0
- package/dist/test-live.d.ts.map +1 -0
- package/dist/test-live.js +333 -0
- package/dist/test-live.js.map +1 -0
- package/dist/types.d.ts +7 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/viewer-html.d.ts.map +1 -1
- package/dist/viewer-html.js +270 -58
- package/dist/viewer-html.js.map +1 -1
- package/dist/viewer-script.d.ts +20 -2
- package/dist/viewer-script.d.ts.map +1 -1
- package/dist/viewer-script.js +911 -154
- package/dist/viewer-script.js.map +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +6 -32
- package/scripts/test-cli-help.sh +51 -0
- package/scripts/verify-form.sh +67 -0
- package/scripts/verify-login.sh +65 -0
- package/scripts/verify-recording.sh +80 -0
- package/scripts/verify-upload.sh +41 -0
- package/skills/agent-browser/SKILL.md +297 -160
- package/skills/agent-browser/references/commands.md +3 -0
- package/skills/agent-browser/references/mobile-viewer.md +188 -0
- package/skills/agent-browser/references/network-monitoring.md +232 -0
- package/skills/agent-browser/references/recorder.md +319 -0
- package/skills/agent-browser/references/viewer-mode.md +148 -0
- package/skills/agent-browser/templates/api-interception.sh +3 -1
- package/skills/agent-browser/templates/data-extraction.sh +8 -4
- package/skills/agent-browser/templates/form-automation.sh +18 -23
- package/skills/agent-browser/templates/network-intercept-crawl.sh +256 -0
- package/skills/agent-browser/templates/recorder-workflow.sh +51 -0
- package/skills/agent-browser/templates/viewer-remote.sh +41 -0
- package/dist/__tests__/test-iframe.d.ts +0 -2
- package/dist/__tests__/test-iframe.d.ts.map +0 -1
- package/dist/__tests__/test-iframe.js +0 -52
- package/dist/__tests__/test-iframe.js.map +0 -1
- package/dist/cli-new.d.ts +0 -3
- package/dist/cli-new.d.ts.map +0 -1
- package/dist/cli-new.js +0 -308
- package/dist/cli-new.js.map +0 -1
- package/dist/cli-old.d.ts +0 -3
- package/dist/cli-old.d.ts.map +0 -1
- package/dist/cli-old.js +0 -1101
- package/dist/cli-old.js.map +0 -1
- package/dist/recorder/binding.d.ts +0 -24
- package/dist/recorder/binding.d.ts.map +0 -1
- package/dist/recorder/binding.js +0 -215
- package/dist/recorder/binding.js.map +0 -1
- package/dist/recorder/index.d.ts +0 -4
- package/dist/recorder/index.d.ts.map +0 -1
- package/dist/recorder/index.js +0 -4
- package/dist/recorder/index.js.map +0 -1
- package/dist/recorder/recorder.d.ts +0 -19
- package/dist/recorder/recorder.d.ts.map +0 -1
- package/dist/recorder/recorder.js +0 -101
- package/dist/recorder/recorder.js.map +0 -1
- package/dist/recorder/store.d.ts +0 -22
- package/dist/recorder/store.d.ts.map +0 -1
- package/dist/recorder/store.js +0 -150
- package/dist/recorder/store.js.map +0 -1
- package/dist/recorder/types.d.ts +0 -73
- package/dist/recorder/types.d.ts.map +0 -1
- package/dist/recorder/types.js +0 -5
- package/dist/recorder/types.js.map +0 -1
package/dist/viewer-script.js
CHANGED
|
@@ -52,13 +52,18 @@ export function sendUserActivity(state, qualityBadge, ws) {
|
|
|
52
52
|
}, 2000);
|
|
53
53
|
qualityBadge.textContent = 'interacting';
|
|
54
54
|
}
|
|
55
|
-
export function screenToPage(screenX, screenY,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
export function screenToPage(screenX, screenY, rect, deviceWidth, deviceHeight, element) {
|
|
56
|
+
if (rect.width <= 0 || rect.height <= 0)
|
|
57
|
+
return { x: 0, y: 0 };
|
|
58
|
+
const scaleX = deviceWidth / rect.width;
|
|
59
|
+
const scaleY = deviceHeight / rect.height;
|
|
60
|
+
let pageX = Math.round((screenX - rect.left) * scaleX);
|
|
61
|
+
let pageY = Math.round((screenY - rect.top) * scaleY);
|
|
62
|
+
if (element) {
|
|
63
|
+
pageX += element.x;
|
|
64
|
+
pageY += element.y;
|
|
65
|
+
}
|
|
66
|
+
return { x: pageX, y: pageY };
|
|
62
67
|
}
|
|
63
68
|
export function updateModifiers(e) {
|
|
64
69
|
let modifiers = 0;
|
|
@@ -83,8 +88,11 @@ export function buildViewerScript() {
|
|
|
83
88
|
const urlParams = new URLSearchParams(location.search);
|
|
84
89
|
const instanceId = urlParams.get('instanceId');
|
|
85
90
|
const session = urlParams.get('session') || 'default';
|
|
91
|
+
const rawSelector = urlParams.get('selector');
|
|
92
|
+
const selector = rawSelector ? decodeURIComponent(rawSelector) : undefined;
|
|
86
93
|
const wsParam = instanceId ? 'instanceId=' + instanceId : 'session=' + session;
|
|
87
|
-
|
|
94
|
+
|
|
95
|
+
const wsUrl = wsProtocol + '//' + location.hostname + ':' + port + '?' + wsParam + (selector ? '&selector=' + encodeURIComponent(selector) : '');
|
|
88
96
|
|
|
89
97
|
// Background management
|
|
90
98
|
let shouldReconnect = true;
|
|
@@ -97,19 +105,131 @@ export function buildViewerScript() {
|
|
|
97
105
|
const statusText = document.getElementById('statusText');
|
|
98
106
|
const urlDisplay = document.getElementById('urlDisplay');
|
|
99
107
|
const qualityBadge = document.getElementById('qualityBadge');
|
|
100
|
-
const tabsContainer = document.getElementById('tabs');
|
|
101
108
|
const connecting = document.getElementById('connecting');
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
|
|
110
|
+
const ua = (navigator.userAgent || '').toLowerCase();
|
|
111
|
+
|
|
112
|
+
function detectDeviceMode() {
|
|
113
|
+
var uaMatch = /iphone|ipod|android(?=.*mobile)|mobile|tablet|ipad/i.test(ua);
|
|
114
|
+
var hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
115
|
+
return uaMatch || hasTouch ? 'mobile' : 'desktop';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
var _deviceCurrent = detectDeviceMode();
|
|
119
|
+
|
|
120
|
+
const DeviceMode = {
|
|
121
|
+
_current: _deviceCurrent,
|
|
122
|
+
_listeners: [],
|
|
123
|
+
get current() { return this._current; },
|
|
124
|
+
onModeChange: function(fn) { this._listeners.push(fn); },
|
|
125
|
+
switchTo: function(mode) {
|
|
126
|
+
if (mode === this._current) return;
|
|
127
|
+
var prev = this._current;
|
|
128
|
+
this._current = mode;
|
|
129
|
+
if (mode === 'desktop') {
|
|
130
|
+
MobileModule.detach();
|
|
131
|
+
DesktopModule.attach();
|
|
132
|
+
} else {
|
|
133
|
+
DesktopModule.detach();
|
|
134
|
+
MobileModule.attach();
|
|
135
|
+
}
|
|
136
|
+
this._listeners.forEach(function(fn) { fn(mode, prev); });
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
var hiddenInput = null;
|
|
141
|
+
|
|
142
|
+
const DesktopModule = {
|
|
143
|
+
attach: function() {
|
|
144
|
+
if (hiddenInput && hiddenInput.parentNode) return;
|
|
145
|
+
hiddenInput = document.createElement('input');
|
|
146
|
+
hiddenInput.type = 'text';
|
|
147
|
+
hiddenInput.style.cssText = 'position:fixed;right:8px;bottom:80px;opacity:0.01;width:1px;height:1px;border:none;outline:none;padding:0;margin:0;font-size:16px;pointer-events:none;';
|
|
148
|
+
hiddenInput.id = 'hidden-input';
|
|
149
|
+
hiddenInput.setAttribute('autocomplete', 'off');
|
|
150
|
+
hiddenInput.setAttribute('autocorrect', 'off');
|
|
151
|
+
hiddenInput.setAttribute('autocapitalize', 'off');
|
|
152
|
+
hiddenInput.setAttribute('spellcheck', 'false');
|
|
153
|
+
document.body.appendChild(hiddenInput);
|
|
154
|
+
|
|
155
|
+
hiddenInput.addEventListener('compositionstart', () => {
|
|
156
|
+
isComposing = true;
|
|
157
|
+
lastInputValue = hiddenInput.value;
|
|
158
|
+
console.log('[Viewer] compositionstart, lastInputValue:', lastInputValue);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
hiddenInput.addEventListener('compositionend', (e) => {
|
|
162
|
+
isComposing = false;
|
|
163
|
+
const newText = hiddenInput.value.slice(lastInputValue.length);
|
|
164
|
+
console.log('[Viewer] compositionend, newText:', newText, 'hiddenInput.value:', hiddenInput.value);
|
|
165
|
+
if (newText) {
|
|
166
|
+
sendUserActivity();
|
|
167
|
+
safeSend(JSON.stringify({
|
|
168
|
+
type: 'keyboard_insert_text',
|
|
169
|
+
text: newText
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
lastInputValue = '';
|
|
173
|
+
hiddenInput.value = '';
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
hiddenInput.addEventListener('input', (e) => {
|
|
177
|
+
console.log('[Viewer] input event, isComposing:', isComposing, 'hiddenInput.value:', hiddenInput.value);
|
|
178
|
+
if (isComposing) return;
|
|
179
|
+
|
|
180
|
+
const newValue = hiddenInput.value;
|
|
181
|
+
if (newValue.length > 0) {
|
|
182
|
+
sendUserActivity();
|
|
183
|
+
safeSend(JSON.stringify({
|
|
184
|
+
type: 'keyboard_insert_text',
|
|
185
|
+
text: newValue
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
hiddenInput.value = '';
|
|
189
|
+
lastInputValue = '';
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
hiddenInput.addEventListener('paste', (e) => {
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
e.stopPropagation();
|
|
195
|
+
|
|
196
|
+
const text = e.clipboardData.getData('text');
|
|
197
|
+
console.log('[Viewer] paste event, text:', text);
|
|
198
|
+
if (text) {
|
|
199
|
+
sendUserActivity();
|
|
200
|
+
safeSend(JSON.stringify({
|
|
201
|
+
type: 'keyboard_insert_text',
|
|
202
|
+
text: text
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
focusHiddenInput();
|
|
208
|
+
},
|
|
209
|
+
detach: function() {
|
|
210
|
+
if (hiddenInput) { hiddenInput.blur(); if (hiddenInput.parentNode) hiddenInput.parentNode.removeChild(hiddenInput); hiddenInput = null; }
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const MobileModule = {
|
|
215
|
+
attach: function() {
|
|
216
|
+
if (touchpad) { touchpad.style.display = 'flex'; touchpad.style.position = 'relative'; touchpad.style.background = 'linear-gradient(180deg, #1a1a2e 0%, #16213e 100%)'; touchpad.style.borderTop = '2px solid #4ecca3'; touchpad.style.justifyContent = 'center'; touchpad.style.zIndex = '10'; }
|
|
217
|
+
setupToolbar();
|
|
218
|
+
if (!cursorInitialized) { cursorInitialized = true; setTimeout(initCursor, 50); }
|
|
219
|
+
},
|
|
220
|
+
detach: function() {
|
|
221
|
+
var ip = document.getElementById('input-panel');
|
|
222
|
+
if (ip) { ip.style.display = 'none'; ip.style.bottom = '0px'; }
|
|
223
|
+
if (cursor) cursor.style.display = 'block';
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const degradedToast = document.createElement('div');
|
|
228
|
+
degradedToast.id = 'degraded-toast';
|
|
229
|
+
degradedToast.style.cssText = 'position:fixed;top:10px;left:50%;transform:translateX(-50%);background:rgba(255,200,0,0.9);color:#000;padding:10px 20px;border-radius:4px;font-family:sans-serif;font-size:14px;z-index:9999;display:none;pointer-events:none;';
|
|
230
|
+
degradedToast.textContent = 'Element not found, showing full page';
|
|
231
|
+
document.body.appendChild(degradedToast);
|
|
232
|
+
|
|
113
233
|
let ws = null;
|
|
114
234
|
let metadata = { deviceWidth: 1280, deviceHeight: 720, pageScaleFactor: 1, format: 'jpeg' };
|
|
115
235
|
let userActivityTimeout = null;
|
|
@@ -121,7 +241,8 @@ export function buildViewerScript() {
|
|
|
121
241
|
let lastInputValue = '';
|
|
122
242
|
let fixedSize = false;
|
|
123
243
|
let isRecording = false;
|
|
124
|
-
|
|
244
|
+
var _inputPollRaf = null;
|
|
245
|
+
|
|
125
246
|
function connect() {
|
|
126
247
|
ws = new WebSocket(wsUrl);
|
|
127
248
|
ws.binaryType = 'arraybuffer';
|
|
@@ -185,7 +306,15 @@ export function buildViewerScript() {
|
|
|
185
306
|
switch (msg.type) {
|
|
186
307
|
case 'frame':
|
|
187
308
|
pendingBinary = true;
|
|
309
|
+
const prevElement = metadata.element;
|
|
188
310
|
metadata = msg.metadata;
|
|
311
|
+
if (prevElement && !metadata.element) {
|
|
312
|
+
metadata.element = prevElement;
|
|
313
|
+
}
|
|
314
|
+
if (metadata.element) {
|
|
315
|
+
metadata.deviceWidth = metadata.element.width;
|
|
316
|
+
metadata.deviceHeight = metadata.element.height;
|
|
317
|
+
}
|
|
189
318
|
if (msg.format) metadata.format = msg.format;
|
|
190
319
|
if (msg.state) {
|
|
191
320
|
qualityBadge.textContent = msg.state;
|
|
@@ -204,6 +333,17 @@ export function buildViewerScript() {
|
|
|
204
333
|
metadata.deviceWidth = msg.viewportWidth;
|
|
205
334
|
metadata.deviceHeight = msg.viewportHeight;
|
|
206
335
|
}
|
|
336
|
+
if (msg.element) {
|
|
337
|
+
metadata.element = msg.element;
|
|
338
|
+
} else {
|
|
339
|
+
metadata.element = undefined;
|
|
340
|
+
if (selector && msg.degraded) {
|
|
341
|
+
showDegradedMessage();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (screen.src && metadata.deviceWidth && metadata.deviceHeight) {
|
|
345
|
+
fitImageToContainer();
|
|
346
|
+
}
|
|
207
347
|
}
|
|
208
348
|
break;
|
|
209
349
|
|
|
@@ -212,16 +352,24 @@ export function buildViewerScript() {
|
|
|
212
352
|
document.title = msg.data.title + ' - Agent Browser Viewer';
|
|
213
353
|
break;
|
|
214
354
|
|
|
215
|
-
case '
|
|
216
|
-
|
|
355
|
+
case 'input_focused':
|
|
356
|
+
if (inputMode) return;
|
|
357
|
+
if (DeviceMode.current !== 'mobile') return;
|
|
358
|
+
var sel = msg.selector || (msg.id ? '#' + msg.id : '');
|
|
359
|
+
enterInputMode(msg.value || '', msg.inputType || msg.tag || '', msg.placeholder || '', sel);
|
|
217
360
|
break;
|
|
218
361
|
|
|
219
|
-
case '
|
|
220
|
-
|
|
362
|
+
case 'input_value':
|
|
363
|
+
if (!inputMode) {
|
|
364
|
+
var field = document.getElementById('input-field');
|
|
365
|
+
if (field && typeof msg.text === 'string') {
|
|
366
|
+
field.value = msg.text;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
221
369
|
break;
|
|
222
370
|
|
|
223
|
-
case '
|
|
224
|
-
|
|
371
|
+
case 'input_blur':
|
|
372
|
+
exitInputMode();
|
|
225
373
|
break;
|
|
226
374
|
}
|
|
227
375
|
};
|
|
@@ -230,27 +378,53 @@ export function buildViewerScript() {
|
|
|
230
378
|
function handleBinary(data) {
|
|
231
379
|
if (!pendingBinary) return;
|
|
232
380
|
pendingBinary = false;
|
|
233
|
-
|
|
381
|
+
|
|
234
382
|
const blob = new Blob([data], {
|
|
235
383
|
type: metadata.format === 'webp' ? 'image/webp' : 'image/jpeg'
|
|
236
384
|
});
|
|
237
385
|
const url = URL.createObjectURL(blob);
|
|
238
|
-
|
|
386
|
+
|
|
239
387
|
const cleanup = () => {
|
|
240
388
|
URL.revokeObjectURL(url);
|
|
241
389
|
connecting.style.display = 'none';
|
|
242
390
|
screen.style.display = 'block';
|
|
391
|
+
fitImageToContainer();
|
|
392
|
+
if (!cursorInitialized && DeviceMode.current === 'mobile') {
|
|
393
|
+
cursorInitialized = true;
|
|
394
|
+
setTimeout(initCursor, 50);
|
|
395
|
+
}
|
|
243
396
|
};
|
|
244
|
-
|
|
397
|
+
|
|
245
398
|
screen.onload = cleanup;
|
|
246
399
|
screen.onerror = cleanup;
|
|
247
400
|
screen.src = url;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function fitImageToContainer() {
|
|
404
|
+
if (!metadata.deviceWidth || !metadata.deviceHeight) return;
|
|
405
|
+
var container = screen.parentElement;
|
|
406
|
+
if (!container) return;
|
|
407
|
+
var cw = container.clientWidth;
|
|
408
|
+
var ch = container.clientHeight;
|
|
409
|
+
if (cw <= 0 || ch <= 0) return;
|
|
410
|
+
|
|
411
|
+
var imgW = metadata.deviceWidth;
|
|
412
|
+
var imgH = metadata.deviceHeight;
|
|
413
|
+
var imgRatio = imgW / imgH;
|
|
414
|
+
var contRatio = cw / ch;
|
|
415
|
+
|
|
416
|
+
var dw, dh;
|
|
417
|
+
if (imgRatio > contRatio) {
|
|
418
|
+
dw = cw;
|
|
419
|
+
dh = cw / imgRatio;
|
|
420
|
+
} else {
|
|
421
|
+
dh = ch;
|
|
422
|
+
dw = ch * imgRatio;
|
|
253
423
|
}
|
|
424
|
+
|
|
425
|
+
screen.style.width = Math.round(dw) + 'px';
|
|
426
|
+
screen.style.height = Math.round(dh) + 'px';
|
|
427
|
+
|
|
254
428
|
}
|
|
255
429
|
|
|
256
430
|
function safeSend(data) {
|
|
@@ -272,13 +446,19 @@ export function buildViewerScript() {
|
|
|
272
446
|
|
|
273
447
|
function screenToPage(screenX, screenY) {
|
|
274
448
|
const rect = screen.getBoundingClientRect();
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
449
|
+
if (rect.width <= 0 || rect.height <= 0) return { x: 0, y: 0 };
|
|
450
|
+
|
|
451
|
+
var scaleX = metadata.deviceWidth / rect.width;
|
|
452
|
+
var scaleY = metadata.deviceHeight / rect.height;
|
|
453
|
+
var pageX = Math.round((screenX - rect.left) * scaleX);
|
|
454
|
+
var pageY = Math.round((screenY - rect.top) * scaleY);
|
|
455
|
+
|
|
456
|
+
if (metadata.element) {
|
|
457
|
+
pageX += metadata.element.x;
|
|
458
|
+
pageY += metadata.element.y;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return { x: pageX, y: pageY };
|
|
282
462
|
}
|
|
283
463
|
|
|
284
464
|
function updateModifiers(e) {
|
|
@@ -288,7 +468,14 @@ export function buildViewerScript() {
|
|
|
288
468
|
if (e.metaKey) modifiers |= 4;
|
|
289
469
|
if (e.shiftKey) modifiers |= 8;
|
|
290
470
|
}
|
|
291
|
-
|
|
471
|
+
|
|
472
|
+
function showDegradedMessage() {
|
|
473
|
+
degradedToast.style.display = 'block';
|
|
474
|
+
setTimeout(() => {
|
|
475
|
+
degradedToast.style.display = 'none';
|
|
476
|
+
}, 3000);
|
|
477
|
+
}
|
|
478
|
+
|
|
292
479
|
function focusHiddenInput() {
|
|
293
480
|
hiddenInput.focus();
|
|
294
481
|
hiddenInput.select();
|
|
@@ -297,7 +484,7 @@ export function buildViewerScript() {
|
|
|
297
484
|
screen.addEventListener('dragstart', (e) => e.preventDefault());
|
|
298
485
|
|
|
299
486
|
screen.addEventListener('click', () => {
|
|
300
|
-
focusHiddenInput();
|
|
487
|
+
if (DeviceMode.current === 'desktop') focusHiddenInput();
|
|
301
488
|
});
|
|
302
489
|
|
|
303
490
|
screen.addEventListener('mousemove', (e) => {
|
|
@@ -369,58 +556,6 @@ export function buildViewerScript() {
|
|
|
369
556
|
e.preventDefault();
|
|
370
557
|
});
|
|
371
558
|
|
|
372
|
-
hiddenInput.addEventListener('compositionstart', () => {
|
|
373
|
-
isComposing = true;
|
|
374
|
-
lastInputValue = hiddenInput.value;
|
|
375
|
-
console.log('[Viewer] compositionstart, lastInputValue:', lastInputValue);
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
hiddenInput.addEventListener('compositionend', (e) => {
|
|
379
|
-
isComposing = false;
|
|
380
|
-
const newText = hiddenInput.value.slice(lastInputValue.length);
|
|
381
|
-
console.log('[Viewer] compositionend, newText:', newText, 'hiddenInput.value:', hiddenInput.value);
|
|
382
|
-
if (newText) {
|
|
383
|
-
sendUserActivity();
|
|
384
|
-
safeSend(JSON.stringify({
|
|
385
|
-
type: 'keyboard_insert_text',
|
|
386
|
-
text: newText
|
|
387
|
-
}));
|
|
388
|
-
}
|
|
389
|
-
lastInputValue = '';
|
|
390
|
-
hiddenInput.value = '';
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
hiddenInput.addEventListener('input', (e) => {
|
|
394
|
-
console.log('[Viewer] input event, isComposing:', isComposing, 'hiddenInput.value:', hiddenInput.value);
|
|
395
|
-
if (isComposing) return;
|
|
396
|
-
|
|
397
|
-
const newValue = hiddenInput.value;
|
|
398
|
-
if (newValue.length > 0) {
|
|
399
|
-
sendUserActivity();
|
|
400
|
-
safeSend(JSON.stringify({
|
|
401
|
-
type: 'keyboard_insert_text',
|
|
402
|
-
text: newValue
|
|
403
|
-
}));
|
|
404
|
-
}
|
|
405
|
-
hiddenInput.value = '';
|
|
406
|
-
lastInputValue = '';
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
hiddenInput.addEventListener('paste', (e) => {
|
|
410
|
-
e.preventDefault();
|
|
411
|
-
e.stopPropagation();
|
|
412
|
-
|
|
413
|
-
const text = e.clipboardData.getData('text');
|
|
414
|
-
console.log('[Viewer] paste event, text:', text);
|
|
415
|
-
if (text) {
|
|
416
|
-
sendUserActivity();
|
|
417
|
-
safeSend(JSON.stringify({
|
|
418
|
-
type: 'keyboard_insert_text',
|
|
419
|
-
text: text
|
|
420
|
-
}));
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
|
-
|
|
424
559
|
document.addEventListener('keydown', (e) => {
|
|
425
560
|
console.log('[Viewer] keydown, key:', e.key, 'target:', e.target === hiddenInput ? 'hiddenInput' : 'other', 'metaKey:', e.metaKey, 'ctrlKey:', e.ctrlKey);
|
|
426
561
|
if (e.target === hiddenInput) {
|
|
@@ -443,7 +578,10 @@ export function buildViewerScript() {
|
|
|
443
578
|
return;
|
|
444
579
|
}
|
|
445
580
|
}
|
|
446
|
-
|
|
581
|
+
|
|
582
|
+
const mobileInputField = document.getElementById('input-field');
|
|
583
|
+
if (mobileInputField && e.target === mobileInputField) return;
|
|
584
|
+
|
|
447
585
|
if (isComposing) return;
|
|
448
586
|
|
|
449
587
|
sendUserActivity();
|
|
@@ -461,6 +599,8 @@ export function buildViewerScript() {
|
|
|
461
599
|
document.addEventListener('keyup', (e) => {
|
|
462
600
|
console.log('[Viewer] keyup, key:', e.key);
|
|
463
601
|
if (isComposing && e.target === hiddenInput) return;
|
|
602
|
+
const mobileInputField = document.getElementById('input-field');
|
|
603
|
+
if (mobileInputField && e.target === mobileInputField) return;
|
|
464
604
|
|
|
465
605
|
safeSend(JSON.stringify({
|
|
466
606
|
type: 'keyboard_up',
|
|
@@ -468,74 +608,691 @@ export function buildViewerScript() {
|
|
|
468
608
|
}));
|
|
469
609
|
});
|
|
470
610
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
611
|
+
const cursor = document.getElementById('cursor');
|
|
612
|
+
const touchpad = document.getElementById('touchpad');
|
|
613
|
+
const screenContainer = document.getElementById('screenContainer');
|
|
614
|
+
|
|
615
|
+
// Initialize modules based on detected mode
|
|
616
|
+
if (DeviceMode.current === 'desktop') {
|
|
617
|
+
DesktopModule.attach();
|
|
618
|
+
} else {
|
|
619
|
+
MobileModule.attach();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
let cursorPos = { x: 0, y: 0 };
|
|
623
|
+
let dragMode = false;
|
|
624
|
+
let isMouseDown = false;
|
|
625
|
+
let lastTouchPos = null;
|
|
626
|
+
let touchMoved = false;
|
|
627
|
+
let twoFingerStartPos = null;
|
|
628
|
+
let longPressTimer = null;
|
|
629
|
+
let longPressHintTimer = null;
|
|
630
|
+
let cursorInitialized = false;
|
|
631
|
+
|
|
632
|
+
const CURSOR_SENSITIVITY = 1.5;
|
|
633
|
+
const WHEEL_SENSITIVITY = 2.0;
|
|
634
|
+
const LONG_PRESS_MS = 800;
|
|
635
|
+
const COOLDOWN_MS = 200;
|
|
636
|
+
const MOVE_THRESHOLD = 5;
|
|
637
|
+
const ACCELERATION = 0.8;
|
|
638
|
+
const ACCEL_MAX_VELOCITY = 3.0;
|
|
639
|
+
|
|
640
|
+
let lastMoveTime = 0;
|
|
641
|
+
|
|
642
|
+
function computeAcceleration(dx, dy) {
|
|
643
|
+
const now = Date.now();
|
|
644
|
+
const dt = now - lastMoveTime;
|
|
645
|
+
lastMoveTime = now;
|
|
646
|
+
if (dt <= 0 || dt > 200) return CURSOR_SENSITIVITY;
|
|
647
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
648
|
+
const velocity = Math.min(dist / dt, ACCEL_MAX_VELOCITY);
|
|
649
|
+
return CURSOR_SENSITIVITY * (1 + ACCELERATION * velocity);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function computeWheelAccel(dx, dy) {
|
|
653
|
+
const now = Date.now();
|
|
654
|
+
const dt = now - lastMoveTime;
|
|
655
|
+
lastMoveTime = now;
|
|
656
|
+
if (dt <= 0 || dt > 200) return WHEEL_SENSITIVITY;
|
|
657
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
658
|
+
const velocity = Math.min(dist / dt, ACCEL_MAX_VELOCITY);
|
|
659
|
+
return WHEEL_SENSITIVITY * (1 + ACCELERATION * velocity);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
let screenRect = null;
|
|
663
|
+
let moveAllowed = false;
|
|
664
|
+
var inputMode = false;
|
|
665
|
+
let moveCooldownUntil = 0;
|
|
666
|
+
let lastWheelDeltaX = 0;
|
|
667
|
+
let lastWheelDeltaY = 0;
|
|
668
|
+
let momentumActive = false;
|
|
669
|
+
let keyboardVvHandler = null;
|
|
670
|
+
let _scrollGuard = null;
|
|
671
|
+
|
|
672
|
+
function updateScreenRect() {
|
|
673
|
+
screenRect = screen.getBoundingClientRect();
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function enterInputMode(initialValue, inputType, placeholder, selector) {
|
|
677
|
+
if (inputMode) return;
|
|
678
|
+
inputMode = true;
|
|
679
|
+
|
|
680
|
+
cursor.style.display = 'none';
|
|
681
|
+
|
|
682
|
+
const ip = document.getElementById('input-panel');
|
|
683
|
+
const tp = document.getElementById('touchpad');
|
|
684
|
+
|
|
685
|
+
if (ip) {
|
|
686
|
+
ip.style.display = 'flex';
|
|
687
|
+
ip.style.bottom = '0px';
|
|
688
|
+
}
|
|
689
|
+
if (tp) tp.style.display = 'none';
|
|
690
|
+
|
|
691
|
+
var labelParts = [];
|
|
692
|
+
if (inputType) labelParts.push(inputType);
|
|
693
|
+
if (placeholder && placeholder !== initialValue) labelParts.push(placeholder);
|
|
694
|
+
var targetEl = document.getElementById('input-target');
|
|
695
|
+
if (targetEl) targetEl.textContent = labelParts.length > 0 ? labelParts.join(' | ') : 'input';
|
|
696
|
+
|
|
697
|
+
window._currentTargetSelector = selector || '';
|
|
698
|
+
|
|
699
|
+
var field = document.getElementById('input-field');
|
|
700
|
+
if (field) {
|
|
701
|
+
field.value = initialValue || '';
|
|
702
|
+
field.dataset.lastSent = initialValue || '';
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
window.scrollTo(0, 0);
|
|
706
|
+
document.body.scrollTop = 0;
|
|
707
|
+
document.documentElement.scrollTop = 0;
|
|
708
|
+
|
|
709
|
+
document.body.style.touchAction = 'none';
|
|
710
|
+
document.documentElement.style.touchAction = 'none';
|
|
711
|
+
|
|
712
|
+
setTimeout(function() {
|
|
713
|
+
if (!field) return;
|
|
714
|
+
|
|
715
|
+
// Capture original viewport height AFTER panel is visible, BEFORE focus
|
|
716
|
+
var origVh = window.visualViewport ? window.visualViewport.height : window.innerHeight;
|
|
717
|
+
|
|
718
|
+
// Register visualViewport listener for keyboard detection
|
|
719
|
+
if (window.visualViewport) {
|
|
720
|
+
var kbTolerance = Math.floor(window.innerHeight * 0.1);
|
|
721
|
+
|
|
722
|
+
keyboardVvHandler = function() {
|
|
723
|
+
if (!inputMode || !ip) return;
|
|
724
|
+
var currentH = window.visualViewport.height;
|
|
725
|
+
var kbHeight = Math.max(0, origVh - currentH);
|
|
726
|
+
// Fallback: use innerHeight difference
|
|
727
|
+
if (kbHeight < kbTolerance) {
|
|
728
|
+
kbHeight = Math.max(kbHeight, Math.max(0, window.innerHeight - currentH));
|
|
729
|
+
}
|
|
730
|
+
if (kbHeight > kbTolerance) {
|
|
731
|
+
ip.style.bottom = kbHeight + 'px';
|
|
732
|
+
} else {
|
|
733
|
+
ip.style.bottom = '0px';
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
window.visualViewport.addEventListener('resize', keyboardVvHandler);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
field.focus();
|
|
740
|
+
|
|
741
|
+
window.scrollTo(0, 0);
|
|
742
|
+
document.body.scrollTop = 0;
|
|
743
|
+
document.documentElement.scrollTop = 0;
|
|
744
|
+
|
|
745
|
+
// Delayed check: wait for keyboard animation to complete (~300ms)
|
|
746
|
+
setTimeout(function() {
|
|
747
|
+
if (keyboardVvHandler) keyboardVvHandler();
|
|
748
|
+
window.scrollTo(0, 0);
|
|
749
|
+
document.body.scrollTop = 0;
|
|
750
|
+
document.documentElement.scrollTop = 0;
|
|
751
|
+
}, 350);
|
|
752
|
+
|
|
753
|
+
// Scroll guard interval: continuously fight iOS auto-scroll (reference demo)
|
|
754
|
+
if (!_scrollGuard) {
|
|
755
|
+
_scrollGuard = setInterval(function() {
|
|
756
|
+
if (!inputMode) return;
|
|
757
|
+
if (window.scrollY > 0 || document.body.scrollTop > 0 ||
|
|
758
|
+
document.documentElement.scrollTop > 0) {
|
|
759
|
+
window.scrollTo(0, 0);
|
|
760
|
+
document.body.scrollTop = 0;
|
|
761
|
+
document.documentElement.scrollTop = 0;
|
|
762
|
+
}
|
|
763
|
+
}, 100);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// RAF poll: reliable value-change detection for IME/CJK/clipboard/paste
|
|
767
|
+
var _pollField = field;
|
|
768
|
+
var _lastPolled = _pollField.value || '';
|
|
769
|
+
window._fieldComposing = false;
|
|
770
|
+
|
|
771
|
+
_pollField.addEventListener('compositionstart', function() {
|
|
772
|
+
window._fieldComposing = true;
|
|
773
|
+
});
|
|
774
|
+
_pollField.addEventListener('compositionend', function() {
|
|
775
|
+
window._fieldComposing = false;
|
|
776
|
+
// Double-RAF: yields current frame + next paint cycle.
|
|
777
|
+
// On mobile browsers (iOS Safari, Android WebView), .value may
|
|
778
|
+
// not be updated until 1-2 frames after compositionend fires.
|
|
779
|
+
requestAnimationFrame(function() {
|
|
780
|
+
requestAnimationFrame(function() {
|
|
781
|
+
syncInputToRemote(_pollField);
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
(function startPoll() {
|
|
787
|
+
function poll() {
|
|
788
|
+
if (!inputMode || !_pollField) { _inputPollRaf = null; return; }
|
|
789
|
+
var cur = _pollField.value;
|
|
790
|
+
if (cur !== _lastPolled) {
|
|
791
|
+
_lastPolled = cur;
|
|
792
|
+
if (!window._fieldComposing) {
|
|
793
|
+
syncInputToRemote(_pollField);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
_inputPollRaf = requestAnimationFrame(poll);
|
|
797
|
+
}
|
|
798
|
+
_inputPollRaf = requestAnimationFrame(poll);
|
|
799
|
+
})();
|
|
800
|
+
}, 100);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function exitInputMode() {
|
|
804
|
+
if (!inputMode) return;
|
|
805
|
+
inputMode = false;
|
|
806
|
+
|
|
807
|
+
if (_inputPollRaf) { cancelAnimationFrame(_inputPollRaf); _inputPollRaf = null; }
|
|
808
|
+
window._fieldComposing = false;
|
|
809
|
+
|
|
810
|
+
cursor.style.display = 'block';
|
|
811
|
+
|
|
812
|
+
const field = document.getElementById('input-field');
|
|
813
|
+
if (field) { field.value = ''; field.blur(); delete field.dataset.lastSent; }
|
|
814
|
+
|
|
815
|
+
const ip = document.getElementById('input-panel');
|
|
816
|
+
const tp = document.getElementById('touchpad');
|
|
817
|
+
|
|
818
|
+
if (ip) {
|
|
819
|
+
ip.style.display = 'none';
|
|
820
|
+
ip.style.bottom = '0px';
|
|
821
|
+
}
|
|
822
|
+
if (tp) tp.style.display = DeviceMode.current === 'mobile' ? 'flex' : 'none';
|
|
823
|
+
|
|
824
|
+
// Cleanup visualViewport handler
|
|
825
|
+
if (keyboardVvHandler && window.visualViewport) {
|
|
826
|
+
window.visualViewport.removeEventListener('resize', keyboardVvHandler);
|
|
827
|
+
keyboardVvHandler = null;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Cleanup scroll guard
|
|
831
|
+
if (_scrollGuard) {
|
|
832
|
+
clearInterval(_scrollGuard);
|
|
833
|
+
_scrollGuard = null;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Restore touch action
|
|
837
|
+
document.body.style.touchAction = '';
|
|
838
|
+
document.documentElement.style.touchAction = '';
|
|
839
|
+
|
|
478
840
|
safeSend(JSON.stringify({
|
|
479
|
-
type: '
|
|
480
|
-
|
|
481
|
-
touchPoints: touchPoints
|
|
841
|
+
type: 'input_blur_element',
|
|
842
|
+
selector: window._currentTargetSelector || ''
|
|
482
843
|
}));
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
var _syncDebounceTimer = null;
|
|
847
|
+
function syncInputToRemote(field) {
|
|
848
|
+
if (!field || !inputMode) return;
|
|
849
|
+
var current = field.value;
|
|
850
|
+
var lastSent = field.dataset.lastSent || '';
|
|
851
|
+
if (current === lastSent) return;
|
|
852
|
+
|
|
853
|
+
var isFirstSync = !field.dataset.lastSent;
|
|
854
|
+
function doSend() {
|
|
855
|
+
safeSend(JSON.stringify({
|
|
856
|
+
type: 'input_fill',
|
|
857
|
+
text: current,
|
|
858
|
+
selector: window._currentTargetSelector || ''
|
|
859
|
+
}));
|
|
860
|
+
field.dataset.lastSent = current;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (isFirstSync) {
|
|
864
|
+
doSend();
|
|
865
|
+
} else {
|
|
866
|
+
clearTimeout(_syncDebounceTimer);
|
|
867
|
+
_syncDebounceTimer = setTimeout(doSend, 30);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function sendInputText() {
|
|
872
|
+
const field = document.getElementById('input-field');
|
|
873
|
+
if (!field || !field.value.trim()) return;
|
|
874
|
+
|
|
875
|
+
var finalText = field.value;
|
|
876
|
+
var sel = window._currentTargetSelector || '';
|
|
877
|
+
|
|
878
|
+
// Send final text via input_fill + Enter key
|
|
491
879
|
safeSend(JSON.stringify({
|
|
492
|
-
type: '
|
|
493
|
-
|
|
494
|
-
|
|
880
|
+
type: 'input_fill',
|
|
881
|
+
text: finalText,
|
|
882
|
+
selector: sel
|
|
495
883
|
}));
|
|
496
|
-
e.preventDefault();
|
|
497
|
-
}, { passive: false });
|
|
498
|
-
|
|
499
|
-
screen.addEventListener('touchend', (e) => {
|
|
500
|
-
const touchPoints = Array.from(e.changedTouches).map(t => {
|
|
501
|
-
const pos = screenToPage(t.clientX, t.clientY);
|
|
502
|
-
return { x: pos.x, y: pos.y, id: t.identifier };
|
|
503
|
-
});
|
|
504
884
|
safeSend(JSON.stringify({
|
|
505
|
-
type: '
|
|
506
|
-
eventType: '
|
|
507
|
-
|
|
885
|
+
type: 'input_keyboard',
|
|
886
|
+
eventType: 'keyDown',
|
|
887
|
+
key: 'Enter',
|
|
888
|
+
code: 'Enter',
|
|
889
|
+
modifiers: 0,
|
|
890
|
+
selector: sel
|
|
508
891
|
}));
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
};
|
|
521
|
-
tabsContainer.appendChild(tab);
|
|
522
|
-
}
|
|
523
|
-
tab.textContent = title || url || 'New Tab';
|
|
524
|
-
tab.title = url;
|
|
525
|
-
tab.classList.toggle('active', active);
|
|
892
|
+
safeSend(JSON.stringify({
|
|
893
|
+
type: 'input_keyboard',
|
|
894
|
+
eventType: 'keyUp',
|
|
895
|
+
key: 'Enter',
|
|
896
|
+
code: 'Enter',
|
|
897
|
+
modifiers: 0,
|
|
898
|
+
selector: sel
|
|
899
|
+
}));
|
|
900
|
+
|
|
901
|
+
field.value = '';
|
|
902
|
+
exitInputMode();
|
|
526
903
|
}
|
|
527
|
-
|
|
528
|
-
function
|
|
529
|
-
|
|
530
|
-
|
|
904
|
+
|
|
905
|
+
function startMomentum() {
|
|
906
|
+
momentumActive = true;
|
|
907
|
+
var frame = function() {
|
|
908
|
+
if (!momentumActive) return;
|
|
909
|
+
lastWheelDeltaX *= 0.92;
|
|
910
|
+
lastWheelDeltaY *= 0.92;
|
|
911
|
+
if (Math.abs(lastWheelDeltaX) < 0.5 && Math.abs(lastWheelDeltaY) < 0.5) {
|
|
912
|
+
momentumActive = false;
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
var pagePos = screenToPage(cursorPos.x, cursorPos.y);
|
|
916
|
+
safeSend(JSON.stringify({
|
|
917
|
+
type: 'input_mouse',
|
|
918
|
+
eventType: 'mouseWheel',
|
|
919
|
+
x: pagePos.x,
|
|
920
|
+
y: pagePos.y,
|
|
921
|
+
deltaX: lastWheelDeltaX,
|
|
922
|
+
deltaY: lastWheelDeltaY,
|
|
923
|
+
modifiers: 0
|
|
924
|
+
}));
|
|
925
|
+
requestAnimationFrame(frame);
|
|
926
|
+
};
|
|
927
|
+
requestAnimationFrame(frame);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function initCursor() {
|
|
931
|
+
updateScreenRect();
|
|
932
|
+
cursorPos = { x: screenRect.left + screenRect.width / 2, y: screenRect.top + screenRect.height / 2 };
|
|
933
|
+
updateCursor();
|
|
934
|
+
cursor.style.display = 'block';
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function updateCursor() {
|
|
938
|
+
cursor.style.left = cursorPos.x + 'px';
|
|
939
|
+
cursor.style.top = cursorPos.y + 'px';
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function clampCursor(val, min, max) {
|
|
943
|
+
return Math.max(min, Math.min(max, val));
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function setCursorMode(mode) {
|
|
947
|
+
cursor.className = '';
|
|
948
|
+
if (mode === 'move') cursor.classList.add('cursor-move');
|
|
949
|
+
else if (mode === 'drag') cursor.classList.add('cursor-drag');
|
|
950
|
+
else if (mode === 'longpress') cursor.classList.add('cursor-longpress');
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function showModeBadge(text, color) {
|
|
954
|
+
var badge = document.getElementById('modeBadge');
|
|
955
|
+
if (!badge) return;
|
|
956
|
+
badge.textContent = text;
|
|
957
|
+
badge.style.background = color;
|
|
958
|
+
badge.style.display = 'block';
|
|
531
959
|
}
|
|
960
|
+
|
|
961
|
+
function hideModeBadge() {
|
|
962
|
+
var badge = document.getElementById('modeBadge');
|
|
963
|
+
if (!badge) return;
|
|
964
|
+
badge.style.display = 'none';
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Touchpad toolbar setup (always available when mobile module is active)
|
|
968
|
+
var _toolbarSetupDone = false;
|
|
969
|
+
function setupToolbar() {
|
|
970
|
+
if (_toolbarSetupDone) return;
|
|
971
|
+
_toolbarSetupDone = true;
|
|
972
|
+
var toolbar = document.getElementById('touchpadToolbar');
|
|
973
|
+
if (toolbar) {
|
|
974
|
+
toolbar.addEventListener('click', function(e) {
|
|
975
|
+
var btn = e.target.closest ? e.target.closest('.tpk-key') : null;
|
|
976
|
+
if (!btn) return;
|
|
977
|
+
e.preventDefault();
|
|
978
|
+
e.stopPropagation();
|
|
979
|
+
sendUserActivity();
|
|
980
|
+
var key = btn.dataset.key || '';
|
|
981
|
+
var code = btn.dataset.code || '';
|
|
982
|
+
safeSend(JSON.stringify({ type: 'input_keyboard', eventType: 'keyDown', key: key, code: code, modifiers: 0 }));
|
|
983
|
+
safeSend(JSON.stringify({ type: 'input_keyboard', eventType: 'keyUp', key: key, code: code, modifiers: 0 }));
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
var expandBtn = document.getElementById('tpkExpand');
|
|
987
|
+
var collapseBtn = document.getElementById('tpkCollapse');
|
|
988
|
+
if (expandBtn) expandBtn.addEventListener('click', function(e) { e.stopPropagation(); toolbar.classList.remove('collapsed'); });
|
|
989
|
+
if (collapseBtn) collapseBtn.addEventListener('click', function(e) { e.stopPropagation(); toolbar.classList.add('collapsed'); });
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Touch event handlers (always registered; guarded by DeviceMode.current)
|
|
994
|
+
touchpad.addEventListener('touchstart', (e) => {
|
|
995
|
+
if (DeviceMode.current !== 'mobile') return;
|
|
996
|
+
e.preventDefault();
|
|
997
|
+
sendUserActivity();
|
|
998
|
+
|
|
999
|
+
if (e.touches.length === 2) {
|
|
1000
|
+
clearTimeout(longPressTimer);
|
|
1001
|
+
longPressTimer = null;
|
|
1002
|
+
dragMode = false;
|
|
1003
|
+
moveAllowed = false;
|
|
1004
|
+
lastTouchPos = null;
|
|
1005
|
+
momentumActive = false;
|
|
1006
|
+
setCursorMode(null);
|
|
1007
|
+
hideModeBadge();
|
|
1008
|
+
const t0 = e.touches[0];
|
|
1009
|
+
const t1 = e.touches[1];
|
|
1010
|
+
twoFingerStartPos = {
|
|
1011
|
+
lastMidX: (t0.clientX + t1.clientX) / 2,
|
|
1012
|
+
lastMidY: (t0.clientY + t1.clientY) / 2,
|
|
1013
|
+
};
|
|
1014
|
+
lastMoveTime = Date.now();
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (e.touches.length === 1) {
|
|
1019
|
+
if (Date.now() < moveCooldownUntil) return;
|
|
1020
|
+
moveAllowed = true;
|
|
1021
|
+
setCursorMode('move');
|
|
1022
|
+
showModeBadge('MOVE', 'rgba(68, 140, 255, 0.7)');
|
|
1023
|
+
const t = e.touches[0];
|
|
1024
|
+
lastTouchPos = { x: t.clientX, y: t.clientY };
|
|
1025
|
+
lastMoveTime = Date.now();
|
|
1026
|
+
touchMoved = false;
|
|
1027
|
+
|
|
1028
|
+
clearTimeout(longPressTimer);
|
|
1029
|
+
clearTimeout(longPressHintTimer);
|
|
1030
|
+
longPressTimer = setTimeout(() => {
|
|
1031
|
+
longPressTimer = null;
|
|
1032
|
+
longPressHintTimer = null;
|
|
1033
|
+
dragMode = true;
|
|
1034
|
+
isMouseDown = true;
|
|
1035
|
+
touchMoved = true;
|
|
1036
|
+
setCursorMode('drag');
|
|
1037
|
+
showModeBadge('DRAG', 'rgba(255, 165, 0, 0.8)');
|
|
1038
|
+
const pagePos = screenToPage(cursorPos.x, cursorPos.y);
|
|
1039
|
+
safeSend(JSON.stringify({
|
|
1040
|
+
type: 'input_mouse',
|
|
1041
|
+
eventType: 'mousePressed',
|
|
1042
|
+
x: pagePos.x,
|
|
1043
|
+
y: pagePos.y,
|
|
1044
|
+
button: 'left',
|
|
1045
|
+
clickCount: 1,
|
|
1046
|
+
modifiers: 0
|
|
1047
|
+
}));
|
|
1048
|
+
}, LONG_PRESS_MS);
|
|
1049
|
+
}
|
|
1050
|
+
}, { passive: false });
|
|
1051
|
+
|
|
1052
|
+
touchpad.addEventListener('touchend', (e) => {
|
|
1053
|
+
if (DeviceMode.current !== 'mobile') return;
|
|
1054
|
+
e.preventDefault();
|
|
1055
|
+
clearTimeout(longPressTimer);
|
|
1056
|
+
clearTimeout(longPressHintTimer);
|
|
1057
|
+
if (e.touches.length === 0) {
|
|
1058
|
+
if (twoFingerStartPos) {
|
|
1059
|
+
moveCooldownUntil = Date.now() + COOLDOWN_MS;
|
|
1060
|
+
if (Math.abs(lastWheelDeltaX) > 2 || Math.abs(lastWheelDeltaY) > 2) {
|
|
1061
|
+
startMomentum();
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
moveAllowed = false;
|
|
1065
|
+
setCursorMode(null);
|
|
1066
|
+
hideModeBadge();
|
|
1067
|
+
if (dragMode) {
|
|
1068
|
+
const pagePos = screenToPage(cursorPos.x, cursorPos.y);
|
|
1069
|
+
safeSend(JSON.stringify({
|
|
1070
|
+
type: 'input_mouse',
|
|
1071
|
+
eventType: 'mouseReleased',
|
|
1072
|
+
x: pagePos.x,
|
|
1073
|
+
y: pagePos.y,
|
|
1074
|
+
button: 'left',
|
|
1075
|
+
clickCount: 1,
|
|
1076
|
+
modifiers: 0
|
|
1077
|
+
}));
|
|
1078
|
+
dragMode = false;
|
|
1079
|
+
isMouseDown = false;
|
|
1080
|
+
} else if (!touchMoved) {
|
|
1081
|
+
// Single tap/click: send click event, then attempt focus
|
|
1082
|
+
const pagePos = screenToPage(cursorPos.x, cursorPos.y);
|
|
1083
|
+
|
|
1084
|
+
safeSend(JSON.stringify({
|
|
1085
|
+
type: 'input_mouse',
|
|
1086
|
+
eventType: 'mousePressed',
|
|
1087
|
+
x: pagePos.x,
|
|
1088
|
+
y: pagePos.y,
|
|
1089
|
+
button: 'left',
|
|
1090
|
+
clickCount: 1,
|
|
1091
|
+
modifiers: 0
|
|
1092
|
+
}));
|
|
1093
|
+
safeSend(JSON.stringify({
|
|
1094
|
+
type: 'input_mouse',
|
|
1095
|
+
eventType: 'mouseReleased',
|
|
1096
|
+
x: pagePos.x,
|
|
1097
|
+
y: pagePos.y,
|
|
1098
|
+
button: 'left',
|
|
1099
|
+
clickCount: 1,
|
|
1100
|
+
modifiers: 0
|
|
1101
|
+
}));
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
}
|
|
1105
|
+
lastTouchPos = null;
|
|
1106
|
+
twoFingerStartPos = null;
|
|
1107
|
+
touchMoved = false;
|
|
1108
|
+
}
|
|
1109
|
+
}, { passive: false });
|
|
1110
|
+
|
|
1111
|
+
touchpad.addEventListener('touchmove', (e) => {
|
|
1112
|
+
if (DeviceMode.current !== 'mobile') return;
|
|
1113
|
+
e.preventDefault();
|
|
1114
|
+
|
|
1115
|
+
if (e.touches.length === 2 && twoFingerStartPos) {
|
|
1116
|
+
clearTimeout(longPressTimer);
|
|
1117
|
+
longPressTimer = null;
|
|
1118
|
+
dragMode = false;
|
|
1119
|
+
moveAllowed = false;
|
|
1120
|
+
lastTouchPos = null;
|
|
1121
|
+
const t0 = e.touches[0];
|
|
1122
|
+
const t1 = e.touches[1];
|
|
1123
|
+
const midX = (t0.clientX + t1.clientX) / 2;
|
|
1124
|
+
const midY = (t0.clientY + t1.clientY) / 2;
|
|
1125
|
+
const rawDX = midX - twoFingerStartPos.lastMidX;
|
|
1126
|
+
const rawDY = midY - twoFingerStartPos.lastMidY;
|
|
1127
|
+
const wMult = computeWheelAccel(rawDX, rawDY);
|
|
1128
|
+
const deltaX = rawDX * wMult;
|
|
1129
|
+
const deltaY = rawDY * wMult;
|
|
1130
|
+
lastWheelDeltaX = deltaX;
|
|
1131
|
+
lastWheelDeltaY = deltaY;
|
|
1132
|
+
const pagePos = screenToPage(cursorPos.x, cursorPos.y);
|
|
1133
|
+
safeSend(JSON.stringify({
|
|
1134
|
+
type: 'input_mouse',
|
|
1135
|
+
eventType: 'mouseWheel',
|
|
1136
|
+
x: pagePos.x,
|
|
1137
|
+
y: pagePos.y,
|
|
1138
|
+
deltaX: deltaX,
|
|
1139
|
+
deltaY: deltaY,
|
|
1140
|
+
modifiers: 0
|
|
1141
|
+
}));
|
|
1142
|
+
twoFingerStartPos.lastMidX = midX;
|
|
1143
|
+
twoFingerStartPos.lastMidY = midY;
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
if (e.touches.length === 1 && lastTouchPos && moveAllowed) {
|
|
1148
|
+
const t = e.touches[0];
|
|
1149
|
+
const dx = t.clientX - lastTouchPos.x;
|
|
1150
|
+
const dy = t.clientY - lastTouchPos.y;
|
|
1151
|
+
|
|
1152
|
+
if (!touchMoved && Math.sqrt(dx * dx + dy * dy) > MOVE_THRESHOLD) {
|
|
1153
|
+
touchMoved = true;
|
|
1154
|
+
clearTimeout(longPressTimer);
|
|
1155
|
+
clearTimeout(longPressHintTimer);
|
|
1156
|
+
longPressTimer = null;
|
|
1157
|
+
longPressHintTimer = null;
|
|
1158
|
+
setCursorMode('move');
|
|
1159
|
+
showModeBadge('MOVE', 'rgba(68, 140, 255, 0.7)');
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
lastTouchPos = { x: t.clientX, y: t.clientY };
|
|
1163
|
+
|
|
1164
|
+
if (touchMoved) {
|
|
1165
|
+
sendUserActivity();
|
|
1166
|
+
if (!screenRect) updateScreenRect();
|
|
1167
|
+
const accel = computeAcceleration(dx, dy);
|
|
1168
|
+
cursorPos.x = clampCursor(cursorPos.x + dx * accel, screenRect.left, screenRect.right);
|
|
1169
|
+
cursorPos.y = clampCursor(cursorPos.y + dy * accel, screenRect.top, screenRect.bottom);
|
|
1170
|
+
updateCursor();
|
|
1171
|
+
|
|
1172
|
+
const pagePos = screenToPage(cursorPos.x, cursorPos.y);
|
|
1173
|
+
var dbg = document.getElementById('debug-overlay');
|
|
1174
|
+
if (dbg && (!window._moveDebugCount)) window._moveDebugCount = 0;
|
|
1175
|
+
if (dbg && window._moveDebugCount < 8) {
|
|
1176
|
+
window._moveDebugCount++;
|
|
1177
|
+
dbg.textContent += ' | cur:' + Math.round(cursorPos.x) + ',' + Math.round(cursorPos.y)
|
|
1178
|
+
+ ' -> page:' + pagePos.x + ',' + pagePos.y;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
safeSend(JSON.stringify({
|
|
1182
|
+
type: 'input_mouse',
|
|
1183
|
+
eventType: 'mouseMoved',
|
|
1184
|
+
x: pagePos.x,
|
|
1185
|
+
y: pagePos.y,
|
|
1186
|
+
button: dragMode ? 'left' : 'none',
|
|
1187
|
+
clickCount: 1,
|
|
1188
|
+
modifiers: 0
|
|
1189
|
+
}));
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}, { passive: false });
|
|
1193
|
+
|
|
1194
|
+
touchpad.addEventListener('touchcancel', () => {
|
|
1195
|
+
if (DeviceMode.current !== 'mobile') return;
|
|
1196
|
+
clearTimeout(longPressTimer);
|
|
1197
|
+
clearTimeout(longPressHintTimer);
|
|
1198
|
+
momentumActive = false;
|
|
1199
|
+
if (dragMode) {
|
|
1200
|
+
const pagePos = screenToPage(cursorPos.x, cursorPos.y);
|
|
1201
|
+
safeSend(JSON.stringify({
|
|
1202
|
+
type: 'input_mouse',
|
|
1203
|
+
eventType: 'mouseReleased',
|
|
1204
|
+
x: pagePos.x,
|
|
1205
|
+
y: pagePos.y,
|
|
1206
|
+
button: 'left',
|
|
1207
|
+
clickCount: 1,
|
|
1208
|
+
modifiers: 0
|
|
1209
|
+
}));
|
|
1210
|
+
}
|
|
1211
|
+
dragMode = false;
|
|
1212
|
+
isMouseDown = false;
|
|
1213
|
+
moveAllowed = false;
|
|
1214
|
+
setCursorMode(null);
|
|
1215
|
+
hideModeBadge();
|
|
1216
|
+
moveCooldownUntil = twoFingerStartPos ? Date.now() + COOLDOWN_MS : 0;
|
|
1217
|
+
lastTouchPos = null;
|
|
1218
|
+
twoFingerStartPos = null;
|
|
1219
|
+
touchMoved = false;
|
|
1220
|
+
}, { passive: false });
|
|
532
1221
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
1222
|
+
var inputSendBtn = document.getElementById('input-send');
|
|
1223
|
+
if (inputSendBtn) inputSendBtn.addEventListener('click', function(e) {
|
|
1224
|
+
e.preventDefault();
|
|
1225
|
+
e.stopPropagation();
|
|
1226
|
+
sendInputText();
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
document.addEventListener('pointerdown', function(e) {
|
|
1230
|
+
if (!inputMode) return;
|
|
1231
|
+
var panel = document.getElementById('input-panel');
|
|
1232
|
+
if (panel && !panel.contains(e.target)) {
|
|
1233
|
+
exitInputMode();
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
var inputField = document.getElementById('input-field');
|
|
1238
|
+
if (inputField) {
|
|
1239
|
+
inputField.addEventListener('input', function(e) {
|
|
1240
|
+
if (window._fieldComposing) return;
|
|
1241
|
+
syncInputToRemote(inputField);
|
|
1242
|
+
});
|
|
1243
|
+
inputField.addEventListener('compositionend', function(e) {
|
|
1244
|
+
window._fieldComposing = false;
|
|
1245
|
+
requestAnimationFrame(function() {
|
|
1246
|
+
requestAnimationFrame(function() {
|
|
1247
|
+
syncInputToRemote(inputField);
|
|
1248
|
+
});
|
|
1249
|
+
});
|
|
1250
|
+
});
|
|
1251
|
+
inputField.addEventListener('keydown', function(e) {
|
|
1252
|
+
if (e.key === 'Enter') {
|
|
1253
|
+
e.preventDefault();
|
|
1254
|
+
sendInputText();
|
|
1255
|
+
} else if (e.key === 'Escape') {
|
|
1256
|
+
e.preventDefault();
|
|
1257
|
+
exitInputMode();
|
|
1258
|
+
} else if (e.key === 'Backspace' || e.key === 'Delete') {
|
|
1259
|
+
// Let it fall through - the field value will change,
|
|
1260
|
+
// then syncInputToRemote will pick up the change and send input_fill
|
|
1261
|
+
}
|
|
536
1262
|
});
|
|
537
1263
|
}
|
|
538
1264
|
|
|
1265
|
+
// Image sizing: re-fit on container resize (phone rotation, window resize)
|
|
1266
|
+
var resizeTimer = null;
|
|
1267
|
+
window.addEventListener('resize', () => {
|
|
1268
|
+
clearTimeout(resizeTimer);
|
|
1269
|
+
resizeTimer = setTimeout(function() {
|
|
1270
|
+
fitImageToContainer();
|
|
1271
|
+
var newMode = detectDeviceMode();
|
|
1272
|
+
if (newMode !== DeviceMode.current) DeviceMode.switchTo(newMode);
|
|
1273
|
+
}, 100);
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
window.addEventListener('orientationchange', () => {
|
|
1277
|
+
setTimeout(function() {
|
|
1278
|
+
var newMode = detectDeviceMode();
|
|
1279
|
+
if (newMode !== DeviceMode.current) DeviceMode.switchTo(newMode);
|
|
1280
|
+
}, 200);
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
// matchMedia pointer:coarse as additional trigger
|
|
1284
|
+
if (window.matchMedia) {
|
|
1285
|
+
try {
|
|
1286
|
+
var mql = window.matchMedia('(pointer:coarse)');
|
|
1287
|
+
if (mql && typeof mql.addEventListener === 'function') {
|
|
1288
|
+
mql.addEventListener('change', function(e) {
|
|
1289
|
+
var newMode = e.matches ? 'mobile' : 'desktop';
|
|
1290
|
+
DeviceMode.switchTo(newMode);
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
} catch(err) {}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
539
1296
|
// Recorder functionality
|
|
540
1297
|
const recordBtn = document.getElementById('recordBtn');
|
|
541
1298
|
const recordText = document.getElementById('recordText');
|
|
@@ -579,7 +1336,7 @@ export function buildViewerScript() {
|
|
|
579
1336
|
}
|
|
580
1337
|
});
|
|
581
1338
|
|
|
582
|
-
focusHiddenInput();
|
|
1339
|
+
if (DeviceMode.current === 'desktop') focusHiddenInput();
|
|
583
1340
|
connect();
|
|
584
1341
|
`;
|
|
585
1342
|
}
|