claude-opencode-viewer 2.2.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +11 -0
- package/.idea/claude_opencode_viewer.iml +9 -0
- package/.idea/inspectionProfiles/Project_Default.xml +5 -0
- package/.idea/modules.xml +8 -0
- package/.idea/smartfox/common_info.xml +9 -0
- package/.idea/tdkConfig.xml +6 -0
- package/.idea/vcs.xml +6 -0
- package/index.html +198 -274
- package/package.json +1 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="JAVA_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
|
4
|
+
<exclude-output />
|
|
5
|
+
<content url="file://$MODULE_DIR$" />
|
|
6
|
+
<orderEntry type="inheritedJdk" />
|
|
7
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
8
|
+
</component>
|
|
9
|
+
</module>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="ProjectModuleManager">
|
|
4
|
+
<modules>
|
|
5
|
+
<module fileurl="file://$PROJECT_DIR$/.idea/claude_opencode_viewer.iml" filepath="$PROJECT_DIR$/.idea/claude_opencode_viewer.iml" />
|
|
6
|
+
</modules>
|
|
7
|
+
</component>
|
|
8
|
+
</project>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="SmartFoxWorkToolsProjectState">
|
|
4
|
+
<option name="branch" value="fix/v2.1.0-keyboard" />
|
|
5
|
+
<option name="gitUrl" value="https://github.com/ChrisJason121238/claude-opencode-viewer.git" />
|
|
6
|
+
<option name="localBranch" value="fix/v2.1.0-keyboard" />
|
|
7
|
+
<option name="revision" value="e81a3afd4423f60c1d4f185dd4b16cf7ad1848cc" />
|
|
8
|
+
</component>
|
|
9
|
+
</project>
|
package/.idea/vcs.xml
ADDED
package/index.html
CHANGED
|
@@ -2,139 +2,157 @@
|
|
|
2
2
|
<html lang="zh-CN">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0,
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
|
6
6
|
<title>Claude OpenCode Viewer</title>
|
|
7
7
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
|
|
8
8
|
<style>
|
|
9
9
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
10
|
-
html, body {
|
|
11
|
-
|
|
10
|
+
html, body { margin: 0; padding: 0; overflow: hidden; }
|
|
11
|
+
|
|
12
|
+
/* 参考 cc-viewer 的 App.jsx 行 1319: 移动端容器使用 100vw/100vh */
|
|
13
|
+
#layout {
|
|
14
|
+
width: 100vw;
|
|
15
|
+
height: 100vh;
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
background: #000;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* 参考 cc-viewer 的 App.jsx 行 1320-1335: 顶部工具栏 */
|
|
23
|
+
#header {
|
|
24
|
+
padding: 10px 12px;
|
|
25
|
+
background: #111;
|
|
26
|
+
border-bottom: 1px solid #222;
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: space-between;
|
|
30
|
+
flex-shrink: 0;
|
|
31
|
+
height: 40px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#mode-switcher {
|
|
35
|
+
display: flex;
|
|
36
|
+
gap: 4px;
|
|
37
|
+
align-items: center;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#mode-label {
|
|
41
|
+
font-size: 12px;
|
|
42
|
+
color: #666;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#mode-select {
|
|
46
|
+
background: #1a1a1a;
|
|
47
|
+
color: #d4d4d4;
|
|
48
|
+
border: 1px solid #333;
|
|
49
|
+
border-radius: 4px;
|
|
50
|
+
padding: 4px 24px 4px 8px;
|
|
51
|
+
font-size: 12px;
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
-webkit-appearance: none;
|
|
54
|
+
appearance: none;
|
|
55
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 10 10'%3E%3Cpath fill='%23888' d='M5 7L1 3h8z'/%3E%3C/svg%3E");
|
|
56
|
+
background-repeat: no-repeat;
|
|
57
|
+
background-position: right 6px center;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* 参考 cc-viewer 的 App.jsx 行 1425: 内容区使用 flex: 1 + relative + overflow: hidden */
|
|
61
|
+
#content {
|
|
62
|
+
flex: 1;
|
|
63
|
+
position: relative;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* 参考 cc-viewer 的 TerminalPanel.jsx: 终端容器样式 */
|
|
68
|
+
#terminal-container {
|
|
69
|
+
height: 100%;
|
|
12
70
|
display: flex;
|
|
13
71
|
flex-direction: column;
|
|
14
72
|
background: #0a0a0a;
|
|
15
73
|
}
|
|
74
|
+
|
|
16
75
|
#terminal {
|
|
17
76
|
flex: 1;
|
|
18
|
-
min-height: 0;
|
|
19
77
|
overflow: hidden;
|
|
20
78
|
padding: 4px 8px;
|
|
21
79
|
touch-action: none;
|
|
22
80
|
overscroll-behavior: contain;
|
|
23
81
|
}
|
|
82
|
+
|
|
24
83
|
#terminal.transitioning {
|
|
25
84
|
opacity: 0.3;
|
|
26
85
|
transition: opacity 0.3s ease;
|
|
27
86
|
}
|
|
87
|
+
|
|
88
|
+
/* 参考 cc-viewer 的 TerminalPanel.module.css: 虚拟键盘栏 */
|
|
28
89
|
#virtual-keybar {
|
|
29
|
-
display: none;
|
|
30
|
-
background: #111;
|
|
31
|
-
border-top: 1px solid #222;
|
|
32
|
-
flex-shrink: 0;
|
|
33
|
-
}
|
|
34
|
-
#virtual-keybar-row {
|
|
35
90
|
display: flex;
|
|
36
91
|
gap: 6px;
|
|
37
92
|
padding: 8px 10px;
|
|
93
|
+
background: #111;
|
|
94
|
+
border-top: 1px solid #222;
|
|
38
95
|
overflow-x: auto;
|
|
96
|
+
flex-shrink: 0;
|
|
39
97
|
-webkit-overflow-scrolling: touch;
|
|
40
|
-
scrollbar-width: none;
|
|
41
|
-
}
|
|
42
|
-
#virtual-keybar-row::-webkit-scrollbar {
|
|
43
|
-
display: none;
|
|
44
98
|
}
|
|
45
|
-
|
|
99
|
+
|
|
100
|
+
.virtual-key {
|
|
46
101
|
flex-shrink: 0;
|
|
47
|
-
|
|
48
|
-
height: 40px;
|
|
102
|
+
padding: 12px 20px;
|
|
49
103
|
border: 1px solid #333;
|
|
50
|
-
border-radius:
|
|
104
|
+
border-radius: 8px;
|
|
51
105
|
background: #1a1a1a;
|
|
52
106
|
color: #ccc;
|
|
53
|
-
font-size:
|
|
54
|
-
touch-action: manipulation;
|
|
55
|
-
white-space: nowrap;
|
|
56
|
-
}
|
|
57
|
-
#virtual-keybar-row button:active {
|
|
58
|
-
background: #333;
|
|
59
|
-
}
|
|
60
|
-
#input-bar {
|
|
61
|
-
display: none;
|
|
62
|
-
background: #111;
|
|
63
|
-
border-top: 1px solid #222;
|
|
64
|
-
flex-shrink: 0;
|
|
65
|
-
padding: 8px 10px;
|
|
66
|
-
display: flex;
|
|
67
|
-
gap: 8px;
|
|
68
|
-
align-items: center;
|
|
69
|
-
}
|
|
70
|
-
#input-bar input {
|
|
71
|
-
flex: 1;
|
|
72
|
-
min-width: 0;
|
|
73
|
-
padding: 10px 12px;
|
|
74
|
-
background: #1a1a1a;
|
|
75
|
-
border: 1px solid #333;
|
|
76
|
-
border-radius: 6px;
|
|
77
|
-
color: #d4d4d4;
|
|
78
|
-
font-size: 14px;
|
|
107
|
+
font-size: 15px;
|
|
79
108
|
font-family: Menlo, Monaco, monospace;
|
|
80
|
-
outline: none;
|
|
81
|
-
}
|
|
82
|
-
#input-bar input:focus {
|
|
83
|
-
border-color: #4ade80;
|
|
84
|
-
}
|
|
85
|
-
#mode-switcher {
|
|
86
|
-
display: none;
|
|
87
|
-
flex-shrink: 0;
|
|
88
|
-
}
|
|
89
|
-
#mode-switcher select {
|
|
90
|
-
background: #1a1a1a;
|
|
91
|
-
color: #d4d4d4;
|
|
92
|
-
border: 1px solid #333;
|
|
93
|
-
border-radius: 4px;
|
|
94
|
-
padding: 10px 28px 10px 12px;
|
|
95
|
-
font-size: 13px;
|
|
96
109
|
cursor: pointer;
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
border-color: #4ade80;
|
|
110
|
+
user-select: none;
|
|
111
|
+
-webkit-tap-highlight-color: transparent;
|
|
112
|
+
touch-action: pan-x;
|
|
113
|
+
min-width: 44px;
|
|
114
|
+
min-height: 44px;
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
justify-content: center;
|
|
118
|
+
border: none;
|
|
107
119
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
|
|
121
|
+
.virtual-key:active {
|
|
122
|
+
background: #333;
|
|
123
|
+
border-color: #555;
|
|
124
|
+
color: #fff;
|
|
112
125
|
}
|
|
113
126
|
</style>
|
|
114
127
|
</head>
|
|
115
128
|
<body>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
129
|
+
<!-- 参考 cc-viewer 的 App.jsx 行 1315-1607: 完整的移动端布局结构 -->
|
|
130
|
+
<div id="layout">
|
|
131
|
+
<div id="header">
|
|
132
|
+
<div style="font-size: 12px; color: #aaa;">Claude OpenCode Viewer</div>
|
|
133
|
+
<div id="mode-switcher">
|
|
134
|
+
<span id="mode-label">Mode:</span>
|
|
135
|
+
<select id="mode-select">
|
|
136
|
+
<option value="opencode">OpenCode</option>
|
|
137
|
+
<option value="claude">Claude</option>
|
|
138
|
+
</select>
|
|
139
|
+
</div>
|
|
125
140
|
</div>
|
|
126
|
-
</div>
|
|
127
141
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
142
|
+
<div id="content">
|
|
143
|
+
<div id="terminal-container">
|
|
144
|
+
<div id="terminal"></div>
|
|
145
|
+
<div id="virtual-keybar">
|
|
146
|
+
<button class="virtual-key" data-key="up">↑</button>
|
|
147
|
+
<button class="virtual-key" data-key="down">↓</button>
|
|
148
|
+
<button class="virtual-key" data-key="left">←</button>
|
|
149
|
+
<button class="virtual-key" data-key="right">→</button>
|
|
150
|
+
<button class="virtual-key" data-key="enter">Enter</button>
|
|
151
|
+
<button class="virtual-key" data-key="tab">Tab</button>
|
|
152
|
+
<button class="virtual-key" data-key="esc">Esc</button>
|
|
153
|
+
<button class="virtual-key" data-key="ctrlc">Ctrl+C</button>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
138
156
|
</div>
|
|
139
157
|
</div>
|
|
140
158
|
|
|
@@ -166,7 +184,6 @@
|
|
|
166
184
|
|
|
167
185
|
term.open(document.getElementById('terminal'));
|
|
168
186
|
|
|
169
|
-
var keybar = document.getElementById('virtual-keybar');
|
|
170
187
|
var modeSelect = document.getElementById('mode-select');
|
|
171
188
|
var terminalEl = document.getElementById('terminal');
|
|
172
189
|
var ws = null;
|
|
@@ -180,25 +197,34 @@
|
|
|
180
197
|
var momentumRaf = null;
|
|
181
198
|
var velocitySamples = [];
|
|
182
199
|
|
|
200
|
+
// 参考 cc-viewer 的 App.jsx 行 76-94: iOS visualViewport 处理
|
|
201
|
+
// iOS 虚拟键盘弹出时,Safari 会滚动整个文档将页面上推,
|
|
202
|
+
// 导致导航栏消失在视口之外。通过 visualViewport 的 resize + scroll
|
|
203
|
+
// 事件同步可见区域的高度和偏移,用 fixed 定位将布局锁定在可见区域内。
|
|
183
204
|
if (isIOS && window.visualViewport) {
|
|
184
|
-
var
|
|
205
|
+
var layoutEl = document.getElementById('layout');
|
|
185
206
|
var onVVChange = function() {
|
|
207
|
+
if (!layoutEl) return;
|
|
186
208
|
var vv = window.visualViewport;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
209
|
+
layoutEl.style.position = 'fixed';
|
|
210
|
+
layoutEl.style.top = vv.offsetTop + 'px';
|
|
211
|
+
layoutEl.style.height = vv.height + 'px';
|
|
212
|
+
layoutEl.style.width = '100%';
|
|
213
|
+
layoutEl.style.left = '0';
|
|
191
214
|
setTimeout(mobileFixedResize, 50);
|
|
192
215
|
};
|
|
193
216
|
window.visualViewport.addEventListener('resize', onVVChange);
|
|
194
217
|
window.visualViewport.addEventListener('scroll', onVVChange);
|
|
218
|
+
onVVChange();
|
|
195
219
|
}
|
|
196
220
|
|
|
197
221
|
function getCellDims() {
|
|
198
222
|
return term._core && term._core._renderService && term._core._renderService.dimensions && term._core._renderService.dimensions.css && term._core._renderService.dimensions.css.cell;
|
|
199
223
|
}
|
|
200
224
|
|
|
225
|
+
// 参考 cc-viewer 的 TerminalPanel.jsx 行 410-447: 移动端固定尺寸计算
|
|
201
226
|
function mobileFixedResize() {
|
|
227
|
+
if (!term) return;
|
|
202
228
|
var cellDims = getCellDims();
|
|
203
229
|
if (!cellDims || !cellDims.width || !cellDims.height) {
|
|
204
230
|
setTimeout(mobileFixedResize, 50);
|
|
@@ -207,17 +233,15 @@
|
|
|
207
233
|
|
|
208
234
|
var padX = 16;
|
|
209
235
|
var padY = 8;
|
|
210
|
-
var topBarHeight =
|
|
211
|
-
var
|
|
212
|
-
var keybarHeight = isMobile ? 96 : 0;
|
|
236
|
+
var topBarHeight = 40;
|
|
237
|
+
var keybarHeight = 52;
|
|
213
238
|
|
|
214
239
|
var availW = window.innerWidth - padX;
|
|
215
|
-
var availH = window.innerHeight - topBarHeight -
|
|
240
|
+
var availH = window.innerHeight - topBarHeight - keybarHeight - padY;
|
|
216
241
|
|
|
217
242
|
var currentFontSize = term.options.fontSize;
|
|
218
243
|
var currentCharWidth = cellDims.width;
|
|
219
244
|
var targetFontSize = Math.floor(currentFontSize * availW / (MOBILE_COLS * currentCharWidth) * 10) / 10;
|
|
220
|
-
targetFontSize = Math.max(8, targetFontSize);
|
|
221
245
|
|
|
222
246
|
term.options.fontSize = targetFontSize;
|
|
223
247
|
|
|
@@ -278,20 +302,26 @@
|
|
|
278
302
|
pixelAccum = 0;
|
|
279
303
|
}
|
|
280
304
|
|
|
305
|
+
// 参考 cc-viewer 的 TerminalPanel.jsx 行 172-309: 移动端触摸滚动完整实现
|
|
281
306
|
function flushScroll() {
|
|
282
307
|
scrollRaf = null;
|
|
283
308
|
if (pendingDy === 0) return;
|
|
284
309
|
pixelAccum += pendingDy;
|
|
285
310
|
pendingDy = 0;
|
|
286
311
|
var cellDims = getCellDims();
|
|
287
|
-
var lh = (cellDims && cellDims.height) ||
|
|
312
|
+
var lh = (cellDims && cellDims.height) || 15;
|
|
288
313
|
var lines = Math.trunc(pixelAccum / lh);
|
|
289
|
-
if (lines !== 0) {
|
|
314
|
+
if (lines !== 0) {
|
|
315
|
+
term.scrollLines(lines);
|
|
316
|
+
pixelAccum -= lines * lh;
|
|
317
|
+
}
|
|
290
318
|
}
|
|
291
319
|
|
|
292
320
|
if (isMobile) {
|
|
293
|
-
|
|
294
|
-
|
|
321
|
+
setTimeout(function() {
|
|
322
|
+
var screen = document.querySelector('.xterm-screen');
|
|
323
|
+
if (!screen) return;
|
|
324
|
+
|
|
295
325
|
screen.addEventListener('touchstart', function(e) {
|
|
296
326
|
stopMomentum();
|
|
297
327
|
if (e.touches.length !== 1) return;
|
|
@@ -309,7 +339,9 @@
|
|
|
309
339
|
if (dt > 0) {
|
|
310
340
|
var v = dy / dt * 16;
|
|
311
341
|
velocitySamples.push({ v: v, t: now });
|
|
312
|
-
while (velocitySamples.length > 0 && now - velocitySamples[0].t > 100)
|
|
342
|
+
while (velocitySamples.length > 0 && now - velocitySamples[0].t > 100) {
|
|
343
|
+
velocitySamples.shift();
|
|
344
|
+
}
|
|
313
345
|
}
|
|
314
346
|
pendingDy += dy;
|
|
315
347
|
if (!scrollRaf) scrollRaf = requestAnimationFrame(flushScroll);
|
|
@@ -318,19 +350,24 @@
|
|
|
318
350
|
}, { passive: true });
|
|
319
351
|
|
|
320
352
|
screen.addEventListener('touchend', function() {
|
|
321
|
-
if (scrollRaf) {
|
|
353
|
+
if (scrollRaf) {
|
|
354
|
+
cancelAnimationFrame(scrollRaf);
|
|
355
|
+
scrollRaf = null;
|
|
356
|
+
}
|
|
322
357
|
if (pendingDy !== 0) {
|
|
323
358
|
pixelAccum += pendingDy;
|
|
324
359
|
pendingDy = 0;
|
|
325
360
|
var cellDims = getCellDims();
|
|
326
|
-
var lh = (cellDims && cellDims.height) ||
|
|
361
|
+
var lh = (cellDims && cellDims.height) || 15;
|
|
327
362
|
var lines = Math.trunc(pixelAccum / lh);
|
|
328
363
|
if (lines !== 0) term.scrollLines(lines);
|
|
329
364
|
pixelAccum = 0;
|
|
330
365
|
}
|
|
366
|
+
|
|
331
367
|
var velocity = 0;
|
|
332
368
|
if (velocitySamples.length >= 2) {
|
|
333
|
-
var totalWeight = 0
|
|
369
|
+
var totalWeight = 0;
|
|
370
|
+
var weightedV = 0;
|
|
334
371
|
var latest = velocitySamples[velocitySamples.length - 1].t;
|
|
335
372
|
for (var i = 0; i < velocitySamples.length; i++) {
|
|
336
373
|
var s = velocitySamples[i];
|
|
@@ -341,13 +378,14 @@
|
|
|
341
378
|
velocity = totalWeight > 0 ? weightedV / totalWeight : 0;
|
|
342
379
|
}
|
|
343
380
|
velocitySamples = [];
|
|
381
|
+
|
|
344
382
|
if (Math.abs(velocity) < 0.5) return;
|
|
345
383
|
var friction = 0.95;
|
|
346
384
|
var mAccum = 0;
|
|
347
385
|
var tick = function() {
|
|
348
386
|
if (Math.abs(velocity) < 0.3) {
|
|
349
387
|
var cellDims = getCellDims();
|
|
350
|
-
var lh = (cellDims && cellDims.height) ||
|
|
388
|
+
var lh = (cellDims && cellDims.height) || 15;
|
|
351
389
|
var rest = Math.round(mAccum / lh);
|
|
352
390
|
if (rest !== 0) term.scrollLines(rest);
|
|
353
391
|
momentumRaf = null;
|
|
@@ -355,15 +393,18 @@
|
|
|
355
393
|
}
|
|
356
394
|
mAccum += velocity;
|
|
357
395
|
var cellDims = getCellDims();
|
|
358
|
-
var lh = (cellDims && cellDims.height) ||
|
|
396
|
+
var lh = (cellDims && cellDims.height) || 15;
|
|
359
397
|
var lines = Math.trunc(mAccum / lh);
|
|
360
|
-
if (lines !== 0) {
|
|
398
|
+
if (lines !== 0) {
|
|
399
|
+
term.scrollLines(lines);
|
|
400
|
+
mAccum -= lines * lh;
|
|
401
|
+
}
|
|
361
402
|
velocity *= friction;
|
|
362
403
|
momentumRaf = requestAnimationFrame(tick);
|
|
363
404
|
};
|
|
364
405
|
momentumRaf = requestAnimationFrame(tick);
|
|
365
406
|
}, { passive: true });
|
|
366
|
-
}
|
|
407
|
+
}, 100);
|
|
367
408
|
}
|
|
368
409
|
|
|
369
410
|
function startTransition() {
|
|
@@ -438,56 +479,9 @@
|
|
|
438
479
|
|
|
439
480
|
window.addEventListener('resize', resize);
|
|
440
481
|
if (isMobile) {
|
|
441
|
-
window.addEventListener('orientationchange', function() {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
// 输入框状态
|
|
445
|
-
var inputField = document.getElementById('input-field');
|
|
446
|
-
var inputFocused = false;
|
|
447
|
-
|
|
448
|
-
if (inputField) {
|
|
449
|
-
// 跟踪输入框焦点状态
|
|
450
|
-
inputField.addEventListener('focus', function() {
|
|
451
|
-
inputFocused = true;
|
|
452
|
-
// iOS 上需要临时恢复 body 滚动能力
|
|
453
|
-
if (isIOS && document.body.style.position === 'fixed') {
|
|
454
|
-
var scrollTop = -parseFloat(document.body.style.top || '0');
|
|
455
|
-
document.body.style.position = '';
|
|
456
|
-
document.body.style.top = '';
|
|
457
|
-
window.scrollTo(0, document.body.scrollHeight);
|
|
458
|
-
// 恢复 fixed 定位
|
|
459
|
-
setTimeout(function() {
|
|
460
|
-
document.body.style.position = 'fixed';
|
|
461
|
-
document.body.style.top = '-' + scrollTop + 'px';
|
|
462
|
-
}, 300);
|
|
463
|
-
} else {
|
|
464
|
-
window.scrollTo(0, document.body.scrollHeight);
|
|
465
|
-
}
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
inputField.addEventListener('blur', function() {
|
|
469
|
-
inputFocused = false;
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
// 输入框回车处理
|
|
473
|
-
inputField.addEventListener('keydown', function(e) {
|
|
474
|
-
if (e.key === 'Enter' && ws && ws.readyState === 1 && !isTransitioning) {
|
|
475
|
-
var value = inputField.value;
|
|
476
|
-
if (value) {
|
|
477
|
-
console.log('[input enter] sending:', value);
|
|
478
|
-
ws.send(JSON.stringify({ type: 'input', data: value + '\r\n' }));
|
|
479
|
-
inputField.value = '';
|
|
480
|
-
}
|
|
481
|
-
e.preventDefault();
|
|
482
|
-
}
|
|
482
|
+
window.addEventListener('orientationchange', function() {
|
|
483
|
+
setTimeout(resize, 200);
|
|
483
484
|
});
|
|
484
|
-
|
|
485
|
-
// 移动端触摸输入框时阻止终端获焦
|
|
486
|
-
if (isMobile) {
|
|
487
|
-
inputField.addEventListener('touchstart', function(e) {
|
|
488
|
-
e.stopPropagation();
|
|
489
|
-
}, { passive: true });
|
|
490
|
-
}
|
|
491
485
|
}
|
|
492
486
|
|
|
493
487
|
// 虚拟按键映射表
|
|
@@ -498,123 +492,53 @@
|
|
|
498
492
|
'right': '\x1b[C',
|
|
499
493
|
'esc': '\x1b',
|
|
500
494
|
'ctrlc': '\x03',
|
|
501
|
-
'tab': '\t'
|
|
495
|
+
'tab': '\t',
|
|
496
|
+
'enter': '\r'
|
|
502
497
|
};
|
|
503
498
|
|
|
504
|
-
// 方向键列表
|
|
505
|
-
var ARROW_KEYS = ['up', 'down', 'left', 'right'];
|
|
506
|
-
|
|
507
499
|
function sendKey(keyName) {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
// 方向键特殊处理:根据焦点位置决定行为
|
|
512
|
-
if (ARROW_KEYS.indexOf(keyName) !== -1) {
|
|
513
|
-
if (isInputFocused) {
|
|
514
|
-
// 焦点在输入框:模拟方向键操作输入框光标
|
|
515
|
-
var cursorPos = inputField.selectionStart;
|
|
516
|
-
var textLen = inputField.value.length;
|
|
517
|
-
|
|
518
|
-
if (keyName === 'left' && cursorPos > 0) {
|
|
519
|
-
inputField.setSelectionRange(cursorPos - 1, cursorPos - 1);
|
|
520
|
-
} else if (keyName === 'right' && cursorPos < textLen) {
|
|
521
|
-
inputField.setSelectionRange(cursorPos + 1, cursorPos + 1);
|
|
522
|
-
}
|
|
523
|
-
// 上下方向键在单行输入框中无操作
|
|
524
|
-
return;
|
|
525
|
-
} else {
|
|
526
|
-
// 焦点在终端:发送方向键到终端,并收起键盘
|
|
527
|
-
if (isMobile) {
|
|
528
|
-
inputField.blur();
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
} else {
|
|
532
|
-
// 非方向键:点击时收起键盘
|
|
533
|
-
if (isMobile) {
|
|
534
|
-
inputField.blur();
|
|
535
|
-
}
|
|
500
|
+
var seq = KEY_MAP[keyName];
|
|
501
|
+
if (seq && ws && ws.readyState === 1 && !isTransitioning) {
|
|
502
|
+
ws.send(JSON.stringify({ type: 'input', data: seq }));
|
|
536
503
|
}
|
|
504
|
+
}
|
|
537
505
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
console.log('[enter] sending \\r to terminal');
|
|
553
|
-
ws.send(JSON.stringify({ type: 'input', data: '\r\n' }));
|
|
554
|
-
}
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
506
|
+
// 参考 cc-viewer 的 TerminalPanel.jsx 行 519-546: 虚拟按键触摸处理
|
|
507
|
+
var keybar = document.getElementById('virtual-keybar');
|
|
508
|
+
var vkStartX = 0, vkStartY = 0, vkMoved = false, vkTarget = null;
|
|
509
|
+
|
|
510
|
+
keybar.addEventListener('touchstart', function(e) {
|
|
511
|
+
if (!e.target.classList.contains('virtual-key')) return;
|
|
512
|
+
e.preventDefault();
|
|
513
|
+
var touch = e.touches[0];
|
|
514
|
+
vkStartX = touch.clientX;
|
|
515
|
+
vkStartY = touch.clientY;
|
|
516
|
+
vkMoved = false;
|
|
517
|
+
vkTarget = e.target;
|
|
518
|
+
vkTarget.style.background = '#333';
|
|
519
|
+
});
|
|
557
520
|
|
|
558
|
-
|
|
559
|
-
if (
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
521
|
+
keybar.addEventListener('touchmove', function(e) {
|
|
522
|
+
if (vkMoved) return;
|
|
523
|
+
var touch = e.touches[0];
|
|
524
|
+
var dx = touch.clientX - vkStartX;
|
|
525
|
+
var dy = touch.clientY - vkStartY;
|
|
526
|
+
if (dx * dx + dy * dy > 64) {
|
|
527
|
+
vkMoved = true;
|
|
563
528
|
}
|
|
529
|
+
});
|
|
564
530
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if (
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
} else {
|
|
571
|
-
console.log('[sendKey] skipped:', keyName, 'ws:', ws?.readyState, 'transitioning:', isTransitioning);
|
|
531
|
+
keybar.addEventListener('touchend', function(e) {
|
|
532
|
+
e.preventDefault();
|
|
533
|
+
if (vkTarget) {
|
|
534
|
+
vkTarget.style.background = '';
|
|
535
|
+
vkTarget = null;
|
|
572
536
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
var vkStartX = 0, vkStartY = 0, vkMoved = false;
|
|
579
|
-
|
|
580
|
-
keybar.addEventListener('touchstart', function(e) {
|
|
581
|
-
if (e.target.tagName !== 'BUTTON') return;
|
|
582
|
-
var touch = e.touches[0];
|
|
583
|
-
vkStartX = touch.clientX;
|
|
584
|
-
vkStartY = touch.clientY;
|
|
585
|
-
vkMoved = false;
|
|
586
|
-
e.target.style.background = '#333';
|
|
587
|
-
}, { passive: true });
|
|
588
|
-
|
|
589
|
-
keybar.addEventListener('touchmove', function(e) {
|
|
590
|
-
if (e.target.tagName !== 'BUTTON') return;
|
|
591
|
-
if (vkMoved) return;
|
|
592
|
-
var touch = e.touches[0];
|
|
593
|
-
var dx = touch.clientX - vkStartX;
|
|
594
|
-
var dy = touch.clientY - vkStartY;
|
|
595
|
-
if (dx * dx + dy * dy > 100) { // 10px 阈值,允许横向滑动
|
|
596
|
-
vkMoved = true;
|
|
597
|
-
}
|
|
598
|
-
}, { passive: true });
|
|
599
|
-
|
|
600
|
-
keybar.addEventListener('touchend', function(e) {
|
|
601
|
-
if (e.target.tagName !== 'BUTTON') return;
|
|
602
|
-
e.preventDefault();
|
|
603
|
-
e.target.style.background = '';
|
|
604
|
-
if (!vkMoved) {
|
|
605
|
-
var keyName = e.target.getAttribute('data-key');
|
|
606
|
-
sendKey(keyName);
|
|
607
|
-
}
|
|
608
|
-
}, { passive: false });
|
|
609
|
-
} else {
|
|
610
|
-
// PC 端:直接用 click
|
|
611
|
-
keybar.addEventListener('click', function(e) {
|
|
612
|
-
if (e.target.tagName === 'BUTTON') {
|
|
613
|
-
var keyName = e.target.getAttribute('data-key');
|
|
614
|
-
sendKey(keyName);
|
|
615
|
-
}
|
|
616
|
-
});
|
|
617
|
-
}
|
|
537
|
+
if (!vkMoved && e.target.classList.contains('virtual-key')) {
|
|
538
|
+
var keyName = e.target.getAttribute('data-key');
|
|
539
|
+
sendKey(keyName);
|
|
540
|
+
}
|
|
541
|
+
});
|
|
618
542
|
|
|
619
543
|
connect();
|
|
620
544
|
setTimeout(resize, 100);
|