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