@restregular/dsh-visual-browser 0.1.0 → 0.2.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/README.md +57 -21
- package/client.js +1077 -437
- package/lib/browser.js +1255 -963
- package/lib/cdp.js +208 -192
- package/lib/routes.js +101 -47
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +6 -6
- package/test/format.test.mjs +62 -0
- package/test/package.test.mjs +68 -0
- package/test/routes.test.mjs +248 -0
- package/test/scripts.test.mjs +214 -0
package/client.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client half of dsh-visual-browser.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* A tab type for the right Sidebar: a live picture of the page Chrome is
|
|
5
|
+
* showing, a guide card under the built-in Browser entry that opens it, and a
|
|
6
|
+
* collapsible operation log. Pictures arrive as one long MJPEG response that the
|
|
7
|
+
* <img> element decodes natively; pointer, wheel and keyboard events go out in
|
|
8
|
+
* animation-frame batches, so a click costs one round trip instead of a
|
|
9
|
+
* screenshot. The whole file is an IIFE because every client bundle shares one
|
|
10
|
+
* global lexical scope inside the page.
|
|
8
11
|
*/
|
|
9
12
|
(function () {
|
|
10
13
|
'use strict';
|
|
@@ -12,10 +15,17 @@
|
|
|
12
15
|
var MODULE_ID = '@restregular/dsh-visual-browser';
|
|
13
16
|
var API = '/visual-browser/api';
|
|
14
17
|
var NS = 'dsh-visual-browser';
|
|
18
|
+
var KIND = 'visual-browser';
|
|
19
|
+
var BROWSER_TYPE_ID = '@deepseek-ai/dsh-client-ui-sidebar-browser';
|
|
20
|
+
var BROWSER_KIND = 'browser';
|
|
15
21
|
|
|
16
22
|
var DICT = {
|
|
17
23
|
zh: {
|
|
18
|
-
'panel.title': '
|
|
24
|
+
'panel.title': '可视化浏览器',
|
|
25
|
+
'panel.subtitle': '实时操作 Chrome',
|
|
26
|
+
'guide.title': '可视化浏览器',
|
|
27
|
+
'guide.description': '在这里实时操作 Chrome',
|
|
28
|
+
'guide.open': '在可视化浏览器中打开',
|
|
19
29
|
'state.running': '运行中',
|
|
20
30
|
'state.starting': '启动中',
|
|
21
31
|
'state.stopped': '未启动',
|
|
@@ -25,19 +35,21 @@
|
|
|
25
35
|
'action.back': '后退',
|
|
26
36
|
'action.forward': '前进',
|
|
27
37
|
'action.reload': '刷新页面',
|
|
28
|
-
'action.capture': '截取画面',
|
|
29
|
-
'action.live': '实时画面',
|
|
30
38
|
'action.open': '打开',
|
|
39
|
+
'action.reconnect': '重新连接画面',
|
|
31
40
|
'url.placeholder': '输入网址,例如 example.com',
|
|
32
|
-
'hint.interactive': '
|
|
33
|
-
'hint.headless': '
|
|
41
|
+
'hint.interactive': '直接用鼠标操作画面;点一下画面后可用键盘输入',
|
|
42
|
+
'hint.headless': '无头浏览器:窗口不可见,画面只在这里',
|
|
34
43
|
'empty.title': '浏览器还没启动',
|
|
35
|
-
'empty.desc': '在上方输入网址打开,或者让 Agent 用 browser_open
|
|
44
|
+
'empty.desc': '在上方输入网址打开,或者让 Agent 用 browser_open 打开一个页面;之后每次操作都会在这里实时显示。',
|
|
45
|
+
'connecting': '正在连接画面…',
|
|
36
46
|
'timeline.title': '操作记录',
|
|
37
|
-
'timeline.empty': '还没有操作。Agent
|
|
38
|
-
'timeline.
|
|
39
|
-
'timeline.
|
|
47
|
+
'timeline.empty': '还没有操作。Agent 的点击、输入与你自己的操作都会记录在这里。',
|
|
48
|
+
'timeline.count': '条',
|
|
49
|
+
'timeline.expand': '展开操作记录',
|
|
50
|
+
'timeline.collapse': '折叠操作记录',
|
|
40
51
|
'error.offline': '连不上宿主路由(/visual-browser/api),请确认插件已启用且宿主仍在运行。',
|
|
52
|
+
'error.frame': '画面流中断,正在重连…',
|
|
41
53
|
'kind.start': '启动',
|
|
42
54
|
'kind.close': '关闭',
|
|
43
55
|
'kind.open': '打开',
|
|
@@ -55,12 +67,18 @@
|
|
|
55
67
|
'kind.screenshot': '截图',
|
|
56
68
|
'kind.dialog': '对话框',
|
|
57
69
|
'kind.user-click': '你点击',
|
|
70
|
+
'kind.user-drag': '你拖动',
|
|
58
71
|
'kind.user-scroll': '你滚动',
|
|
59
72
|
'kind.user-key': '你按键',
|
|
73
|
+
'kind.user-type': '你输入',
|
|
60
74
|
'busy': '处理中…',
|
|
61
75
|
},
|
|
62
76
|
en: {
|
|
63
|
-
'panel.title': 'Browser',
|
|
77
|
+
'panel.title': 'Visual Browser',
|
|
78
|
+
'panel.subtitle': 'Drive Chrome live',
|
|
79
|
+
'guide.title': 'Visual Browser',
|
|
80
|
+
'guide.description': 'Drive Chrome here, live',
|
|
81
|
+
'guide.open': 'Open in the Visual Browser',
|
|
64
82
|
'state.running': 'Running',
|
|
65
83
|
'state.starting': 'Starting',
|
|
66
84
|
'state.stopped': 'Stopped',
|
|
@@ -70,19 +88,21 @@
|
|
|
70
88
|
'action.back': 'Back',
|
|
71
89
|
'action.forward': 'Forward',
|
|
72
90
|
'action.reload': 'Reload page',
|
|
73
|
-
'action.capture': 'Refresh frame',
|
|
74
|
-
'action.live': 'Live view',
|
|
75
91
|
'action.open': 'Open',
|
|
92
|
+
'action.reconnect': 'Reconnect the picture',
|
|
76
93
|
'url.placeholder': 'Enter a URL, for example example.com',
|
|
77
|
-
'hint.interactive': 'Click the picture
|
|
78
|
-
'hint.headless': 'Headless browser:
|
|
94
|
+
'hint.interactive': 'Click straight into the picture; click once to type with the keyboard',
|
|
95
|
+
'hint.headless': 'Headless browser: no window to watch, the picture here is the browser',
|
|
79
96
|
'empty.title': 'The browser is not running yet',
|
|
80
|
-
'empty.desc': 'Open a URL above, or let the Agent call browser_open.
|
|
97
|
+
'empty.desc': 'Open a URL above, or let the Agent call browser_open. Everything that happens then shows up here live.',
|
|
98
|
+
'connecting': 'Connecting the picture…',
|
|
81
99
|
'timeline.title': 'Actions',
|
|
82
|
-
'timeline.empty': 'No actions yet.
|
|
83
|
-
'timeline.
|
|
84
|
-
'timeline.
|
|
100
|
+
'timeline.empty': 'No actions yet. Agent clicks, typing, and your own gestures are listed here.',
|
|
101
|
+
'timeline.count': 'items',
|
|
102
|
+
'timeline.expand': 'Expand the action log',
|
|
103
|
+
'timeline.collapse': 'Collapse the action log',
|
|
85
104
|
'error.offline': 'Cannot reach the host route (/visual-browser/api). Check that the plugin is enabled and the host is running.',
|
|
105
|
+
'error.frame': 'The picture stream dropped; reconnecting…',
|
|
86
106
|
'kind.start': 'start',
|
|
87
107
|
'kind.close': 'close',
|
|
88
108
|
'kind.open': 'open',
|
|
@@ -100,56 +120,78 @@
|
|
|
100
120
|
'kind.screenshot': 'shot',
|
|
101
121
|
'kind.dialog': 'dialog',
|
|
102
122
|
'kind.user-click': 'you clicked',
|
|
123
|
+
'kind.user-drag': 'you dragged',
|
|
103
124
|
'kind.user-scroll': 'you scrolled',
|
|
104
125
|
'kind.user-key': 'you pressed',
|
|
126
|
+
'kind.user-type': 'you typed',
|
|
105
127
|
'busy': 'Working…',
|
|
106
128
|
},
|
|
107
129
|
};
|
|
108
130
|
|
|
109
131
|
var CSS = [
|
|
110
|
-
|
|
111
|
-
'.dvb-
|
|
112
|
-
'.dvb-
|
|
113
|
-
'.dvb-dot{width:8px;height:8px;border-radius:50%;flex:none;background:var(--dsw-alias-state-idle-primary)}',
|
|
132
|
+
// ---- tab body -------------------------------------------------------
|
|
133
|
+
'.dvb-root{display:flex;flex-direction:column;width:100%;height:100%;min-height:0;overflow:hidden;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5}',
|
|
134
|
+
'.dvb-bar{flex:none;display:flex;align-items:center;gap:6px;padding:8px 10px 6px;min-width:0}',
|
|
135
|
+
'.dvb-dot{width:8px;height:8px;border-radius:50%;flex:none;background:var(--dsw-alias-state-idle-primary,var(--dsw-alias-border-l3))}',
|
|
114
136
|
'.dvb-dot[data-on="true"]{background:var(--dsw-alias-state-success-primary)}',
|
|
115
137
|
'.dvb-dot[data-error="true"]{background:var(--dsw-alias-state-error-primary)}',
|
|
116
|
-
'.dvb-
|
|
117
|
-
'.dvb-status{font-size:
|
|
118
|
-
'.dvb-
|
|
119
|
-
'.dvb-tools{display:flex;align-items:center;gap:
|
|
120
|
-
'.dvb-btn{display:inline-flex;align-items:center;justify-content:center;gap:
|
|
121
|
-
'.dvb-btn:hover:not(:disabled){background:var(--dsw-alias-bg-
|
|
122
|
-
'.dvb-btn:disabled{opacity:.
|
|
123
|
-
'.dvb-btn[data-
|
|
124
|
-
'.dvb-btn[data-variant="primary"]{background:var(--dsw-alias-brand-primary);
|
|
125
|
-
'.dvb-
|
|
126
|
-
'.dvb-
|
|
127
|
-
'.dvb-input{
|
|
128
|
-
'.dvb-input
|
|
129
|
-
|
|
130
|
-
'.dvb-stage{flex:1 1 auto;min-height:
|
|
131
|
-
'.dvb-
|
|
132
|
-
'.dvb-
|
|
133
|
-
'.dvb-
|
|
134
|
-
'.dvb-
|
|
135
|
-
'.dvb-
|
|
136
|
-
'.dvb-
|
|
137
|
-
'.dvb-
|
|
138
|
-
'.dvb-
|
|
139
|
-
'.dvb-
|
|
140
|
-
'.dvb-
|
|
141
|
-
'.dvb-
|
|
142
|
-
|
|
143
|
-
'.dvb-
|
|
144
|
-
'.dvb-
|
|
145
|
-
'.dvb-
|
|
146
|
-
'.dvb-
|
|
147
|
-
'.dvb-
|
|
148
|
-
'.dvb-
|
|
138
|
+
'.dvb-spacer{flex:1 1 auto;min-width:6px}',
|
|
139
|
+
'.dvb-status{font-size:11.5px;color:var(--dsw-alias-label-secondary);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
140
|
+
'.dvb-title{font-weight:600;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}',
|
|
141
|
+
'.dvb-tools{display:flex;align-items:center;gap:2px;flex:none}',
|
|
142
|
+
'.dvb-btn{display:inline-flex;align-items:center;justify-content:center;gap:5px;height:26px;min-width:26px;padding:0 7px;border-radius:7px;border:1px solid transparent;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit;font-size:12px}',
|
|
143
|
+
'.dvb-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
|
|
144
|
+
'.dvb-btn:disabled{opacity:.4;cursor:default}',
|
|
145
|
+
'.dvb-btn[data-variant="primary"]{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-foreground,#fff);padding:0 10px}',
|
|
146
|
+
'.dvb-btn[data-variant="primary"]:hover:not(:disabled){background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-foreground,#fff);filter:brightness(1.06)}',
|
|
147
|
+
'.dvb-addr{flex:none;display:flex;align-items:center;gap:6px;padding:0 10px 8px;min-width:0}',
|
|
148
|
+
'.dvb-input{flex:1 1 auto;min-width:0;height:28px;padding:0 9px;border-radius:7px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px}',
|
|
149
|
+
'.dvb-input::placeholder{color:var(--dsw-alias-label-tertiary)}',
|
|
150
|
+
'.dvb-input:focus{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-brand-primary));outline-offset:-1px}',
|
|
151
|
+
// ---- stage ----------------------------------------------------------
|
|
152
|
+
'.dvb-stage{position:relative;flex:1 1 auto;min-height:120px;overflow:hidden;display:flex;align-items:center;justify-content:center;padding:8px;background:var(--dsw-alias-bg-layer-1);border-top:1px solid var(--dsw-alias-border-l1);border-bottom:1px solid var(--dsw-alias-border-l1);outline:none}',
|
|
153
|
+
'.dvb-stage[data-focus="true"]{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
154
|
+
'.dvb-live{display:block;max-width:100%;max-height:100%;width:auto;height:auto;border-radius:6px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);touch-action:none;user-select:none;-webkit-user-drag:none;cursor:default}',
|
|
155
|
+
'.dvb-live[data-busy="true"]{cursor:progress}',
|
|
156
|
+
'.dvb-cursor{position:absolute;left:0;top:0;width:18px;height:18px;margin:-9px 0 0 -9px;border-radius:50%;border:2px solid var(--dsw-alias-brand-primary);box-shadow:0 0 0 1px var(--dsw-alias-bg-base);pointer-events:none;opacity:0;will-change:transform}',
|
|
157
|
+
'.dvb-cursor[data-visible="true"]{opacity:.85}',
|
|
158
|
+
'.dvb-ripple{position:absolute;left:0;top:0;width:22px;height:22px;margin:-11px 0 0 -11px;border-radius:50%;background:var(--dsw-alias-brand-primary);opacity:0;pointer-events:none;will-change:transform,opacity}',
|
|
159
|
+
'.dvb-veil{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:8px;border:0;font:inherit;font-size:12px;color:var(--dsw-alias-label-secondary);background:color-mix(in srgb,var(--dsw-alias-bg-layer-1) 78%,transparent);cursor:pointer}',
|
|
160
|
+
'.dvb-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;text-align:center;padding:20px 24px;color:var(--dsw-alias-label-secondary)}',
|
|
161
|
+
'.dvb-empty-title{font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
162
|
+
'.dvb-empty-desc{max-width:340px;font-size:12px}',
|
|
163
|
+
'.dvb-hint{padding:5px 10px 7px;font-size:11px;color:var(--dsw-alias-label-tertiary);display:flex;gap:8px;flex-wrap:wrap;flex:none}',
|
|
164
|
+
// ---- log ------------------------------------------------------------
|
|
165
|
+
'.dvb-log{flex:0 0 auto;display:flex;flex-direction:column;min-height:0;background:var(--dsw-alias-bg-base)}',
|
|
166
|
+
'.dvb-log-head{display:flex;align-items:center;gap:6px;width:100%;padding:7px 10px;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;text-align:left;cursor:pointer}',
|
|
167
|
+
'.dvb-log-head:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
168
|
+
'.dvb-log-chevron{display:inline-flex;transition:transform .16s ease}',
|
|
169
|
+
'.dvb-log[data-open="true"] .dvb-log-chevron{transform:rotate(90deg)}',
|
|
170
|
+
'.dvb-log-title{font-weight:600;color:var(--dsw-alias-label-secondary)}',
|
|
171
|
+
'.dvb-log-count{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-tertiary)}',
|
|
172
|
+
'.dvb-log-body{overflow-y:auto;overscroll-behavior:contain;max-height:min(44vh,320px);min-height:0;padding:0 6px 8px;display:flex;flex-direction:column;gap:1px}',
|
|
173
|
+
'.dvb-log-empty{padding:3px 6px 10px;font-size:11.5px;color:var(--dsw-alias-label-tertiary)}',
|
|
174
|
+
'.dvb-row{display:flex;align-items:baseline;gap:6px;padding:4px 6px;border-radius:6px;font-size:12px;min-width:0}',
|
|
175
|
+
'.dvb-row:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
176
|
+
'.dvb-row-time{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-tertiary);flex:none;width:52px;font-size:11px}',
|
|
177
|
+
'.dvb-row-kind{flex:none;padding:0 5px;border-radius:4px;border:.5px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);font-size:10.5px;line-height:1.6;white-space:nowrap}',
|
|
178
|
+
'.dvb-row-kind[data-kind^="user"]{border-color:transparent;background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-foreground,#fff)}',
|
|
179
|
+
'.dvb-row-text{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary)}',
|
|
149
180
|
'.dvb-row-text[data-error="true"]{color:var(--dsw-alias-state-error-primary)}',
|
|
150
|
-
'.dvb-
|
|
151
|
-
|
|
152
|
-
'.dvb-
|
|
181
|
+
'.dvb-error{margin:0 10px 8px;padding:6px 9px;border-radius:7px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);font-size:11.5px}',
|
|
182
|
+
// ---- guide cards ----------------------------------------------------
|
|
183
|
+
'.dvb-guide{display:flex;flex-direction:column;width:100%;box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-xl,16px);background:var(--dsw-alias-bg-layer-1);overflow:hidden}',
|
|
184
|
+
'.dvb-guide-card,.dvb-guide-extra{display:flex;align-items:center;gap:12px;width:100%;box-sizing:border-box;padding:12px 16px;border:0;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer}',
|
|
185
|
+
'.dvb-guide-card{padding-bottom:10px}',
|
|
186
|
+
'.dvb-guide-extra{padding-top:10px;padding-bottom:11px}',
|
|
187
|
+
'.dvb-guide-card:hover,.dvb-guide-extra:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
188
|
+
'.dvb-guide-glyph{display:flex;align-items:center;justify-content:center;width:26px;height:26px;flex:none;color:var(--dsw-alias-label-secondary)}',
|
|
189
|
+
'.dvb-guide-extra-glyph{display:flex;align-items:center;justify-content:center;width:26px;height:26px;flex:none;border-radius:7px;background:var(--dsw-alias-state-business-tertiary,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary))}',
|
|
190
|
+
'.dvb-guide-text{display:flex;flex-direction:column;flex:1;min-width:0;gap:2px}',
|
|
191
|
+
'.dvb-guide-title{font-size:13.5px;line-height:1.35;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
192
|
+
'.dvb-guide-desc{font-size:10.5px;line-height:1.35;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
193
|
+
'.dvb-guide-sep{height:1px;margin:0 14px;background:var(--dsw-alias-border-l3)}',
|
|
194
|
+
'.dvb-guide-card>svg,.dvb-guide-extra>svg{color:var(--dsw-alias-label-tertiary);flex:none}',
|
|
153
195
|
].join('\n');
|
|
154
196
|
|
|
155
197
|
window.__ModuleLoader__.load({
|
|
@@ -217,9 +259,9 @@
|
|
|
217
259
|
}, 'dsh-visual-browser: styles');
|
|
218
260
|
}
|
|
219
261
|
|
|
220
|
-
//
|
|
262
|
+
// -------------------------------------------------------------------
|
|
221
263
|
// Host API
|
|
222
|
-
//
|
|
264
|
+
// -------------------------------------------------------------------
|
|
223
265
|
|
|
224
266
|
async function requestJson(path, options) {
|
|
225
267
|
var init = { method: (options && options.method) || 'GET', headers: {} };
|
|
@@ -254,92 +296,259 @@
|
|
|
254
296
|
}
|
|
255
297
|
|
|
256
298
|
function shortUrl(value) {
|
|
257
|
-
|
|
258
|
-
return text.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
299
|
+
return String(value || '').replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
259
300
|
}
|
|
260
301
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
302
|
+
/**
|
|
303
|
+
* Input batches: everything the user does is queued and flushed once per
|
|
304
|
+
* animation frame as a single ordered POST. Moves and wheel deltas are
|
|
305
|
+
* folded together, so a fast flick is one request instead of thirty.
|
|
306
|
+
*/
|
|
307
|
+
function createSender() {
|
|
308
|
+
var queue = [];
|
|
309
|
+
var inFlight = false;
|
|
310
|
+
var frame = 0;
|
|
311
|
+
var disposed = false;
|
|
264
312
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
313
|
+
var compact = function (events) {
|
|
314
|
+
var kept = [];
|
|
315
|
+
for (var index = 0; index < events.length; index += 1) {
|
|
316
|
+
var event = events[index];
|
|
317
|
+
var last = kept.length > 0 ? kept[kept.length - 1] : null;
|
|
318
|
+
if (last !== null && event.t === 'pointer' && event.type === 'move' && last.t === 'pointer' && last.type === 'move') {
|
|
319
|
+
kept[kept.length - 1] = event;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (last !== null && event.t === 'wheel' && last.t === 'wheel') {
|
|
323
|
+
last.dx = (last.dx || 0) + (event.dx || 0);
|
|
324
|
+
last.dy = (last.dy || 0) + (event.dy || 0);
|
|
325
|
+
last.x = event.x;
|
|
326
|
+
last.y = event.y;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
kept.push(event);
|
|
330
|
+
}
|
|
331
|
+
return kept.slice(0, 120);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
var flush = function () {
|
|
335
|
+
frame = 0;
|
|
336
|
+
if (disposed || inFlight) return;
|
|
337
|
+
if (queue.length === 0) return;
|
|
338
|
+
var batch = compact(queue);
|
|
339
|
+
queue = [];
|
|
340
|
+
if (batch.length === 0) return;
|
|
341
|
+
inFlight = true;
|
|
342
|
+
fetch(API + '/input', {
|
|
343
|
+
method: 'POST',
|
|
344
|
+
headers: { 'content-type': 'application/json' },
|
|
345
|
+
body: JSON.stringify({ events: batch }),
|
|
346
|
+
keepalive: true,
|
|
347
|
+
})
|
|
348
|
+
.catch(function () {
|
|
349
|
+
/* a dropped input batch is not worth a visible error */
|
|
350
|
+
})
|
|
351
|
+
.then(function () {
|
|
352
|
+
inFlight = false;
|
|
353
|
+
if (disposed) return;
|
|
354
|
+
if (queue.length > 0 && frame === 0) frame = window.requestAnimationFrame(flush);
|
|
355
|
+
});
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
send: function (event) {
|
|
360
|
+
if (disposed) return;
|
|
361
|
+
queue.push(event);
|
|
362
|
+
if (frame !== 0) return;
|
|
363
|
+
frame = window.requestAnimationFrame(flush);
|
|
364
|
+
},
|
|
365
|
+
flushNow: function () {
|
|
366
|
+
if (frame !== 0) {
|
|
367
|
+
window.cancelAnimationFrame(frame);
|
|
368
|
+
frame = 0;
|
|
369
|
+
}
|
|
370
|
+
flush();
|
|
371
|
+
},
|
|
372
|
+
dispose: function () {
|
|
373
|
+
disposed = true;
|
|
374
|
+
queue = [];
|
|
375
|
+
if (frame !== 0) window.cancelAnimationFrame(frame);
|
|
376
|
+
frame = 0;
|
|
377
|
+
},
|
|
277
378
|
};
|
|
278
|
-
var paths;
|
|
279
|
-
switch (name) {
|
|
280
|
-
case 'play':
|
|
281
|
-
paths = [h('path', { key: 'a', d: 'M8 5.5v13l11-6.5-11-6.5Z', fill: 'currentColor', stroke: 'none' })];
|
|
282
|
-
break;
|
|
283
|
-
case 'stop':
|
|
284
|
-
paths = [h('rect', { key: 'a', x: 6, y: 6, width: 12, height: 12, rx: 2.5, fill: 'currentColor', stroke: 'none' })];
|
|
285
|
-
break;
|
|
286
|
-
case 'back':
|
|
287
|
-
paths = [h('path', { key: 'a', d: 'M14.5 6.5 8 12l6.5 5.5' })];
|
|
288
|
-
break;
|
|
289
|
-
case 'forward':
|
|
290
|
-
paths = [h('path', { key: 'a', d: 'M9.5 6.5 16 12l-6.5 5.5' })];
|
|
291
|
-
break;
|
|
292
|
-
case 'reload':
|
|
293
|
-
paths = [
|
|
294
|
-
h('path', { key: 'a', d: 'M19 12a7 7 0 1 1-2.1-5' }),
|
|
295
|
-
h('path', { key: 'b', d: 'M19 4.5V9h-4.5' }),
|
|
296
|
-
];
|
|
297
|
-
break;
|
|
298
|
-
case 'camera':
|
|
299
|
-
paths = [
|
|
300
|
-
h('path', { key: 'a', d: 'M4.5 8.5h3l1.5-2h6l1.5 2h3v9.5h-15Z' }),
|
|
301
|
-
h('circle', { key: 'b', cx: 12, cy: 13, r: 3.2 }),
|
|
302
|
-
];
|
|
303
|
-
break;
|
|
304
|
-
case 'live':
|
|
305
|
-
paths = [
|
|
306
|
-
h('circle', { key: 'a', cx: 12, cy: 12, r: 3 }),
|
|
307
|
-
h('path', { key: 'b', d: 'M6.5 6.5a7.8 7.8 0 0 0 0 11M17.5 6.5a7.8 7.8 0 0 1 0 11' }),
|
|
308
|
-
];
|
|
309
|
-
break;
|
|
310
|
-
default:
|
|
311
|
-
paths = [];
|
|
312
|
-
}
|
|
313
|
-
return h('svg', common, paths);
|
|
314
379
|
}
|
|
315
380
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
381
|
+
// -------------------------------------------------------------------
|
|
382
|
+
// Icons
|
|
383
|
+
// -------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
function svg(props, paths) {
|
|
319
386
|
return h(
|
|
320
387
|
'svg',
|
|
321
388
|
{
|
|
322
|
-
width: size,
|
|
323
|
-
height: size,
|
|
389
|
+
width: props.size || 16,
|
|
390
|
+
height: props.size || 16,
|
|
324
391
|
viewBox: '0 0 24 24',
|
|
325
392
|
fill: 'none',
|
|
326
393
|
stroke: 'currentColor',
|
|
327
|
-
strokeWidth: 1.
|
|
394
|
+
strokeWidth: props.strokeWidth || 1.9,
|
|
328
395
|
strokeLinecap: 'round',
|
|
329
396
|
strokeLinejoin: 'round',
|
|
330
397
|
'aria-hidden': true,
|
|
331
398
|
focusable: false,
|
|
332
|
-
style: { opacity: active ? 1 : 0.86 },
|
|
333
399
|
},
|
|
400
|
+
paths,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function icon(name, size) {
|
|
405
|
+
switch (name) {
|
|
406
|
+
case 'play':
|
|
407
|
+
return svg({ size: size }, [h('path', { key: 'a', d: 'M8 5.5v13l11-6.5-11-6.5Z', fill: 'currentColor', stroke: 'none' })]);
|
|
408
|
+
case 'stop':
|
|
409
|
+
return svg({ size: size }, [h('rect', { key: 'a', x: 6, y: 6, width: 12, height: 12, rx: 2.5, fill: 'currentColor', stroke: 'none' })]);
|
|
410
|
+
case 'back':
|
|
411
|
+
return svg({ size: size }, [h('path', { key: 'a', d: 'M14.5 6.5 8 12l6.5 5.5' })]);
|
|
412
|
+
case 'forward':
|
|
413
|
+
return svg({ size: size }, [h('path', { key: 'a', d: 'M9.5 6.5 16 12l-6.5 5.5' })]);
|
|
414
|
+
case 'reload':
|
|
415
|
+
return svg({ size: size }, [
|
|
416
|
+
h('path', { key: 'a', d: 'M19 12a7 7 0 1 1-2.1-5' }),
|
|
417
|
+
h('path', { key: 'b', d: 'M19 4.5V9h-4.5' }),
|
|
418
|
+
]);
|
|
419
|
+
case 'chevron':
|
|
420
|
+
return svg({ size: size || 12 }, [h('path', { key: 'a', d: 'M9.5 6l6 6-6 6' })]);
|
|
421
|
+
case 'globe':
|
|
422
|
+
return svg({ size: size || 24, strokeWidth: 1.6 }, [
|
|
423
|
+
h('circle', { key: 'a', cx: 12, cy: 12, r: 8.5 }),
|
|
424
|
+
h('path', { key: 'b', d: 'M3.5 12h17' }),
|
|
425
|
+
h('path', { key: 'c', d: 'M12 3.5c2.6 2.4 4 5.4 4 8.5s-1.4 6.1-4 8.5c-2.6-2.4-4-5.4-4-8.5s1.4-6.1 4-8.5Z' }),
|
|
426
|
+
]);
|
|
427
|
+
default:
|
|
428
|
+
return svg({ size: size }, []);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function BrowserGlyph(props) {
|
|
433
|
+
return svg({ size: (props && props.size) || 18, strokeWidth: 1.8 }, [
|
|
334
434
|
h('rect', { key: 'a', x: 3, y: 4.5, width: 18, height: 15, rx: 2.6 }),
|
|
335
435
|
h('path', { key: 'b', d: 'M3 8.5h18' }),
|
|
336
436
|
h('circle', { key: 'c', cx: 12, cy: 13.6, r: 2.2, fill: 'currentColor', stroke: 'none' }),
|
|
437
|
+
]);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// -------------------------------------------------------------------
|
|
441
|
+
// Guide cards
|
|
442
|
+
// -------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
/** The right-Sidebar controller, kept in step by `apply` for guide cards. */
|
|
445
|
+
var sidebarRightOpen = null;
|
|
446
|
+
|
|
447
|
+
function openTabFrom(info, kind) {
|
|
448
|
+
if (info && info.tab && info.tab.actions && typeof info.tab.actions.openTab === 'function') {
|
|
449
|
+
try {
|
|
450
|
+
info.tab.actions.openTab(kind, { replaceTab: true });
|
|
451
|
+
return;
|
|
452
|
+
} catch (error) {
|
|
453
|
+
console.warn('[dsh-visual-browser] tab.actions.openTab failed:', error && error.message ? error.message : error);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (typeof sidebarRightOpen === 'function') {
|
|
457
|
+
try {
|
|
458
|
+
sidebarRightOpen(kind, { replaceTab: true });
|
|
459
|
+
return;
|
|
460
|
+
} catch (error) {
|
|
461
|
+
console.warn('[dsh-visual-browser] sidebarRight.openTab failed:', error && error.message ? error.message : error);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
console.warn('[dsh-visual-browser] no way to open tab', kind);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** The card that carries our own entry when the built-in Browser is absent. */
|
|
469
|
+
function VisualBrowserGuideCard(props) {
|
|
470
|
+
var t = props.t || translateCurrent;
|
|
471
|
+
var info = typeof props.useTabInfo === 'function' ? props.useTabInfo() : null;
|
|
472
|
+
return h(
|
|
473
|
+
'div',
|
|
474
|
+
{ className: 'dvb-guide', 'data-dvb-guide': 'own' },
|
|
475
|
+
h(
|
|
476
|
+
'button',
|
|
477
|
+
{
|
|
478
|
+
type: 'button',
|
|
479
|
+
className: 'dvb-guide-card',
|
|
480
|
+
'data-sidebar-right-guide-entry': props.kind || KIND,
|
|
481
|
+
onClick: function () {
|
|
482
|
+
openTabFrom(info, props.kind || KIND);
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
h('span', { className: 'dvb-guide-glyph' }, h(BrowserGlyph, { size: 24 })),
|
|
486
|
+
h(
|
|
487
|
+
'span',
|
|
488
|
+
{ className: 'dvb-guide-text' },
|
|
489
|
+
h('span', { className: 'dvb-guide-title' }, props.title || t('guide.title')),
|
|
490
|
+
h('span', { className: 'dvb-guide-desc' }, props.description || t('guide.description')),
|
|
491
|
+
),
|
|
492
|
+
icon('chevron', 14),
|
|
493
|
+
),
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* The built-in Browser card, with one extra row under it: this is what
|
|
499
|
+
* puts a door to the Visual Browser directly beneath the system browser.
|
|
500
|
+
*/
|
|
501
|
+
function BrowserExtraGuideCard(props) {
|
|
502
|
+
var t = props.t || translateCurrent;
|
|
503
|
+
var info = typeof props.useTabInfo === 'function' ? props.useTabInfo() : null;
|
|
504
|
+
return h(
|
|
505
|
+
'div',
|
|
506
|
+
{ className: 'dvb-guide', 'data-dvb-guide': 'browser' },
|
|
507
|
+
h(
|
|
508
|
+
'button',
|
|
509
|
+
{
|
|
510
|
+
type: 'button',
|
|
511
|
+
className: 'dvb-guide-card',
|
|
512
|
+
'data-sidebar-right-guide-entry': props.kind || BROWSER_KIND,
|
|
513
|
+
onClick: function () {
|
|
514
|
+
openTabFrom(info, props.kind || BROWSER_KIND);
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
h('span', { className: 'dvb-guide-glyph' }, icon('globe', 24)),
|
|
518
|
+
h(
|
|
519
|
+
'span',
|
|
520
|
+
{ className: 'dvb-guide-text' },
|
|
521
|
+
h('span', { className: 'dvb-guide-title' }, props.title || t('panel.title')),
|
|
522
|
+
props.description ? h('span', { className: 'dvb-guide-desc' }, props.description) : null,
|
|
523
|
+
),
|
|
524
|
+
icon('chevron', 14),
|
|
525
|
+
),
|
|
526
|
+
h('div', { className: 'dvb-guide-sep', role: 'none' }),
|
|
527
|
+
h(
|
|
528
|
+
'button',
|
|
529
|
+
{
|
|
530
|
+
type: 'button',
|
|
531
|
+
className: 'dvb-guide-extra',
|
|
532
|
+
title: t('guide.open'),
|
|
533
|
+
onClick: function () {
|
|
534
|
+
openTabFrom(info, KIND);
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
h('span', { className: 'dvb-guide-extra-glyph' }, h(BrowserGlyph, { size: 16 })),
|
|
538
|
+
h(
|
|
539
|
+
'span',
|
|
540
|
+
{ className: 'dvb-guide-text' },
|
|
541
|
+
h('span', { className: 'dvb-guide-title' }, t('guide.title')),
|
|
542
|
+
h('span', { className: 'dvb-guide-desc' }, t('guide.description')),
|
|
543
|
+
),
|
|
544
|
+
icon('chevron', 14),
|
|
545
|
+
),
|
|
337
546
|
);
|
|
338
547
|
}
|
|
339
548
|
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
549
|
+
// -------------------------------------------------------------------
|
|
550
|
+
// Tab body
|
|
551
|
+
// -------------------------------------------------------------------
|
|
343
552
|
|
|
344
553
|
var EMPTY_STATE = {
|
|
345
554
|
running: false,
|
|
@@ -347,30 +556,64 @@
|
|
|
347
556
|
url: '',
|
|
348
557
|
title: '',
|
|
349
558
|
viewport: { width: 1280, height: 800 },
|
|
350
|
-
|
|
559
|
+
startedAt: 0,
|
|
351
560
|
actionSeq: 0,
|
|
352
|
-
frameSeqs: [],
|
|
353
561
|
actions: [],
|
|
354
562
|
allowUserControl: true,
|
|
355
563
|
lastError: '',
|
|
356
564
|
};
|
|
357
565
|
|
|
358
|
-
|
|
566
|
+
var MODIFIER_MASK = { Alt: 1, Control: 2, Meta: 4, Shift: 8 };
|
|
567
|
+
|
|
568
|
+
function modifierMask(event) {
|
|
569
|
+
var mask = 0;
|
|
570
|
+
if (event.altKey) mask |= MODIFIER_MASK.Alt;
|
|
571
|
+
if (event.ctrlKey) mask |= MODIFIER_MASK.Control;
|
|
572
|
+
if (event.metaKey) mask |= MODIFIER_MASK.Meta;
|
|
573
|
+
if (event.shiftKey) mask |= MODIFIER_MASK.Shift;
|
|
574
|
+
return mask;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function typedText(event) {
|
|
578
|
+
if (event.ctrlKey || event.metaKey || event.altKey) return '';
|
|
579
|
+
if (typeof event.key !== 'string' || event.key.length !== 1) return '';
|
|
580
|
+
return event.key;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function BrowserTab(props) {
|
|
359
584
|
var ctx = props.ctx;
|
|
360
|
-
var t = translateCurrent;
|
|
585
|
+
var t = props.t || translateCurrent;
|
|
361
586
|
var [tick, setTick] = useState(0);
|
|
362
587
|
var [state, setState] = useState(EMPTY_STATE);
|
|
363
588
|
var [reachable, setReachable] = useState(true);
|
|
364
589
|
var [busy, setBusy] = useState('');
|
|
365
590
|
var [error, setError] = useState('');
|
|
366
|
-
var [live, setLive] = useState(false);
|
|
367
|
-
var [liveTick, setLiveTick] = useState(0);
|
|
368
|
-
var [previewSeq, setPreviewSeq] = useState(0);
|
|
369
591
|
var [draft, setDraft] = useState('');
|
|
370
592
|
var [dirty, setDirty] = useState(false);
|
|
593
|
+
var [logOpen, setLogOpen] = useState(true);
|
|
594
|
+
var [live, setLive] = useState(false);
|
|
595
|
+
var [streamRetry, setStreamRetry] = useState(0);
|
|
596
|
+
var [retrying, setRetrying] = useState(false);
|
|
597
|
+
var [focused, setFocused] = useState(false);
|
|
598
|
+
|
|
371
599
|
var stageRef = useRef(null);
|
|
372
|
-
var
|
|
600
|
+
var canvasRef = useRef(null);
|
|
601
|
+
var liveRef = useRef(false);
|
|
602
|
+
var cursorRef = useRef(null);
|
|
603
|
+
var rippleRef = useRef(null);
|
|
604
|
+
var pointerRef = useRef({ down: false, id: 0, x: 0, y: 0, moved: 0, clicks: 1, lastUpAt: 0, lastUpX: 0, lastUpY: 0, button: 0 });
|
|
605
|
+
var stateJsonRef = useRef('');
|
|
606
|
+
var sender = useMemo(function () {
|
|
607
|
+
return createSender();
|
|
608
|
+
}, []);
|
|
373
609
|
|
|
610
|
+
useEffect(function () {
|
|
611
|
+
return function () {
|
|
612
|
+
sender.dispose();
|
|
613
|
+
};
|
|
614
|
+
}, [sender]);
|
|
615
|
+
|
|
616
|
+
// Re-render on language change only.
|
|
374
617
|
useEffect(function () {
|
|
375
618
|
var locale = ctx.get('locale');
|
|
376
619
|
if (!locale || typeof locale.subscribe !== 'function') return undefined;
|
|
@@ -381,6 +624,7 @@
|
|
|
381
624
|
});
|
|
382
625
|
}, [ctx]);
|
|
383
626
|
|
|
627
|
+
// State poll: identity-compare the JSON so an idle panel never re-renders.
|
|
384
628
|
useEffect(
|
|
385
629
|
function () {
|
|
386
630
|
var cancelled = false;
|
|
@@ -390,12 +634,18 @@
|
|
|
390
634
|
var data = await requestJson('/state');
|
|
391
635
|
if (cancelled) return;
|
|
392
636
|
setReachable(true);
|
|
393
|
-
if (data.state)
|
|
637
|
+
if (data.state) {
|
|
638
|
+
var next = JSON.stringify(data.state);
|
|
639
|
+
if (next !== stateJsonRef.current) {
|
|
640
|
+
stateJsonRef.current = next;
|
|
641
|
+
setState(data.state);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
394
644
|
} catch {
|
|
395
645
|
if (!cancelled) setReachable(false);
|
|
396
646
|
}
|
|
397
647
|
if (!cancelled) {
|
|
398
|
-
timer = window.setTimeout(poll, document.visibilityState === 'hidden' ?
|
|
648
|
+
timer = window.setTimeout(poll, document.visibilityState === 'hidden' ? 4000 : 900);
|
|
399
649
|
}
|
|
400
650
|
};
|
|
401
651
|
poll();
|
|
@@ -414,97 +664,409 @@
|
|
|
414
664
|
[state.url, dirty],
|
|
415
665
|
);
|
|
416
666
|
|
|
667
|
+
// A new browser incarnation means a new stream.
|
|
668
|
+
useEffect(
|
|
669
|
+
function () {
|
|
670
|
+
liveRef.current = false;
|
|
671
|
+
setLive(false);
|
|
672
|
+
setRetrying(false);
|
|
673
|
+
},
|
|
674
|
+
[state.startedAt, state.running],
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* The live picture: one long binary response, each frame a length-
|
|
679
|
+
* prefixed JPEG decoded off the main thread and painted on a canvas.
|
|
680
|
+
* Decoding keeps at most one frame queued, so a burst never builds a
|
|
681
|
+
* backlog of stale pictures.
|
|
682
|
+
*/
|
|
417
683
|
useEffect(
|
|
418
684
|
function () {
|
|
419
|
-
if (!
|
|
420
|
-
var
|
|
421
|
-
|
|
422
|
-
|
|
685
|
+
if (!state.running) return undefined;
|
|
686
|
+
var cancelled = false;
|
|
687
|
+
var controller = new AbortController();
|
|
688
|
+
var reader = null;
|
|
689
|
+
var buffer = new Uint8Array(0);
|
|
690
|
+
var pendingFrame = null;
|
|
691
|
+
var decoding = false;
|
|
692
|
+
|
|
693
|
+
var draw = async function (jpeg) {
|
|
694
|
+
var bitmap = await createImageBitmap(new Blob([jpeg], { type: 'image/jpeg' }));
|
|
695
|
+
var canvas = canvasRef.current;
|
|
696
|
+
if (canvas !== null) {
|
|
697
|
+
if (canvas.width !== bitmap.width || canvas.height !== bitmap.height) {
|
|
698
|
+
canvas.width = bitmap.width;
|
|
699
|
+
canvas.height = bitmap.height;
|
|
700
|
+
}
|
|
701
|
+
var context = canvas.getContext('2d');
|
|
702
|
+
if (context) context.drawImage(bitmap, 0, 0);
|
|
703
|
+
if (!liveRef.current) {
|
|
704
|
+
liveRef.current = true;
|
|
705
|
+
setLive(true);
|
|
706
|
+
setRetrying(false);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
bitmap.close();
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
var drain = async function () {
|
|
713
|
+
if (decoding) return;
|
|
714
|
+
decoding = true;
|
|
715
|
+
try {
|
|
716
|
+
while (!cancelled && pendingFrame !== null) {
|
|
717
|
+
var jpeg = pendingFrame;
|
|
718
|
+
pendingFrame = null;
|
|
719
|
+
try {
|
|
720
|
+
await draw(jpeg);
|
|
721
|
+
} catch {
|
|
722
|
+
/* a frame that will not decode is skipped */
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
} finally {
|
|
726
|
+
decoding = false;
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
var pump = async function () {
|
|
731
|
+
var response = await fetch(API + '/stream?r=' + streamRetry, {
|
|
732
|
+
signal: controller.signal,
|
|
733
|
+
cache: 'no-store',
|
|
423
734
|
});
|
|
424
|
-
|
|
735
|
+
if (!response.ok || !response.body) throw new Error('stream ' + response.status);
|
|
736
|
+
reader = response.body.getReader();
|
|
737
|
+
for (;;) {
|
|
738
|
+
var step = await reader.read();
|
|
739
|
+
if (step.done) break;
|
|
740
|
+
var chunk = step.value;
|
|
741
|
+
var joined = new Uint8Array(buffer.length + chunk.length);
|
|
742
|
+
joined.set(buffer, 0);
|
|
743
|
+
joined.set(chunk, buffer.length);
|
|
744
|
+
buffer = joined;
|
|
745
|
+
while (buffer.length >= 8) {
|
|
746
|
+
var view = new DataView(buffer.buffer, buffer.byteOffset, 8);
|
|
747
|
+
var length = view.getUint32(0, true);
|
|
748
|
+
if (length === 0 || length > 8 * 1024 * 1024) {
|
|
749
|
+
buffer = new Uint8Array(0);
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
if (buffer.length < 8 + length) break;
|
|
753
|
+
pendingFrame = buffer.slice(8, 8 + length);
|
|
754
|
+
buffer = buffer.slice(8 + length);
|
|
755
|
+
void drain();
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
throw new Error('stream ended');
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
pump().catch(function () {
|
|
762
|
+
if (cancelled) return;
|
|
763
|
+
liveRef.current = false;
|
|
764
|
+
setLive(false);
|
|
765
|
+
setRetrying(true);
|
|
766
|
+
window.setTimeout(function () {
|
|
767
|
+
if (!cancelled) {
|
|
768
|
+
setStreamRetry(function (value) {
|
|
769
|
+
return value + 1;
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
}, Math.min(4000, 600 + streamRetry * 400));
|
|
773
|
+
});
|
|
774
|
+
|
|
425
775
|
return function () {
|
|
426
|
-
|
|
776
|
+
cancelled = true;
|
|
777
|
+
controller.abort();
|
|
778
|
+
try {
|
|
779
|
+
if (reader) void reader.cancel();
|
|
780
|
+
} catch {
|
|
781
|
+
/* already closed */
|
|
782
|
+
}
|
|
427
783
|
};
|
|
428
784
|
},
|
|
429
|
-
[
|
|
785
|
+
[state.running, state.startedAt, streamRetry],
|
|
430
786
|
);
|
|
431
787
|
|
|
432
|
-
var
|
|
433
|
-
setBusy(label);
|
|
434
|
-
setError('');
|
|
788
|
+
var refreshState = useCallback(async function () {
|
|
435
789
|
try {
|
|
436
|
-
var data = await requestJson(
|
|
437
|
-
|
|
790
|
+
var data = await requestJson('/state');
|
|
791
|
+
setReachable(true);
|
|
792
|
+
if (data.state) {
|
|
793
|
+
stateJsonRef.current = JSON.stringify(data.state);
|
|
438
794
|
setState(data.state);
|
|
439
|
-
setReachable(true);
|
|
440
795
|
}
|
|
441
|
-
} catch
|
|
442
|
-
|
|
443
|
-
} finally {
|
|
444
|
-
setBusy('');
|
|
796
|
+
} catch {
|
|
797
|
+
setReachable(false);
|
|
445
798
|
}
|
|
446
799
|
}, []);
|
|
447
800
|
|
|
448
|
-
var
|
|
449
|
-
function (
|
|
450
|
-
|
|
451
|
-
|
|
801
|
+
var run = useCallback(
|
|
802
|
+
async function (label, path, body) {
|
|
803
|
+
setBusy(label);
|
|
804
|
+
setError('');
|
|
805
|
+
try {
|
|
806
|
+
var data = await requestJson(path, { method: 'POST', body: body || {} });
|
|
807
|
+
if (data && data.state) {
|
|
808
|
+
stateJsonRef.current = JSON.stringify(data.state);
|
|
809
|
+
setState(data.state);
|
|
810
|
+
setReachable(true);
|
|
811
|
+
}
|
|
812
|
+
} catch (failure) {
|
|
813
|
+
setError(failure && failure.message ? failure.message : String(failure));
|
|
814
|
+
} finally {
|
|
815
|
+
setBusy('');
|
|
816
|
+
}
|
|
452
817
|
},
|
|
453
|
-
[
|
|
818
|
+
[],
|
|
454
819
|
);
|
|
455
820
|
|
|
821
|
+
// Geometry of the picture in CSS pixels, read on demand (never cached).
|
|
822
|
+
var frameRect = function () {
|
|
823
|
+
var canvas = canvasRef.current;
|
|
824
|
+
if (!canvas) return null;
|
|
825
|
+
var rect = canvas.getBoundingClientRect();
|
|
826
|
+
if (!rect || rect.width < 2 || rect.height < 2) return null;
|
|
827
|
+
return rect;
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
var toNormalized = function (event) {
|
|
831
|
+
var rect = frameRect();
|
|
832
|
+
if (!rect) return null;
|
|
833
|
+
var x = (event.clientX - rect.left) / rect.width;
|
|
834
|
+
var y = (event.clientY - rect.top) / rect.height;
|
|
835
|
+
if (x < -0.02 || x > 1.02 || y < -0.02 || y > 1.02) return null;
|
|
836
|
+
return { x: Math.min(1, Math.max(0, x)), y: Math.min(1, Math.max(0, y)) };
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
var placeCursor = function (event) {
|
|
840
|
+
var stage = stageRef.current;
|
|
841
|
+
var ring = cursorRef.current;
|
|
842
|
+
if (!stage || !ring) return;
|
|
843
|
+
var stageRect = stage.getBoundingClientRect();
|
|
844
|
+
var x = event.clientX - stageRect.left;
|
|
845
|
+
var y = event.clientY - stageRect.top;
|
|
846
|
+
ring.style.transform = 'translate3d(' + x + 'px,' + y + 'px,0)';
|
|
847
|
+
ring.setAttribute('data-visible', 'true');
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
var hideCursor = function () {
|
|
851
|
+
var ring = cursorRef.current;
|
|
852
|
+
if (ring) ring.setAttribute('data-visible', 'false');
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
var ripple = function (event) {
|
|
856
|
+
var stage = stageRef.current;
|
|
857
|
+
var mark = rippleRef.current;
|
|
858
|
+
if (!stage || !mark || typeof mark.animate !== 'function') return;
|
|
859
|
+
var stageRect = stage.getBoundingClientRect();
|
|
860
|
+
var x = event.clientX - stageRect.left;
|
|
861
|
+
var y = event.clientY - stageRect.top;
|
|
862
|
+
try {
|
|
863
|
+
mark.animate(
|
|
864
|
+
[
|
|
865
|
+
{ transform: 'translate3d(' + x + 'px,' + y + 'px,0) scale(.35)', opacity: 0.5 },
|
|
866
|
+
{ transform: 'translate3d(' + x + 'px,' + y + 'px,0) scale(2.1)', opacity: 0 },
|
|
867
|
+
],
|
|
868
|
+
{ duration: 340, easing: 'ease-out' },
|
|
869
|
+
);
|
|
870
|
+
} catch {
|
|
871
|
+
/* older engines simply skip the ripple */
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
var clickCountFor = function (point) {
|
|
876
|
+
var pointer = pointerRef.current;
|
|
877
|
+
var now = Date.now();
|
|
878
|
+
var rect = frameRect();
|
|
879
|
+
var width = rect ? rect.width : 1;
|
|
880
|
+
var height = rect ? rect.height : 1;
|
|
881
|
+
var near =
|
|
882
|
+
Math.abs(point.x - pointer.lastUpX) * width < 6 && Math.abs(point.y - pointer.lastUpY) * height < 6;
|
|
883
|
+
var count = now - pointer.lastUpAt < 420 && near ? 2 : 1;
|
|
884
|
+
pointer.lastUpAt = now;
|
|
885
|
+
pointer.lastUpX = point.x;
|
|
886
|
+
pointer.lastUpY = point.y;
|
|
887
|
+
return count;
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
var sendFlush = function () {
|
|
891
|
+
// Gesture boundaries are worth an immediate flush; a held-down drag is not.
|
|
892
|
+
sender.flushNow();
|
|
893
|
+
window.setTimeout(function () {
|
|
894
|
+
void refreshState();
|
|
895
|
+
}, 220);
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
var onPointerDown = function (event) {
|
|
899
|
+
if (!state.running || !state.allowUserControl) return;
|
|
900
|
+
if (event.button !== 0 && event.button !== 2 && event.button !== 1) return;
|
|
901
|
+
var point = toNormalized(event);
|
|
902
|
+
if (!point) return;
|
|
903
|
+
event.preventDefault();
|
|
904
|
+
var stage = stageRef.current;
|
|
905
|
+
if (stage && typeof stage.focus === 'function') {
|
|
906
|
+
try {
|
|
907
|
+
stage.focus({ preventScroll: true });
|
|
908
|
+
} catch {
|
|
909
|
+
/* focus on an old engine */
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
var pointer = pointerRef.current;
|
|
913
|
+
pointer.down = true;
|
|
914
|
+
pointer.id = event.pointerId;
|
|
915
|
+
pointer.x = point.x;
|
|
916
|
+
pointer.y = point.y;
|
|
917
|
+
pointer.moved = 0;
|
|
918
|
+
pointer.button = event.button;
|
|
919
|
+
pointer.clicks = pointer.button === 0 ? clickCountFor(point) : 1;
|
|
920
|
+
try {
|
|
921
|
+
if (event.currentTarget.setPointerCapture) event.currentTarget.setPointerCapture(event.pointerId);
|
|
922
|
+
} catch {
|
|
923
|
+
/* capture is a nicety, not a requirement */
|
|
924
|
+
}
|
|
925
|
+
placeCursor(event);
|
|
926
|
+
ripple(event);
|
|
927
|
+
var masked = modifierMask(event);
|
|
928
|
+
sender.send({ t: 'pointer', type: 'move', x: point.x, y: point.y, buttons: event.buttons || 0, modifiers: masked });
|
|
929
|
+
sender.send({
|
|
930
|
+
t: 'pointer',
|
|
931
|
+
type: 'down',
|
|
932
|
+
x: point.x,
|
|
933
|
+
y: point.y,
|
|
934
|
+
button: event.button,
|
|
935
|
+
buttons: event.buttons || 1,
|
|
936
|
+
clicks: pointer.clicks,
|
|
937
|
+
modifiers: masked,
|
|
938
|
+
});
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
var onPointerMove = function (event) {
|
|
942
|
+
if (!state.running || !state.allowUserControl) return;
|
|
943
|
+
placeCursor(event);
|
|
944
|
+
var point = toNormalized(event);
|
|
945
|
+
if (!point) return;
|
|
946
|
+
var pointer = pointerRef.current;
|
|
947
|
+
if (!pointer.down) {
|
|
948
|
+
// Hover matters for menus and tooltips; folding happens in the sender.
|
|
949
|
+
sender.send({ t: 'pointer', type: 'move', x: point.x, y: point.y, buttons: 0, modifiers: modifierMask(event) });
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
var rect = frameRect();
|
|
953
|
+
if (rect) {
|
|
954
|
+
var dx = (point.x - pointer.x) * rect.width;
|
|
955
|
+
var dy = (point.y - pointer.y) * rect.height;
|
|
956
|
+
pointer.moved = Math.max(pointer.moved, Math.sqrt(dx * dx + dy * dy));
|
|
957
|
+
}
|
|
958
|
+
sender.send({ t: 'pointer', type: 'move', x: point.x, y: point.y, buttons: event.buttons || 0, modifiers: modifierMask(event) });
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
var onPointerUp = function (event) {
|
|
962
|
+
var pointer = pointerRef.current;
|
|
963
|
+
if (!pointer.down) return;
|
|
964
|
+
if (!state.running || !state.allowUserControl) {
|
|
965
|
+
pointer.down = false;
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
pointer.down = false;
|
|
969
|
+
try {
|
|
970
|
+
if (event.currentTarget.releasePointerCapture) event.currentTarget.releasePointerCapture(event.pointerId);
|
|
971
|
+
} catch {
|
|
972
|
+
/* already released */
|
|
973
|
+
}
|
|
974
|
+
var point = toNormalized(event) || { x: pointer.x, y: pointer.y };
|
|
975
|
+
var masked = modifierMask(event);
|
|
976
|
+
var viewportWidth = state.viewport && state.viewport.width ? state.viewport.width : 1280;
|
|
977
|
+
var viewportHeight = state.viewport && state.viewport.height ? state.viewport.height : 800;
|
|
978
|
+
var travel = Math.sqrt(
|
|
979
|
+
Math.pow((point.x - pointer.x) * viewportWidth, 2) + Math.pow((point.y - pointer.y) * viewportHeight, 2),
|
|
980
|
+
);
|
|
981
|
+
var drag = Math.max(pointer.moved, travel) > 4;
|
|
982
|
+
sender.send({ t: 'pointer', type: 'move', x: point.x, y: point.y, buttons: 0, modifiers: masked });
|
|
983
|
+
sender.send({
|
|
984
|
+
t: 'pointer',
|
|
985
|
+
type: 'up',
|
|
986
|
+
x: point.x,
|
|
987
|
+
y: point.y,
|
|
988
|
+
button: pointer.button,
|
|
989
|
+
buttons: 0,
|
|
990
|
+
clicks: pointer.clicks || 1,
|
|
991
|
+
drag: drag,
|
|
992
|
+
modifiers: masked,
|
|
993
|
+
});
|
|
994
|
+
sendFlush();
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
// Native wheel listener: React's is passive in places, and the picture
|
|
998
|
+
// must never scroll the sidebar around it.
|
|
456
999
|
useEffect(
|
|
457
1000
|
function () {
|
|
458
|
-
var
|
|
459
|
-
if (!
|
|
1001
|
+
var canvas = canvasRef.current;
|
|
1002
|
+
if (!canvas) return undefined;
|
|
460
1003
|
var onWheel = function (event) {
|
|
461
1004
|
if (!state.running || !state.allowUserControl) return;
|
|
462
1005
|
event.preventDefault();
|
|
463
|
-
var
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
1006
|
+
var rect = frameRect();
|
|
1007
|
+
if (!rect) return;
|
|
1008
|
+
var x = Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width));
|
|
1009
|
+
var y = Math.min(1, Math.max(0, (event.clientY - rect.top) / rect.height));
|
|
1010
|
+
var scale = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? rect.height : 1;
|
|
1011
|
+
sender.send({
|
|
1012
|
+
t: 'wheel',
|
|
1013
|
+
x: x,
|
|
1014
|
+
y: y,
|
|
1015
|
+
dx: Math.round(event.deltaX * scale),
|
|
1016
|
+
dy: Math.round(event.deltaY * scale),
|
|
1017
|
+
modifiers: modifierMask(event),
|
|
1018
|
+
});
|
|
472
1019
|
};
|
|
473
|
-
|
|
1020
|
+
canvas.addEventListener('wheel', onWheel, { passive: false });
|
|
474
1021
|
return function () {
|
|
475
|
-
|
|
476
|
-
var acc = wheelAcc.current;
|
|
477
|
-
if (acc.timer) window.clearTimeout(acc.timer);
|
|
1022
|
+
canvas.removeEventListener('wheel', onWheel);
|
|
478
1023
|
};
|
|
479
1024
|
},
|
|
480
|
-
[
|
|
1025
|
+
[sender, state.running, state.allowUserControl, live],
|
|
481
1026
|
);
|
|
482
1027
|
|
|
483
|
-
var
|
|
1028
|
+
var onKeyDown = function (event) {
|
|
484
1029
|
if (!state.running || !state.allowUserControl) return;
|
|
485
|
-
var
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
1030
|
+
var text = typedText(event);
|
|
1031
|
+
if (text === '' && (event.ctrlKey || event.metaKey) && event.key === 'v') {
|
|
1032
|
+
return; // paste is handled by onPaste with real clipboard text
|
|
1033
|
+
}
|
|
1034
|
+
event.preventDefault();
|
|
1035
|
+
sender.send({
|
|
1036
|
+
t: 'key',
|
|
1037
|
+
type: 'down',
|
|
1038
|
+
key: event.key,
|
|
1039
|
+
code: event.code,
|
|
1040
|
+
keyCode: event.keyCode,
|
|
1041
|
+
text: text,
|
|
1042
|
+
repeat: event.repeat === true,
|
|
1043
|
+
modifiers: modifierMask(event),
|
|
1044
|
+
});
|
|
1045
|
+
sender.flushNow();
|
|
492
1046
|
};
|
|
493
1047
|
|
|
494
|
-
var
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
1048
|
+
var onKeyUp = function (event) {
|
|
1049
|
+
if (!state.running || !state.allowUserControl) return;
|
|
1050
|
+
event.preventDefault();
|
|
1051
|
+
sender.send({
|
|
1052
|
+
t: 'key',
|
|
1053
|
+
type: 'up',
|
|
1054
|
+
key: event.key,
|
|
1055
|
+
code: event.code,
|
|
1056
|
+
keyCode: event.keyCode,
|
|
1057
|
+
modifiers: modifierMask(event),
|
|
1058
|
+
});
|
|
1059
|
+
sender.flushNow();
|
|
1060
|
+
};
|
|
502
1061
|
|
|
503
|
-
var
|
|
504
|
-
|
|
505
|
-
var
|
|
506
|
-
var text =
|
|
507
|
-
|
|
1062
|
+
var onPaste = function (event) {
|
|
1063
|
+
if (!state.running || !state.allowUserControl) return;
|
|
1064
|
+
var data = event.clipboardData;
|
|
1065
|
+
var text = data ? data.getData('text/plain') : '';
|
|
1066
|
+
if (!text) return;
|
|
1067
|
+
event.preventDefault();
|
|
1068
|
+
sender.send({ t: 'text', text: text });
|
|
1069
|
+
sender.flushNow();
|
|
508
1070
|
};
|
|
509
1071
|
|
|
510
1072
|
var openOrStart = function () {
|
|
@@ -512,6 +1074,19 @@
|
|
|
512
1074
|
setDirty(false);
|
|
513
1075
|
};
|
|
514
1076
|
|
|
1077
|
+
var retryStream = function () {
|
|
1078
|
+
setRetrying(true);
|
|
1079
|
+
setStreamRetry(function (value) {
|
|
1080
|
+
return value + 1;
|
|
1081
|
+
});
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
var kindLabel = function (kind) {
|
|
1085
|
+
var key = 'kind.' + kind;
|
|
1086
|
+
var text = t(key);
|
|
1087
|
+
return text === key ? kind : text;
|
|
1088
|
+
};
|
|
1089
|
+
|
|
515
1090
|
var statusText = !reachable
|
|
516
1091
|
? t('state.offline')
|
|
517
1092
|
: state.starting
|
|
@@ -520,275 +1095,269 @@
|
|
|
520
1095
|
? t('state.running')
|
|
521
1096
|
: t('state.stopped');
|
|
522
1097
|
|
|
1098
|
+
var actions = state.actions || [];
|
|
1099
|
+
|
|
523
1100
|
return h(
|
|
524
1101
|
'div',
|
|
525
|
-
{ className: 'dvb-
|
|
1102
|
+
{ className: 'dvb-root' },
|
|
526
1103
|
h(
|
|
527
1104
|
'div',
|
|
528
|
-
{ className: 'dvb-
|
|
1105
|
+
{ className: 'dvb-bar' },
|
|
1106
|
+
h('span', {
|
|
1107
|
+
className: 'dvb-dot',
|
|
1108
|
+
'data-on': String(state.running),
|
|
1109
|
+
'data-error': String(!reachable),
|
|
1110
|
+
title: statusText + (state.headless ? ' · headless' : ''),
|
|
1111
|
+
}),
|
|
529
1112
|
h(
|
|
530
|
-
'
|
|
531
|
-
{ className: 'dvb-
|
|
532
|
-
|
|
533
|
-
className: 'dvb-dot',
|
|
534
|
-
'data-on': String(state.running),
|
|
535
|
-
'data-error': String(!reachable),
|
|
536
|
-
title: statusText + (state.headless ? ' · headless' : ''),
|
|
537
|
-
}),
|
|
538
|
-
h('span', { className: 'dvb-title' }, t('panel.title')),
|
|
539
|
-
h('span', { className: 'dvb-status', title: state.title || state.url || '' }, state.title || shortUrl(state.url) || statusText),
|
|
540
|
-
h('span', { className: 'dvb-spacer' }),
|
|
541
|
-
busy ? h('span', { className: 'dvb-busy' }, t('busy')) : null,
|
|
542
|
-
h(
|
|
543
|
-
'div',
|
|
544
|
-
{ className: 'dvb-tools' },
|
|
545
|
-
h(
|
|
546
|
-
'button',
|
|
547
|
-
{
|
|
548
|
-
type: 'button',
|
|
549
|
-
className: 'dvb-btn',
|
|
550
|
-
title: t('action.back'),
|
|
551
|
-
'aria-label': t('action.back'),
|
|
552
|
-
disabled: !state.running || Boolean(busy),
|
|
553
|
-
onClick: function () {
|
|
554
|
-
void run('back', '/nav', { action: 'back' });
|
|
555
|
-
},
|
|
556
|
-
},
|
|
557
|
-
icon('back'),
|
|
558
|
-
),
|
|
559
|
-
h(
|
|
560
|
-
'button',
|
|
561
|
-
{
|
|
562
|
-
type: 'button',
|
|
563
|
-
className: 'dvb-btn',
|
|
564
|
-
title: t('action.forward'),
|
|
565
|
-
'aria-label': t('action.forward'),
|
|
566
|
-
disabled: !state.running || Boolean(busy),
|
|
567
|
-
onClick: function () {
|
|
568
|
-
void run('forward', '/nav', { action: 'forward' });
|
|
569
|
-
},
|
|
570
|
-
},
|
|
571
|
-
icon('forward'),
|
|
572
|
-
),
|
|
573
|
-
h(
|
|
574
|
-
'button',
|
|
575
|
-
{
|
|
576
|
-
type: 'button',
|
|
577
|
-
className: 'dvb-btn',
|
|
578
|
-
title: t('action.reload'),
|
|
579
|
-
'aria-label': t('action.reload'),
|
|
580
|
-
disabled: !state.running || Boolean(busy),
|
|
581
|
-
onClick: function () {
|
|
582
|
-
void run('reload', '/nav', { action: 'reload' });
|
|
583
|
-
},
|
|
584
|
-
},
|
|
585
|
-
icon('reload'),
|
|
586
|
-
),
|
|
587
|
-
h(
|
|
588
|
-
'button',
|
|
589
|
-
{
|
|
590
|
-
type: 'button',
|
|
591
|
-
className: 'dvb-btn',
|
|
592
|
-
title: t('action.live'),
|
|
593
|
-
'aria-label': t('action.live'),
|
|
594
|
-
'data-on': String(live),
|
|
595
|
-
disabled: !state.running || Boolean(busy),
|
|
596
|
-
onClick: function () {
|
|
597
|
-
setLive(function (value) {
|
|
598
|
-
return !value;
|
|
599
|
-
});
|
|
600
|
-
setLiveTick(function (value) {
|
|
601
|
-
return value + 1;
|
|
602
|
-
});
|
|
603
|
-
},
|
|
604
|
-
},
|
|
605
|
-
icon('live'),
|
|
606
|
-
),
|
|
607
|
-
h(
|
|
608
|
-
'button',
|
|
609
|
-
{
|
|
610
|
-
type: 'button',
|
|
611
|
-
className: 'dvb-btn',
|
|
612
|
-
title: t('action.capture'),
|
|
613
|
-
'aria-label': t('action.capture'),
|
|
614
|
-
disabled: !state.running || Boolean(busy),
|
|
615
|
-
onClick: function () {
|
|
616
|
-
void run('capture', '/capture', {});
|
|
617
|
-
},
|
|
618
|
-
},
|
|
619
|
-
icon('camera'),
|
|
620
|
-
),
|
|
621
|
-
state.running
|
|
622
|
-
? h(
|
|
623
|
-
'button',
|
|
624
|
-
{
|
|
625
|
-
type: 'button',
|
|
626
|
-
className: 'dvb-btn',
|
|
627
|
-
title: t('action.stop'),
|
|
628
|
-
'aria-label': t('action.stop'),
|
|
629
|
-
disabled: Boolean(busy),
|
|
630
|
-
onClick: function () {
|
|
631
|
-
void run('stop', '/stop', {});
|
|
632
|
-
},
|
|
633
|
-
},
|
|
634
|
-
icon('stop'),
|
|
635
|
-
)
|
|
636
|
-
: h(
|
|
637
|
-
'button',
|
|
638
|
-
{
|
|
639
|
-
type: 'button',
|
|
640
|
-
className: 'dvb-btn',
|
|
641
|
-
'data-variant': 'primary',
|
|
642
|
-
disabled: Boolean(busy),
|
|
643
|
-
onClick: openOrStart,
|
|
644
|
-
},
|
|
645
|
-
icon('play'),
|
|
646
|
-
t('action.start'),
|
|
647
|
-
),
|
|
648
|
-
),
|
|
1113
|
+
'span',
|
|
1114
|
+
{ className: 'dvb-title', title: state.title || state.url || t('panel.title') },
|
|
1115
|
+
state.title || shortUrl(state.url) || t('panel.title'),
|
|
649
1116
|
),
|
|
1117
|
+
h('span', { className: 'dvb-spacer' }),
|
|
1118
|
+
busy ? h('span', { className: 'dvb-status' }, t('busy')) : null,
|
|
650
1119
|
h(
|
|
651
1120
|
'div',
|
|
652
|
-
{ className: 'dvb-
|
|
653
|
-
h(
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
1121
|
+
{ className: 'dvb-tools' },
|
|
1122
|
+
h(
|
|
1123
|
+
'button',
|
|
1124
|
+
{
|
|
1125
|
+
type: 'button',
|
|
1126
|
+
className: 'dvb-btn',
|
|
1127
|
+
title: t('action.back'),
|
|
1128
|
+
'aria-label': t('action.back'),
|
|
1129
|
+
disabled: !state.running || Boolean(busy),
|
|
1130
|
+
onClick: function () {
|
|
1131
|
+
void run('back', '/nav', { action: 'back' });
|
|
1132
|
+
},
|
|
662
1133
|
},
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
1134
|
+
icon('back', 16),
|
|
1135
|
+
),
|
|
1136
|
+
h(
|
|
1137
|
+
'button',
|
|
1138
|
+
{
|
|
1139
|
+
type: 'button',
|
|
1140
|
+
className: 'dvb-btn',
|
|
1141
|
+
title: t('action.forward'),
|
|
1142
|
+
'aria-label': t('action.forward'),
|
|
1143
|
+
disabled: !state.running || Boolean(busy),
|
|
1144
|
+
onClick: function () {
|
|
1145
|
+
void run('forward', '/nav', { action: 'forward' });
|
|
1146
|
+
},
|
|
668
1147
|
},
|
|
669
|
-
|
|
1148
|
+
icon('forward', 16),
|
|
1149
|
+
),
|
|
670
1150
|
h(
|
|
671
1151
|
'button',
|
|
672
1152
|
{
|
|
673
1153
|
type: 'button',
|
|
674
1154
|
className: 'dvb-btn',
|
|
675
|
-
|
|
676
|
-
|
|
1155
|
+
title: t('action.reload'),
|
|
1156
|
+
'aria-label': t('action.reload'),
|
|
1157
|
+
disabled: !state.running || Boolean(busy),
|
|
1158
|
+
onClick: function () {
|
|
1159
|
+
void run('reload', '/nav', { action: 'reload' });
|
|
1160
|
+
},
|
|
677
1161
|
},
|
|
678
|
-
|
|
1162
|
+
icon('reload', 16),
|
|
679
1163
|
),
|
|
1164
|
+
state.running
|
|
1165
|
+
? h(
|
|
1166
|
+
'button',
|
|
1167
|
+
{
|
|
1168
|
+
type: 'button',
|
|
1169
|
+
className: 'dvb-btn',
|
|
1170
|
+
title: t('action.stop'),
|
|
1171
|
+
'aria-label': t('action.stop'),
|
|
1172
|
+
disabled: Boolean(busy),
|
|
1173
|
+
onClick: function () {
|
|
1174
|
+
void run('stop', '/stop', {});
|
|
1175
|
+
},
|
|
1176
|
+
},
|
|
1177
|
+
icon('stop', 16),
|
|
1178
|
+
)
|
|
1179
|
+
: h(
|
|
1180
|
+
'button',
|
|
1181
|
+
{
|
|
1182
|
+
type: 'button',
|
|
1183
|
+
className: 'dvb-btn',
|
|
1184
|
+
'data-variant': 'primary',
|
|
1185
|
+
disabled: Boolean(busy),
|
|
1186
|
+
onClick: openOrStart,
|
|
1187
|
+
},
|
|
1188
|
+
icon('play', 14),
|
|
1189
|
+
t('action.start'),
|
|
1190
|
+
),
|
|
1191
|
+
),
|
|
1192
|
+
),
|
|
1193
|
+
h(
|
|
1194
|
+
'div',
|
|
1195
|
+
{ className: 'dvb-addr' },
|
|
1196
|
+
h('input', {
|
|
1197
|
+
className: 'dvb-input',
|
|
1198
|
+
type: 'text',
|
|
1199
|
+
spellCheck: false,
|
|
1200
|
+
placeholder: t('url.placeholder'),
|
|
1201
|
+
value: draft,
|
|
1202
|
+
onChange: function (event) {
|
|
1203
|
+
setDirty(true);
|
|
1204
|
+
setDraft(event.target.value);
|
|
1205
|
+
},
|
|
1206
|
+
onKeyDown: function (event) {
|
|
1207
|
+
if (event.key === 'Enter') {
|
|
1208
|
+
event.preventDefault();
|
|
1209
|
+
openOrStart();
|
|
1210
|
+
}
|
|
1211
|
+
},
|
|
1212
|
+
}),
|
|
1213
|
+
h(
|
|
1214
|
+
'button',
|
|
1215
|
+
{
|
|
1216
|
+
type: 'button',
|
|
1217
|
+
className: 'dvb-btn',
|
|
1218
|
+
disabled: Boolean(busy) || draft.trim() === '',
|
|
1219
|
+
onClick: openOrStart,
|
|
1220
|
+
},
|
|
1221
|
+
t('action.open'),
|
|
680
1222
|
),
|
|
681
1223
|
),
|
|
682
1224
|
error ? h('div', { className: 'dvb-error' }, error) : null,
|
|
683
1225
|
!reachable ? h('div', { className: 'dvb-error' }, t('error.offline')) : null,
|
|
684
1226
|
h(
|
|
685
1227
|
'div',
|
|
686
|
-
{
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
1228
|
+
{
|
|
1229
|
+
className: 'dvb-stage',
|
|
1230
|
+
ref: stageRef,
|
|
1231
|
+
tabIndex: 0,
|
|
1232
|
+
'data-focus': String(focused),
|
|
1233
|
+
onFocus: function () {
|
|
1234
|
+
setFocused(true);
|
|
1235
|
+
},
|
|
1236
|
+
onBlur: function () {
|
|
1237
|
+
setFocused(false);
|
|
1238
|
+
},
|
|
1239
|
+
onKeyDown: onKeyDown,
|
|
1240
|
+
onKeyUp: onKeyUp,
|
|
1241
|
+
onPaste: onPaste,
|
|
1242
|
+
onContextMenu: function (event) {
|
|
1243
|
+
event.preventDefault();
|
|
1244
|
+
},
|
|
1245
|
+
},
|
|
1246
|
+
state.running
|
|
1247
|
+
? h('canvas', {
|
|
1248
|
+
ref: canvasRef,
|
|
1249
|
+
className: 'dvb-live',
|
|
1250
|
+
width: state.viewport && state.viewport.width ? state.viewport.width : 1280,
|
|
1251
|
+
height: state.viewport && state.viewport.height ? state.viewport.height : 800,
|
|
1252
|
+
role: 'img',
|
|
1253
|
+
'aria-label': state.title || state.url || t('panel.title'),
|
|
1254
|
+
'data-busy': String(!live),
|
|
1255
|
+
onPointerDown: onPointerDown,
|
|
1256
|
+
onPointerMove: onPointerMove,
|
|
1257
|
+
onPointerUp: onPointerUp,
|
|
1258
|
+
onPointerCancel: onPointerUp,
|
|
1259
|
+
onPointerLeave: hideCursor,
|
|
1260
|
+
onContextMenu: function (event) {
|
|
1261
|
+
event.preventDefault();
|
|
1262
|
+
},
|
|
1263
|
+
onDragStart: function (event) {
|
|
1264
|
+
event.preventDefault();
|
|
1265
|
+
},
|
|
1266
|
+
})
|
|
705
1267
|
: h(
|
|
706
1268
|
'div',
|
|
707
1269
|
{ className: 'dvb-empty' },
|
|
708
|
-
h(
|
|
709
|
-
width: 56,
|
|
710
|
-
height: 56,
|
|
711
|
-
viewBox: '0 0 24 24',
|
|
712
|
-
fill: 'none',
|
|
713
|
-
stroke: 'var(--dsw-alias-label-secondary)',
|
|
714
|
-
strokeWidth: 1.2,
|
|
715
|
-
'aria-hidden': true,
|
|
716
|
-
},
|
|
717
|
-
h('rect', { x: 2.5, y: 4, width: 19, height: 16, rx: 2.5 }),
|
|
718
|
-
h('path', { d: 'M2.5 8h19' }),
|
|
719
|
-
h('circle', { cx: 12, cy: 13.5, r: 2.6 }),
|
|
720
|
-
),
|
|
1270
|
+
h(BrowserGlyph, { size: 44 }),
|
|
721
1271
|
h('div', { className: 'dvb-empty-title' }, t('empty.title')),
|
|
722
1272
|
h('div', { className: 'dvb-empty-desc' }, t('empty.desc')),
|
|
723
|
-
state.running
|
|
724
|
-
? h('button', {
|
|
725
|
-
type: 'button',
|
|
726
|
-
className: 'dvb-btn',
|
|
727
|
-
disabled: Boolean(busy),
|
|
728
|
-
onClick: function () {
|
|
729
|
-
void run('capture', '/capture', {});
|
|
730
|
-
},
|
|
731
|
-
}, t('action.capture'))
|
|
732
|
-
: null,
|
|
733
1273
|
),
|
|
1274
|
+
state.running && !live
|
|
1275
|
+
? h(
|
|
1276
|
+
'button',
|
|
1277
|
+
{
|
|
1278
|
+
type: 'button',
|
|
1279
|
+
className: 'dvb-veil',
|
|
1280
|
+
onClick: retryStream,
|
|
1281
|
+
title: t('action.reconnect'),
|
|
1282
|
+
},
|
|
1283
|
+
t(retrying ? 'error.frame' : 'connecting'),
|
|
1284
|
+
)
|
|
1285
|
+
: null,
|
|
1286
|
+
h('span', { className: 'dvb-cursor', ref: cursorRef, 'data-visible': 'false' }),
|
|
1287
|
+
h('span', { className: 'dvb-ripple', ref: rippleRef }),
|
|
734
1288
|
),
|
|
1289
|
+
state.running && state.allowUserControl
|
|
1290
|
+
? h(
|
|
1291
|
+
'div',
|
|
1292
|
+
{ className: 'dvb-hint' },
|
|
1293
|
+
h('span', null, t('hint.interactive')),
|
|
1294
|
+
state.headless ? h('span', null, t('hint.headless')) : null,
|
|
1295
|
+
)
|
|
1296
|
+
: null,
|
|
735
1297
|
h(
|
|
736
1298
|
'div',
|
|
737
|
-
{ className: 'dvb-
|
|
1299
|
+
{ className: 'dvb-log', 'data-open': String(logOpen) },
|
|
738
1300
|
h(
|
|
739
|
-
'
|
|
740
|
-
{
|
|
741
|
-
|
|
742
|
-
|
|
1301
|
+
'button',
|
|
1302
|
+
{
|
|
1303
|
+
type: 'button',
|
|
1304
|
+
className: 'dvb-log-head',
|
|
1305
|
+
title: logOpen ? t('timeline.collapse') : t('timeline.expand'),
|
|
1306
|
+
'aria-expanded': String(logOpen),
|
|
1307
|
+
onClick: function () {
|
|
1308
|
+
setLogOpen(function (value) {
|
|
1309
|
+
return !value;
|
|
1310
|
+
});
|
|
1311
|
+
},
|
|
1312
|
+
},
|
|
1313
|
+
h('span', { className: 'dvb-log-chevron' }, icon('chevron', 12)),
|
|
1314
|
+
h('span', { className: 'dvb-log-title' }, t('timeline.title')),
|
|
1315
|
+
h('span', { className: 'dvb-log-count' }, actions.length > 0 ? String(actions.length) : ''),
|
|
743
1316
|
h('span', { className: 'dvb-spacer' }),
|
|
744
|
-
|
|
745
|
-
? h(
|
|
746
|
-
'button',
|
|
747
|
-
{
|
|
748
|
-
type: 'button',
|
|
749
|
-
className: 'dvb-btn',
|
|
750
|
-
onClick: function () {
|
|
751
|
-
setPreviewSeq(0);
|
|
752
|
-
},
|
|
753
|
-
},
|
|
754
|
-
t('timeline.latest'),
|
|
755
|
-
)
|
|
756
|
-
: null,
|
|
1317
|
+
reachable ? null : h('span', { className: 'dvb-status' }, t('state.offline')),
|
|
757
1318
|
),
|
|
758
|
-
|
|
759
|
-
?
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
className: 'dvb-row',
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
if (action.frameSeq) setPreviewSeq(action.frameSeq);
|
|
777
|
-
},
|
|
778
|
-
},
|
|
779
|
-
h('span', { className: 'dvb-row-time' }, clock(action.ts)),
|
|
780
|
-
h('span', { className: 'dvb-row-kind', 'data-kind': action.kind }, kindLabel(action.kind)),
|
|
781
|
-
h('span', { className: 'dvb-row-text', 'data-error': String(action.ok === false) }, label),
|
|
782
|
-
);
|
|
783
|
-
}),
|
|
784
|
-
),
|
|
1319
|
+
logOpen
|
|
1320
|
+
? actions.length === 0
|
|
1321
|
+
? h('div', { className: 'dvb-log-empty' }, t('timeline.empty'))
|
|
1322
|
+
: h(
|
|
1323
|
+
'div',
|
|
1324
|
+
{ className: 'dvb-log-body' },
|
|
1325
|
+
actions.map(function (action) {
|
|
1326
|
+
var label = action.detail || action.target || kindLabel(action.kind);
|
|
1327
|
+
return h(
|
|
1328
|
+
'div',
|
|
1329
|
+
{ key: action.seq, className: 'dvb-row', title: label },
|
|
1330
|
+
h('span', { className: 'dvb-row-time' }, clock(action.ts)),
|
|
1331
|
+
h('span', { className: 'dvb-row-kind', 'data-kind': action.kind }, kindLabel(action.kind)),
|
|
1332
|
+
h('span', { className: 'dvb-row-text', 'data-error': String(action.ok === false) }, label),
|
|
1333
|
+
);
|
|
1334
|
+
}),
|
|
1335
|
+
)
|
|
1336
|
+
: null,
|
|
785
1337
|
),
|
|
786
1338
|
);
|
|
787
1339
|
}
|
|
788
1340
|
|
|
789
|
-
//
|
|
1341
|
+
// -------------------------------------------------------------------
|
|
1342
|
+
// Tab title
|
|
1343
|
+
// -------------------------------------------------------------------
|
|
1344
|
+
|
|
1345
|
+
function BrowserTabTitle(props) {
|
|
1346
|
+
var t = props.t || translateCurrent;
|
|
1347
|
+
var info = null;
|
|
1348
|
+
if (typeof props.useTabInfo === 'function') info = props.useTabInfo();
|
|
1349
|
+
var title = info && info.tab && info.tab.title ? info.tab.title : t('panel.title');
|
|
1350
|
+
return h(
|
|
1351
|
+
'span',
|
|
1352
|
+
{ style: { display: 'inline-flex', alignItems: 'center', gap: '6px', minWidth: 0 } },
|
|
1353
|
+
h(BrowserGlyph, { size: 15 }),
|
|
1354
|
+
h('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, title),
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// -------------------------------------------------------------------
|
|
790
1359
|
// Module
|
|
791
|
-
//
|
|
1360
|
+
// -------------------------------------------------------------------
|
|
792
1361
|
|
|
793
1362
|
return {
|
|
794
1363
|
inject: ['slots', 'locale'],
|
|
@@ -800,6 +1369,7 @@
|
|
|
800
1369
|
console.warn('[dsh-visual-browser] slots service missing; panel disabled');
|
|
801
1370
|
return;
|
|
802
1371
|
}
|
|
1372
|
+
|
|
803
1373
|
var register = function (slot, options, component, label) {
|
|
804
1374
|
try {
|
|
805
1375
|
ctx.effect(function () {
|
|
@@ -811,23 +1381,93 @@
|
|
|
811
1381
|
console.warn('[dsh-visual-browser] could not register ' + slot + ':', error && error.message ? error.message : error);
|
|
812
1382
|
}
|
|
813
1383
|
};
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1384
|
+
|
|
1385
|
+
// A guide card may open our tab through `tab.actions`, but the
|
|
1386
|
+
// controller keeps working from any seat; hold it for the fallback.
|
|
1387
|
+
ctx.inject(['sidebarRight'], function (scope) {
|
|
1388
|
+
var service = scope.get('sidebarRight');
|
|
1389
|
+
if (!service || typeof service.openTab !== 'function') return;
|
|
1390
|
+
var open = service.openTab.bind(service);
|
|
1391
|
+
sidebarRightOpen = open;
|
|
1392
|
+
scope.effect(
|
|
1393
|
+
function () {
|
|
1394
|
+
return function () {
|
|
1395
|
+
if (sidebarRightOpen === open) sidebarRightOpen = null;
|
|
1396
|
+
};
|
|
1397
|
+
},
|
|
1398
|
+
'dsh-visual-browser: sidebar controller handle',
|
|
1399
|
+
);
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
// The tab type waits for the sidebar-right registry: sampling it once
|
|
1403
|
+
// in apply leaves the type unregistered on a cold page boot.
|
|
1404
|
+
ctx.inject(['sidebarRightTabs'], function (scope) {
|
|
1405
|
+
var tabs = scope.get('sidebarRightTabs');
|
|
1406
|
+
if (!tabs || typeof tabs.register !== 'function') {
|
|
1407
|
+
console.warn('[dsh-visual-browser] sidebarRightTabs service missing; tab type not registered');
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
scope.effect(
|
|
1411
|
+
function () {
|
|
1412
|
+
return tabs.register({
|
|
1413
|
+
id: MODULE_ID,
|
|
1414
|
+
kind: KIND,
|
|
1415
|
+
priority: 'extension',
|
|
1416
|
+
title: function () {
|
|
1417
|
+
return translateCurrent('panel.title');
|
|
1418
|
+
},
|
|
1419
|
+
guide: [
|
|
1420
|
+
{
|
|
1421
|
+
id: 'open',
|
|
1422
|
+
order: 40,
|
|
1423
|
+
title: function () {
|
|
1424
|
+
return translateCurrent('guide.title');
|
|
1425
|
+
},
|
|
1426
|
+
description: function () {
|
|
1427
|
+
return translateCurrent('guide.description');
|
|
1428
|
+
},
|
|
1429
|
+
},
|
|
1430
|
+
],
|
|
1431
|
+
});
|
|
821
1432
|
},
|
|
1433
|
+
'dsh-visual-browser: tab type',
|
|
1434
|
+
);
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
register(
|
|
1438
|
+
'sidebar.right.pane.tab',
|
|
1439
|
+
{ key: MODULE_ID, locale: NS },
|
|
1440
|
+
function (props) {
|
|
1441
|
+
return h(BrowserTab, { ctx: ctx, t: props.t });
|
|
822
1442
|
},
|
|
1443
|
+
'dsh-visual-browser: tab body',
|
|
1444
|
+
);
|
|
1445
|
+
register(
|
|
1446
|
+
'sidebar.right.pane.tab.title',
|
|
1447
|
+
{ key: MODULE_ID, locale: NS },
|
|
1448
|
+
BrowserTabTitle,
|
|
1449
|
+
'dsh-visual-browser: tab title',
|
|
1450
|
+
);
|
|
1451
|
+
// Our own card only stands in when the built-in Browser has no card
|
|
1452
|
+
// for us to extend; normally the Browser card carries the extra row.
|
|
1453
|
+
register(
|
|
1454
|
+
'sidebar.right.tab.guide.entry',
|
|
1455
|
+
{ key: MODULE_ID, locale: NS },
|
|
823
1456
|
function (props) {
|
|
824
|
-
|
|
1457
|
+
var tabs = ctx.get('sidebarRightTabs');
|
|
1458
|
+
var browserPresent =
|
|
1459
|
+
tabs !== undefined && typeof tabs.get === 'function' && tabs.get(BROWSER_KIND) !== undefined;
|
|
1460
|
+
if (browserPresent) return null;
|
|
1461
|
+
return h(VisualBrowserGuideCard, props);
|
|
825
1462
|
},
|
|
826
|
-
'dsh-visual-browser:
|
|
1463
|
+
'dsh-visual-browser: own guide card',
|
|
1464
|
+
);
|
|
1465
|
+
register(
|
|
1466
|
+
'sidebar.right.tab.guide.entry',
|
|
1467
|
+
{ key: BROWSER_TYPE_ID, locale: NS },
|
|
1468
|
+
BrowserExtraGuideCard,
|
|
1469
|
+
'dsh-visual-browser: browser guide card',
|
|
827
1470
|
);
|
|
828
|
-
register('main', { key: 'visual-browser' }, function () {
|
|
829
|
-
return h(BrowserPage, { ctx: ctx });
|
|
830
|
-
}, 'dsh-visual-browser: main page');
|
|
831
1471
|
},
|
|
832
1472
|
};
|
|
833
1473
|
},
|