claude-opencode-viewer 2.2.1 → 2.3.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/.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 +521 -336
- 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,163 @@
|
|
|
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
98
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
#virtual-keybar-row button {
|
|
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
|
-
|
|
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;
|
|
105
119
|
outline: none;
|
|
106
|
-
|
|
120
|
+
-webkit-user-select: none;
|
|
121
|
+
-moz-user-select: none;
|
|
122
|
+
-ms-user-select: none;
|
|
107
123
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
124
|
+
|
|
125
|
+
.virtual-key:active {
|
|
126
|
+
background: #333;
|
|
127
|
+
border-color: #555;
|
|
128
|
+
color: #fff;
|
|
112
129
|
}
|
|
113
130
|
</style>
|
|
114
131
|
</head>
|
|
115
132
|
<body>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
133
|
+
<!-- 参考 cc-viewer 的 App.jsx 行 1315-1607: 完整的移动端布局结构 -->
|
|
134
|
+
<div id="layout">
|
|
135
|
+
<div id="header">
|
|
136
|
+
<div style="font-size: 12px; color: #aaa;">Claude OpenCode Viewer</div>
|
|
137
|
+
<div id="mode-switcher">
|
|
138
|
+
<span id="mode-label">Mode:</span>
|
|
139
|
+
<select id="mode-select">
|
|
140
|
+
<option value="opencode">OpenCode</option>
|
|
141
|
+
<option value="claude">Claude</option>
|
|
142
|
+
</select>
|
|
143
|
+
</div>
|
|
125
144
|
</div>
|
|
126
|
-
</div>
|
|
127
145
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
146
|
+
<div id="content">
|
|
147
|
+
<div id="terminal-container">
|
|
148
|
+
<div id="terminal"></div>
|
|
149
|
+
<div id="virtual-keybar">
|
|
150
|
+
<div class="virtual-key" data-key="up">↑</div>
|
|
151
|
+
<div class="virtual-key" data-key="down">↓</div>
|
|
152
|
+
<div class="virtual-key" data-key="left">←</div>
|
|
153
|
+
<div class="virtual-key" data-key="right">→</div>
|
|
154
|
+
<div class="virtual-key" data-key="enter">Enter</div>
|
|
155
|
+
<div class="virtual-key" data-key="tab">Tab</div>
|
|
156
|
+
<div class="virtual-key" data-key="esc">Esc</div>
|
|
157
|
+
<div class="virtual-key" data-key="ctrlc">Ctrl+C</div>
|
|
158
|
+
<div class="virtual-key scroll-key" data-scroll="-5">⇡ 滚动</div>
|
|
159
|
+
<div class="virtual-key scroll-key" data-scroll="5">⇣ 滚动</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
138
162
|
</div>
|
|
139
163
|
</div>
|
|
140
164
|
|
|
@@ -166,7 +190,6 @@
|
|
|
166
190
|
|
|
167
191
|
term.open(document.getElementById('terminal'));
|
|
168
192
|
|
|
169
|
-
var keybar = document.getElementById('virtual-keybar');
|
|
170
193
|
var modeSelect = document.getElementById('mode-select');
|
|
171
194
|
var terminalEl = document.getElementById('terminal');
|
|
172
195
|
var ws = null;
|
|
@@ -180,25 +203,77 @@
|
|
|
180
203
|
var momentumRaf = null;
|
|
181
204
|
var velocitySamples = [];
|
|
182
205
|
|
|
206
|
+
// 未发送消息缓存
|
|
207
|
+
var currentInputBuffer = '';
|
|
208
|
+
var CACHE_KEY = 'claude_opencode_input_cache';
|
|
209
|
+
var cacheRestored = false; // 防止重复恢复
|
|
210
|
+
|
|
211
|
+
function saveInputCache() {
|
|
212
|
+
if (currentInputBuffer) {
|
|
213
|
+
localStorage.setItem(CACHE_KEY, currentInputBuffer);
|
|
214
|
+
console.log('[cache] saved:', currentInputBuffer);
|
|
215
|
+
} else {
|
|
216
|
+
localStorage.removeItem(CACHE_KEY);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function loadInputCache() {
|
|
221
|
+
// 确保只恢复一次
|
|
222
|
+
if (cacheRestored) {
|
|
223
|
+
console.log('[cache] already restored, skipping');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
var cached = localStorage.getItem(CACHE_KEY);
|
|
228
|
+
if (cached && ws && ws.readyState === 1) {
|
|
229
|
+
console.log('[cache] restoring:', cached);
|
|
230
|
+
// 立即清除缓存,防止重复调用
|
|
231
|
+
localStorage.removeItem(CACHE_KEY);
|
|
232
|
+
cacheRestored = true;
|
|
233
|
+
|
|
234
|
+
// 将缓存的输入发送到终端
|
|
235
|
+
ws.send(JSON.stringify({ type: 'input', data: cached }));
|
|
236
|
+
// 重要:恢复后清空 currentInputBuffer,防止再次保存
|
|
237
|
+
currentInputBuffer = '';
|
|
238
|
+
} else {
|
|
239
|
+
console.log('[cache] no cache to restore');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function clearInputCache() {
|
|
244
|
+
currentInputBuffer = '';
|
|
245
|
+
localStorage.removeItem(CACHE_KEY);
|
|
246
|
+
console.log('[cache] cleared');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function getCellDims() {
|
|
250
|
+
return term._core && term._core._renderService && term._core._renderService.dimensions && term._core._renderService.dimensions.css && term._core._renderService.dimensions.css.cell;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 参考 cc-viewer 的 App.jsx 行 76-94: iOS visualViewport 处理
|
|
254
|
+
// iOS 虚拟键盘弹出时,Safari 会滚动整个文档将页面上推,
|
|
255
|
+
// 导致导航栏消失在视口之外。通过 visualViewport 的 resize + scroll
|
|
256
|
+
// 事件同步可见区域的高度和偏移,用 fixed 定位将布局锁定在可见区域内。
|
|
183
257
|
if (isIOS && window.visualViewport) {
|
|
184
|
-
var
|
|
258
|
+
var layoutEl = document.getElementById('layout');
|
|
185
259
|
var onVVChange = function() {
|
|
260
|
+
if (!layoutEl) return;
|
|
186
261
|
var vv = window.visualViewport;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
262
|
+
layoutEl.style.position = 'fixed';
|
|
263
|
+
layoutEl.style.top = vv.offsetTop + 'px';
|
|
264
|
+
layoutEl.style.height = vv.height + 'px';
|
|
265
|
+
layoutEl.style.width = '100%';
|
|
266
|
+
layoutEl.style.left = '0';
|
|
191
267
|
setTimeout(mobileFixedResize, 50);
|
|
192
268
|
};
|
|
193
269
|
window.visualViewport.addEventListener('resize', onVVChange);
|
|
194
270
|
window.visualViewport.addEventListener('scroll', onVVChange);
|
|
271
|
+
onVVChange();
|
|
195
272
|
}
|
|
196
273
|
|
|
197
|
-
|
|
198
|
-
return term._core && term._core._renderService && term._core._renderService.dimensions && term._core._renderService.dimensions.css && term._core._renderService.dimensions.css.cell;
|
|
199
|
-
}
|
|
200
|
-
|
|
274
|
+
// 参考 cc-viewer 的 TerminalPanel.jsx 行 410-447: 移动端固定尺寸计算
|
|
201
275
|
function mobileFixedResize() {
|
|
276
|
+
if (!term) return;
|
|
202
277
|
var cellDims = getCellDims();
|
|
203
278
|
if (!cellDims || !cellDims.width || !cellDims.height) {
|
|
204
279
|
setTimeout(mobileFixedResize, 50);
|
|
@@ -207,17 +282,15 @@
|
|
|
207
282
|
|
|
208
283
|
var padX = 16;
|
|
209
284
|
var padY = 8;
|
|
210
|
-
var topBarHeight =
|
|
211
|
-
var
|
|
212
|
-
var keybarHeight = isMobile ? 96 : 0;
|
|
285
|
+
var topBarHeight = 40;
|
|
286
|
+
var keybarHeight = 52;
|
|
213
287
|
|
|
214
288
|
var availW = window.innerWidth - padX;
|
|
215
|
-
var availH = window.innerHeight - topBarHeight -
|
|
289
|
+
var availH = window.innerHeight - topBarHeight - keybarHeight - padY;
|
|
216
290
|
|
|
217
291
|
var currentFontSize = term.options.fontSize;
|
|
218
292
|
var currentCharWidth = cellDims.width;
|
|
219
293
|
var targetFontSize = Math.floor(currentFontSize * availW / (MOBILE_COLS * currentCharWidth) * 10) / 10;
|
|
220
|
-
targetFontSize = Math.max(8, targetFontSize);
|
|
221
294
|
|
|
222
295
|
term.options.fontSize = targetFontSize;
|
|
223
296
|
|
|
@@ -278,94 +351,189 @@
|
|
|
278
351
|
pixelAccum = 0;
|
|
279
352
|
}
|
|
280
353
|
|
|
354
|
+
// OpenCode 模式触摸滚动实现 - 使用 xterm.js scrollLines API
|
|
355
|
+
var touchScreen = null;
|
|
356
|
+
var touchEventsBound = false;
|
|
357
|
+
|
|
358
|
+
function getLineHeight() {
|
|
359
|
+
var cellDims = getCellDims();
|
|
360
|
+
var height = (cellDims && cellDims.height) || 15;
|
|
361
|
+
console.log('[scroll] lineHeight:', height);
|
|
362
|
+
return height;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function stopMomentum() {
|
|
366
|
+
if (momentumRaf) {
|
|
367
|
+
cancelAnimationFrame(momentumRaf);
|
|
368
|
+
momentumRaf = null;
|
|
369
|
+
}
|
|
370
|
+
if (scrollRaf) {
|
|
371
|
+
cancelAnimationFrame(scrollRaf);
|
|
372
|
+
scrollRaf = null;
|
|
373
|
+
}
|
|
374
|
+
pendingDy = 0;
|
|
375
|
+
pixelAccum = 0;
|
|
376
|
+
}
|
|
377
|
+
|
|
281
378
|
function flushScroll() {
|
|
282
379
|
scrollRaf = null;
|
|
283
380
|
if (pendingDy === 0) return;
|
|
381
|
+
|
|
284
382
|
pixelAccum += pendingDy;
|
|
285
383
|
pendingDy = 0;
|
|
286
|
-
|
|
287
|
-
var lh = (
|
|
384
|
+
|
|
385
|
+
var lh = getLineHeight();
|
|
288
386
|
var lines = Math.trunc(pixelAccum / lh);
|
|
289
|
-
|
|
387
|
+
|
|
388
|
+
if (lines !== 0) {
|
|
389
|
+
console.log('[scroll] scrollLines:', lines, 'pixelAccum:', pixelAccum, 'lineHeight:', lh);
|
|
390
|
+
term.scrollLines(lines);
|
|
391
|
+
pixelAccum -= lines * lh;
|
|
392
|
+
}
|
|
290
393
|
}
|
|
291
394
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
velocitySamples = [];
|
|
301
|
-
}, { passive: true });
|
|
395
|
+
function handleTouchStart(e) {
|
|
396
|
+
console.log('[scroll] touchstart');
|
|
397
|
+
stopMomentum();
|
|
398
|
+
if (e.touches.length !== 1) return;
|
|
399
|
+
lastY = e.touches[0].clientY;
|
|
400
|
+
lastTime = performance.now();
|
|
401
|
+
velocitySamples = [];
|
|
402
|
+
}
|
|
302
403
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
lastTime = now;
|
|
318
|
-
}, { passive: true });
|
|
404
|
+
function handleTouchMove(e) {
|
|
405
|
+
if (e.touches.length !== 1) return;
|
|
406
|
+
var y = e.touches[0].clientY;
|
|
407
|
+
var now = performance.now();
|
|
408
|
+
var dt = now - lastTime;
|
|
409
|
+
var dy = lastY - y;
|
|
410
|
+
|
|
411
|
+
if (dt > 0) {
|
|
412
|
+
var v = dy / dt * 16;
|
|
413
|
+
velocitySamples.push({ v: v, t: now });
|
|
414
|
+
while (velocitySamples.length > 0 && now - velocitySamples[0].t > 100) {
|
|
415
|
+
velocitySamples.shift();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
319
418
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
419
|
+
pendingDy += dy;
|
|
420
|
+
console.log('[scroll] touchmove dy:', dy, 'pendingDy:', pendingDy);
|
|
421
|
+
|
|
422
|
+
if (!scrollRaf) scrollRaf = requestAnimationFrame(flushScroll);
|
|
423
|
+
lastY = y;
|
|
424
|
+
lastTime = now;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function handleTouchEnd() {
|
|
428
|
+
console.log('[scroll] touchend');
|
|
429
|
+
|
|
430
|
+
if (scrollRaf) {
|
|
431
|
+
cancelAnimationFrame(scrollRaf);
|
|
432
|
+
scrollRaf = null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (pendingDy !== 0) {
|
|
436
|
+
pixelAccum += pendingDy;
|
|
437
|
+
pendingDy = 0;
|
|
438
|
+
var lh = getLineHeight();
|
|
439
|
+
var lines = Math.trunc(pixelAccum / lh);
|
|
440
|
+
if (lines !== 0) {
|
|
441
|
+
console.log('[scroll] final scrollLines:', lines);
|
|
442
|
+
term.scrollLines(lines);
|
|
443
|
+
}
|
|
444
|
+
pixelAccum = 0;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
var velocity = 0;
|
|
448
|
+
if (velocitySamples.length >= 2) {
|
|
449
|
+
var totalWeight = 0;
|
|
450
|
+
var weightedV = 0;
|
|
451
|
+
var latest = velocitySamples[velocitySamples.length - 1].t;
|
|
452
|
+
for (var i = 0; i < velocitySamples.length; i++) {
|
|
453
|
+
var s = velocitySamples[i];
|
|
454
|
+
var w = Math.max(0, 1 - (latest - s.t) / 100);
|
|
455
|
+
weightedV += s.v * w;
|
|
456
|
+
totalWeight += w;
|
|
457
|
+
}
|
|
458
|
+
velocity = totalWeight > 0 ? weightedV / totalWeight : 0;
|
|
459
|
+
}
|
|
460
|
+
velocitySamples = [];
|
|
461
|
+
|
|
462
|
+
console.log('[scroll] velocity:', velocity);
|
|
463
|
+
if (Math.abs(velocity) < 0.5) return;
|
|
464
|
+
|
|
465
|
+
var friction = 0.95;
|
|
466
|
+
var mAccum = 0;
|
|
467
|
+
var tick = function() {
|
|
468
|
+
if (Math.abs(velocity) < 0.3) {
|
|
469
|
+
var lh = getLineHeight();
|
|
470
|
+
var rest = Math.round(mAccum / lh);
|
|
471
|
+
if (rest !== 0) {
|
|
472
|
+
console.log('[scroll] momentum final:', rest);
|
|
473
|
+
term.scrollLines(rest);
|
|
342
474
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
475
|
+
momentumRaf = null;
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
mAccum += velocity;
|
|
479
|
+
var lh = getLineHeight();
|
|
480
|
+
var lines = Math.trunc(mAccum / lh);
|
|
481
|
+
if (lines !== 0) {
|
|
482
|
+
term.scrollLines(lines);
|
|
483
|
+
mAccum -= lines * lh;
|
|
484
|
+
}
|
|
485
|
+
velocity *= friction;
|
|
486
|
+
momentumRaf = requestAnimationFrame(tick);
|
|
487
|
+
};
|
|
488
|
+
momentumRaf = requestAnimationFrame(tick);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function unbindTouchScroll() {
|
|
492
|
+
if (touchScreen && touchEventsBound) {
|
|
493
|
+
console.log('[scroll] unbinding touch events');
|
|
494
|
+
touchScreen.removeEventListener('touchstart', handleTouchStart);
|
|
495
|
+
touchScreen.removeEventListener('touchmove', handleTouchMove);
|
|
496
|
+
touchScreen.removeEventListener('touchend', handleTouchEnd);
|
|
497
|
+
touchEventsBound = false;
|
|
498
|
+
touchScreen = null;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function setupMobileTouchScroll() {
|
|
503
|
+
// 先解绑旧的
|
|
504
|
+
unbindTouchScroll();
|
|
505
|
+
|
|
506
|
+
// 只在 opencode 模式下绑定
|
|
507
|
+
if (currentMode !== 'opencode') {
|
|
508
|
+
console.log('[scroll] Not opencode mode, skip binding');
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
var screen = terminalEl.querySelector('.xterm-screen');
|
|
513
|
+
if (!screen) {
|
|
514
|
+
console.log('[scroll] .xterm-screen not found, retrying...');
|
|
515
|
+
setTimeout(setupMobileTouchScroll, 50);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
touchScreen = screen;
|
|
520
|
+
screen.addEventListener('touchstart', handleTouchStart, { passive: true });
|
|
521
|
+
screen.addEventListener('touchmove', handleTouchMove, { passive: true });
|
|
522
|
+
screen.addEventListener('touchend', handleTouchEnd, { passive: true });
|
|
523
|
+
touchEventsBound = true;
|
|
524
|
+
console.log('[scroll] Touch events bound to .xterm-screen (opencode mode)');
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function rebindTouchScroll() {
|
|
528
|
+
if (isMobile) {
|
|
529
|
+
setTimeout(setupMobileTouchScroll, 100);
|
|
366
530
|
}
|
|
367
531
|
}
|
|
368
532
|
|
|
533
|
+
if (isMobile) {
|
|
534
|
+
setupMobileTouchScroll();
|
|
535
|
+
}
|
|
536
|
+
|
|
369
537
|
function startTransition() {
|
|
370
538
|
isTransitioning = true;
|
|
371
539
|
terminalEl.classList.add('transitioning');
|
|
@@ -411,6 +579,8 @@
|
|
|
411
579
|
else if (msg.type === 'exit') throttledWrite('\r\n[进程已退出: ' + msg.exitCode + ']\r\n');
|
|
412
580
|
else if (msg.type === 'mode') {
|
|
413
581
|
endTransition(msg.mode);
|
|
582
|
+
// 模式切换完成后,重新绑定触摸事件
|
|
583
|
+
rebindTouchScroll();
|
|
414
584
|
}
|
|
415
585
|
else if (msg.type === 'switching') {
|
|
416
586
|
// 服务端开始切换,前端清屏
|
|
@@ -428,67 +598,71 @@
|
|
|
428
598
|
};
|
|
429
599
|
|
|
430
600
|
term.onData(function(d) {
|
|
431
|
-
if (ws && ws.readyState === 1)
|
|
601
|
+
if (ws && ws.readyState === 1) {
|
|
602
|
+
ws.send(JSON.stringify({ type: 'input', data: d }));
|
|
603
|
+
|
|
604
|
+
// 缓存管理:跟踪当前行的输入(仅在 opencode 模式且未在恢复中)
|
|
605
|
+
if (currentMode === 'opencode' && cacheRestored) {
|
|
606
|
+
if (d === '\r' || d === '\n' || d === '\r\n') {
|
|
607
|
+
// 回车:命令已发送,清除缓存
|
|
608
|
+
clearInputCache();
|
|
609
|
+
} else if (d === '\x7f' || d === '\b') {
|
|
610
|
+
// 退格:删除最后一个字符
|
|
611
|
+
if (currentInputBuffer.length > 0) {
|
|
612
|
+
currentInputBuffer = currentInputBuffer.slice(0, -1);
|
|
613
|
+
saveInputCache();
|
|
614
|
+
}
|
|
615
|
+
} else if (d === '\x03') {
|
|
616
|
+
// Ctrl+C:中断,清除缓存
|
|
617
|
+
clearInputCache();
|
|
618
|
+
} else if (d === '\x15') {
|
|
619
|
+
// Ctrl+U:清空整行
|
|
620
|
+
clearInputCache();
|
|
621
|
+
} else if (d.length === 1 && d.charCodeAt(0) >= 32 && d.charCodeAt(0) < 127) {
|
|
622
|
+
// 可打印 ASCII 字符:添加到缓冲区
|
|
623
|
+
currentInputBuffer += d;
|
|
624
|
+
saveInputCache();
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
432
629
|
// 点击终端输入时,滚动到底部
|
|
433
630
|
setTimeout(function() {
|
|
434
631
|
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
|
435
632
|
}, 50);
|
|
436
633
|
});
|
|
437
|
-
}
|
|
438
634
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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);
|
|
635
|
+
// 页面加载后尝试恢复缓存的输入(仅在 opencode 模式)
|
|
636
|
+
setTimeout(function() {
|
|
637
|
+
if (currentMode === 'opencode') {
|
|
638
|
+
loadInputCache();
|
|
463
639
|
} else {
|
|
464
|
-
|
|
640
|
+
// 非 opencode 模式,标记为已恢复,避免后续缓存逻辑干扰
|
|
641
|
+
cacheRestored = true;
|
|
465
642
|
}
|
|
466
|
-
});
|
|
643
|
+
}, 800);
|
|
644
|
+
}
|
|
467
645
|
|
|
468
|
-
|
|
469
|
-
|
|
646
|
+
window.addEventListener('resize', resize);
|
|
647
|
+
if (isMobile) {
|
|
648
|
+
window.addEventListener('orientationchange', function() {
|
|
649
|
+
setTimeout(resize, 200);
|
|
470
650
|
});
|
|
651
|
+
}
|
|
471
652
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
ws.send(JSON.stringify({ type: 'input', data: value + '\r\n' }));
|
|
479
|
-
inputField.value = '';
|
|
480
|
-
}
|
|
481
|
-
e.preventDefault();
|
|
482
|
-
}
|
|
483
|
-
});
|
|
653
|
+
// 页面卸载前保存输入缓存
|
|
654
|
+
window.addEventListener('beforeunload', function() {
|
|
655
|
+
if (currentInputBuffer) {
|
|
656
|
+
saveInputCache();
|
|
657
|
+
}
|
|
658
|
+
});
|
|
484
659
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}, { passive: true });
|
|
660
|
+
// 页面可见性变化时保存缓存
|
|
661
|
+
document.addEventListener('visibilitychange', function() {
|
|
662
|
+
if (document.hidden && currentInputBuffer) {
|
|
663
|
+
saveInputCache();
|
|
490
664
|
}
|
|
491
|
-
}
|
|
665
|
+
});
|
|
492
666
|
|
|
493
667
|
// 虚拟按键映射表
|
|
494
668
|
var KEY_MAP = {
|
|
@@ -498,124 +672,135 @@
|
|
|
498
672
|
'right': '\x1b[C',
|
|
499
673
|
'esc': '\x1b',
|
|
500
674
|
'ctrlc': '\x03',
|
|
501
|
-
'tab': '\t'
|
|
675
|
+
'tab': '\t',
|
|
676
|
+
'enter': '\r'
|
|
502
677
|
};
|
|
503
678
|
|
|
504
|
-
// 方向键列表
|
|
505
|
-
var ARROW_KEYS = ['up', 'down', 'left', 'right'];
|
|
506
|
-
|
|
507
679
|
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
|
-
}
|
|
680
|
+
var seq = KEY_MAP[keyName];
|
|
681
|
+
if (seq && ws && ws.readyState === 1 && !isTransitioning) {
|
|
682
|
+
ws.send(JSON.stringify({ type: 'input', data: seq }));
|
|
536
683
|
}
|
|
684
|
+
}
|
|
537
685
|
|
|
538
|
-
|
|
539
|
-
if (
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
var value = inputField.value;
|
|
543
|
-
console.log('[enter] from input:', value);
|
|
544
|
-
ws.send(JSON.stringify({ type: 'input', data: value + '\r\n' }));
|
|
545
|
-
inputField.value = '';
|
|
546
|
-
inputField.blur();
|
|
547
|
-
} else if (isInputFocused && inputField) {
|
|
548
|
-
// 焦点在输入框但内容为空:不发送任何内容,避免误触
|
|
549
|
-
console.log('[enter] input focused but empty, ignored');
|
|
550
|
-
} else {
|
|
551
|
-
// 焦点在终端:直接发送回车
|
|
552
|
-
console.log('[enter] sending \\r to terminal');
|
|
553
|
-
ws.send(JSON.stringify({ type: 'input', data: '\r\n' }));
|
|
554
|
-
}
|
|
555
|
-
return;
|
|
686
|
+
function scrollTerminal(lines) {
|
|
687
|
+
if (term) {
|
|
688
|
+
term.scrollLines(lines);
|
|
689
|
+
console.log('[scroll] scrolled by:', lines);
|
|
556
690
|
}
|
|
691
|
+
}
|
|
557
692
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
693
|
+
// 参考 cc-viewer 的 TerminalPanel.jsx 行 519-546: 虚拟按键触摸处理
|
|
694
|
+
// 每个按键独立绑定事件,不在容器上绑定
|
|
695
|
+
var vkStartX = 0, vkStartY = 0, vkMoved = false, vkTarget = null;
|
|
696
|
+
var scrollInterval = null; // 长按滚动定时器
|
|
697
|
+
|
|
698
|
+
function setupVirtualKeyEvents() {
|
|
699
|
+
var keys = document.querySelectorAll('.virtual-key');
|
|
700
|
+
keys.forEach(function(key) {
|
|
701
|
+
// 防止元素获得焦点
|
|
702
|
+
key.setAttribute('tabindex', '-1');
|
|
703
|
+
|
|
704
|
+
// 移动端触摸事件
|
|
705
|
+
key.addEventListener('touchstart', function(e) {
|
|
706
|
+
var touch = e.touches[0];
|
|
707
|
+
vkStartX = touch.clientX;
|
|
708
|
+
vkStartY = touch.clientY;
|
|
709
|
+
vkMoved = false;
|
|
710
|
+
vkTarget = e.currentTarget;
|
|
711
|
+
vkTarget.style.background = '#333';
|
|
712
|
+
|
|
713
|
+
// 如果是滚动按钮,阻止默认行为(防止键盘弹出)并启动持续滚动
|
|
714
|
+
var scrollLines = e.currentTarget.getAttribute('data-scroll');
|
|
715
|
+
if (scrollLines) {
|
|
716
|
+
e.preventDefault(); // 关键:阻止默认行为,防止键盘弹出
|
|
717
|
+
scrollTerminal(parseInt(scrollLines, 10));
|
|
718
|
+
scrollInterval = setInterval(function() {
|
|
719
|
+
scrollTerminal(parseInt(scrollLines, 10));
|
|
720
|
+
}, 100);
|
|
721
|
+
}
|
|
722
|
+
}, { passive: false }); // 必须是 false 才能调用 preventDefault()
|
|
723
|
+
|
|
724
|
+
key.addEventListener('touchmove', function(e) {
|
|
725
|
+
if (vkMoved) return;
|
|
726
|
+
var touch = e.touches[0];
|
|
727
|
+
var dx = touch.clientX - vkStartX;
|
|
728
|
+
var dy = touch.clientY - vkStartY;
|
|
729
|
+
if (dx * dx + dy * dy > 64) { // 8px 阈值
|
|
730
|
+
vkMoved = true;
|
|
731
|
+
if (vkTarget) {
|
|
732
|
+
vkTarget.style.background = '';
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}, { passive: true });
|
|
564
736
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
console.log('[sendKey] skipped:', keyName, 'ws:', ws?.readyState, 'transitioning:', isTransitioning);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
737
|
+
key.addEventListener('touchend', function(e) {
|
|
738
|
+
// 清除滚动定时器
|
|
739
|
+
if (scrollInterval) {
|
|
740
|
+
clearInterval(scrollInterval);
|
|
741
|
+
scrollInterval = null;
|
|
742
|
+
}
|
|
574
743
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
744
|
+
if (vkTarget) {
|
|
745
|
+
vkTarget.style.background = '';
|
|
746
|
+
vkTarget = null;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// 如果没有移动,触发按键功能并阻止默认行为
|
|
750
|
+
if (!vkMoved) {
|
|
751
|
+
e.preventDefault(); // 阻止默认行为
|
|
752
|
+
var scrollLines = e.currentTarget.getAttribute('data-scroll');
|
|
753
|
+
if (!scrollLines) {
|
|
754
|
+
// 非滚动按钮才触发按键
|
|
755
|
+
var keyName = e.currentTarget.getAttribute('data-key');
|
|
756
|
+
sendKey(keyName);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}, { passive: false });
|
|
760
|
+
|
|
761
|
+
// PC端点击支持
|
|
762
|
+
key.addEventListener('click', function(e) {
|
|
763
|
+
e.preventDefault();
|
|
764
|
+
var scrollLines = e.currentTarget.getAttribute('data-scroll');
|
|
765
|
+
if (scrollLines) {
|
|
766
|
+
scrollTerminal(parseInt(scrollLines, 10));
|
|
767
|
+
} else {
|
|
768
|
+
var keyName = e.currentTarget.getAttribute('data-key');
|
|
769
|
+
sendKey(keyName);
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// PC端鼠标按下支持(长按滚动)
|
|
774
|
+
key.addEventListener('mousedown', function(e) {
|
|
775
|
+
e.preventDefault();
|
|
776
|
+
var scrollLines = e.currentTarget.getAttribute('data-scroll');
|
|
777
|
+
if (scrollLines) {
|
|
778
|
+
scrollTerminal(parseInt(scrollLines, 10));
|
|
779
|
+
scrollInterval = setInterval(function() {
|
|
780
|
+
scrollTerminal(parseInt(scrollLines, 10));
|
|
781
|
+
}, 100);
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
key.addEventListener('mouseup', function(e) {
|
|
786
|
+
if (scrollInterval) {
|
|
787
|
+
clearInterval(scrollInterval);
|
|
788
|
+
scrollInterval = null;
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
key.addEventListener('mouseleave', function(e) {
|
|
793
|
+
if (scrollInterval) {
|
|
794
|
+
clearInterval(scrollInterval);
|
|
795
|
+
scrollInterval = null;
|
|
796
|
+
}
|
|
797
|
+
});
|
|
616
798
|
});
|
|
617
799
|
}
|
|
618
800
|
|
|
801
|
+
// 初始化虚拟按键事件
|
|
802
|
+
setupVirtualKeyEvents();
|
|
803
|
+
|
|
619
804
|
connect();
|
|
620
805
|
setTimeout(resize, 100);
|
|
621
806
|
})();
|