abc1181 commited on
Commit
3f7f1bb
Β·
verified Β·
1 Parent(s): 56a7b7f

Create static/browser.html

Browse files
Files changed (1) hide show
  1. static/browser.html +302 -0
static/browser.html ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Browser</title>
6
+ <style>
7
+ *{box-sizing:border-box;margin:0;padding:0}
8
+
9
+ :root{
10
+ --bg:#dee1e6;
11
+ --tab-active:#ffffff;
12
+ --blue:#1a73e8;
13
+ --text:#202124;
14
+ --icon:#5f6368;
15
+ --hover:rgba(0,0,0,0.08);
16
+ --green:#188038;
17
+ }
18
+
19
+ html,body{
20
+ height:100vh;overflow:hidden;
21
+ font-family:-apple-system,'Segoe UI',Roboto,sans-serif;
22
+ font-size:13px;background:#202124;
23
+ display:flex;flex-direction:column;
24
+ }
25
+
26
+ /* ── Tab strip ── */
27
+ #tab-strip{
28
+ display:flex;align-items:flex-end;
29
+ background:var(--bg);
30
+ padding:6px 0 0 6px;height:36px;
31
+ -webkit-app-region:drag;user-select:none;
32
+ }
33
+ .tab{
34
+ display:flex;align-items:center;gap:6px;
35
+ padding:0 8px 0 10px;height:28px;
36
+ background:var(--tab-active);
37
+ border-radius:8px 8px 0 0;
38
+ min-width:120px;max-width:200px;flex:0 0 auto;
39
+ position:relative;cursor:default;
40
+ }
41
+ .tab-fav{width:16px;height:16px;border-radius:2px;flex-shrink:0}
42
+ .tab-lbl{
43
+ flex:1;overflow:hidden;white-space:nowrap;
44
+ text-overflow:ellipsis;color:var(--text);font-size:12px;
45
+ }
46
+ .tab-x{
47
+ width:16px;height:16px;border:none;background:transparent;
48
+ border-radius:50%;cursor:pointer;color:var(--icon);
49
+ font-size:16px;line-height:1;display:flex;
50
+ align-items:center;justify-content:center;flex-shrink:0;
51
+ }
52
+ .tab-x:hover{background:rgba(0,0,0,0.12)}
53
+
54
+ /* ── Toolbar ── */
55
+ #toolbar{
56
+ display:flex;align-items:center;gap:2px;
57
+ background:var(--bg);padding:4px 8px;height:40px;
58
+ }
59
+ .nb{
60
+ width:32px;height:32px;border:none;background:transparent;
61
+ border-radius:50%;cursor:pointer;display:flex;
62
+ align-items:center;justify-content:center;
63
+ color:var(--icon);flex-shrink:0;
64
+ }
65
+ .nb:hover:not([disabled]){background:var(--hover)}
66
+ .nb[disabled]{opacity:.38;cursor:default}
67
+ .nb svg{width:18px;height:18px;fill:var(--icon)}
68
+
69
+ /* Omnibox */
70
+ #omni{
71
+ flex:1;display:flex;align-items:center;
72
+ background:#fff;border-radius:100px;
73
+ height:32px;padding:0 10px;gap:6px;
74
+ border:1.5px solid transparent;
75
+ transition:border-color .15s,box-shadow .15s;
76
+ }
77
+ #omni:focus-within{
78
+ border-color:var(--blue);
79
+ box-shadow:0 0 0 3px rgba(26,115,232,.2);
80
+ }
81
+ #lock{display:flex;align-items:center;flex-shrink:0}
82
+ #lock svg{width:13px;height:13px;fill:var(--green)}
83
+ #url{
84
+ flex:1;border:none;outline:none;
85
+ font-size:13px;color:var(--text);
86
+ background:transparent;min-width:0;
87
+ }
88
+ #star{flex-shrink:0;cursor:pointer;color:var(--icon);line-height:0}
89
+ #star svg{width:16px;height:16px;fill:var(--icon)}
90
+
91
+ /* ── Viewport ── */
92
+ #vp-wrap{
93
+ flex:1;background:#fff;position:relative;overflow:hidden;
94
+ }
95
+ #vp{
96
+ width:100%;height:100%;display:block;
97
+ cursor:default;outline:none;
98
+ }
99
+
100
+ /* Loading */
101
+ #overlay{
102
+ position:absolute;inset:0;background:#fff;
103
+ display:flex;flex-direction:column;
104
+ align-items:center;justify-content:center;
105
+ gap:14px;z-index:9;transition:opacity .3s;
106
+ }
107
+ #overlay.gone{opacity:0;pointer-events:none}
108
+ .spin{
109
+ width:36px;height:36px;border:3px solid #dee1e6;
110
+ border-top-color:var(--blue);border-radius:50%;
111
+ animation:sp .7s linear infinite;
112
+ }
113
+ @keyframes sp{to{transform:rotate(360deg)}}
114
+
115
+ /* Status */
116
+ #status{
117
+ position:absolute;bottom:8px;right:10px;
118
+ background:rgba(0,0,0,.5);color:#fff;
119
+ font-size:11px;padding:2px 8px;border-radius:10px;
120
+ z-index:10;pointer-events:none;transition:opacity .5s;
121
+ }
122
+ #status.ok{opacity:0}
123
+ </style>
124
+ </head>
125
+ <body>
126
+
127
+ <!-- Tab strip -->
128
+ <div id="tab-strip">
129
+ <div class="tab">
130
+ <img class="tab-fav" id="fav"
131
+ src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='7' fill='%23bbb'/%3E%3C/svg%3E">
132
+ <span class="tab-lbl" id="t-title">New Tab</span>
133
+ <button class="tab-x">βœ•</button>
134
+ </div>
135
+ </div>
136
+
137
+ <!-- Toolbar -->
138
+ <div id="toolbar">
139
+ <!-- Back -->
140
+ <button class="nb" id="b-back" disabled title="Go back">
141
+ <svg viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
142
+ </button>
143
+ <!-- Forward -->
144
+ <button class="nb" id="b-fwd" disabled title="Go forward">
145
+ <svg viewBox="0 0 24 24"><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"/></svg>
146
+ </button>
147
+ <!-- Reload -->
148
+ <button class="nb" id="b-rel" title="Reload">
149
+ <svg viewBox="0 0 24 24"><path d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
150
+ </button>
151
+
152
+ <!-- Omnibox -->
153
+ <div id="omni">
154
+ <span id="lock">
155
+ <svg viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>
156
+ </span>
157
+ <input id="url" type="text" placeholder="Search or type a URL"
158
+ autocomplete="off" spellcheck="false">
159
+ <span id="star">
160
+ <svg viewBox="0 0 24 24"><path d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z"/></svg>
161
+ </span>
162
+ </div>
163
+
164
+ <!-- Menu -->
165
+ <button class="nb" title="Chrome menu" style="margin-left:2px">
166
+ <svg viewBox="0 0 24 24"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg>
167
+ </button>
168
+ </div>
169
+
170
+ <!-- Viewport -->
171
+ <div id="vp-wrap">
172
+ <canvas id="vp" tabindex="0"></canvas>
173
+ <div id="overlay">
174
+ <div class="spin"></div>
175
+ <span style="color:#5f6368">Connecting to browser…</span>
176
+ </div>
177
+ <div id="status">●</div>
178
+ </div>
179
+
180
+ <script>
181
+ const vp = document.getElementById('vp');
182
+ const ctx = vp.getContext('2d');
183
+ const urlIn = document.getElementById('url');
184
+ const tTitle = document.getElementById('t-title');
185
+ const fav = document.getElementById('fav');
186
+ const overlay = document.getElementById('overlay');
187
+ const statusEl= document.getElementById('status');
188
+ const bBack = document.getElementById('b-back');
189
+ const bFwd = document.getElementById('b-fwd');
190
+ const bRel = document.getElementById('b-rel');
191
+ const lock = document.getElementById('lock');
192
+
193
+ const BW = 1280, BH = 800;
194
+
195
+ // ── WebSocket ─────────────────────────────────────────────────────
196
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
197
+ const WS = `${proto}//${location.host}/browser/ws`;
198
+ let ws = null;
199
+
200
+ function setStatus(msg, ok=false){
201
+ statusEl.textContent = msg;
202
+ statusEl.className = ok ? 'ok' : '';
203
+ }
204
+
205
+ function connect(){
206
+ ws = new WebSocket(WS);
207
+ ws.onopen = () => { setStatus('●', true); overlay.classList.add('gone'); };
208
+ ws.onclose = () => { setStatus('Reconnecting…'); setTimeout(connect, 2000); };
209
+ ws.onerror = () => setStatus('Error');
210
+ ws.onmessage = async ({data}) => {
211
+ const msg = JSON.parse(data);
212
+ if (msg.type === 'frame'){
213
+ const ab = Uint8Array.from(atob(msg.data), c=>c.charCodeAt(0));
214
+ const bmp = await createImageBitmap(new Blob([ab],{type:'image/jpeg'}));
215
+ const cw = vp.parentElement.clientWidth;
216
+ const ch = vp.parentElement.clientHeight;
217
+ if (vp.width!==cw) vp.width=cw;
218
+ if (vp.height!==ch) vp.height=ch;
219
+ ctx.drawImage(bmp, 0, 0, cw, ch);
220
+ bmp.close();
221
+ } else if (msg.type === 'nav'){
222
+ urlIn.value = msg.url||'';
223
+ document.title = tTitle.textContent = msg.title||'New Tab';
224
+ // favicon
225
+ try {
226
+ const u = new URL(msg.url);
227
+ fav.src = u.origin+'/favicon.ico';
228
+ fav.onerror = () => fav.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='7' fill='%23bbb'/%3E%3C/svg%3E";
229
+ } catch{}
230
+ // lock colour
231
+ const secure = msg.url?.startsWith('https');
232
+ lock.style.color = secure ? '#188038' : '#5f6368';
233
+ lock.querySelector('svg').style.fill = lock.style.color;
234
+ }
235
+ };
236
+ }
237
+
238
+ const send = obj => ws?.readyState===1 && ws.send(JSON.stringify(obj));
239
+
240
+ // ── Coords ────────────────────────────────────────────────────────
241
+ const coord = e => {
242
+ const r = vp.getBoundingClientRect();
243
+ return {
244
+ x: Math.round((e.clientX-r.left) * (BW/r.width)),
245
+ y: Math.round((e.clientY-r.top) * (BH/r.height))
246
+ };
247
+ };
248
+
249
+ // ── Mouse ─────────────────────────────────────────────────────────
250
+ vp.onmousedown = e => { vp.focus(); send({type:'mousedown', ...coord(e)}); };
251
+ vp.onmouseup = e => send({type:'mouseup', ...coord(e)});
252
+ vp.onclick = e => send({type:'click', ...coord(e)});
253
+ vp.ondblclick = e => send({type:'dblclick', ...coord(e)});
254
+ vp.onmousemove = e => send({type:'mousemove', ...coord(e)});
255
+ vp.onwheel = e => { e.preventDefault(); send({type:'wheel',dx:e.deltaX,dy:e.deltaY}); };
256
+ vp.oncontextmenu= e => e.preventDefault();
257
+
258
+ // ── Keyboard ──────────────────────────────────────────────────────
259
+ const SPECIALS = new Set([
260
+ 'Enter','Backspace','Delete','Tab','Escape',
261
+ 'ArrowLeft','ArrowRight','ArrowUp','ArrowDown',
262
+ 'Home','End','PageUp','PageDown',
263
+ 'F1','F2','F3','F4','F5','F6','F7','F8','F9','F10','F11','F12',
264
+ 'Insert','CapsLock'
265
+ ]);
266
+
267
+ vp.addEventListener('keydown', e => {
268
+ e.preventDefault();
269
+ const k = e.key;
270
+ if (e.ctrlKey || e.metaKey || e.altKey || SPECIALS.has(k)){
271
+ send({type:'keydown', key:k,
272
+ ctrl:e.ctrlKey, shift:e.shiftKey, alt:e.altKey, meta:e.metaKey});
273
+ } else if (k.length===1){
274
+ send({type:'type', text:k});
275
+ }
276
+ });
277
+
278
+ // ── URL bar ───��───────────────────────────────────────────────────
279
+ urlIn.addEventListener('keydown', e => {
280
+ if (e.key==='Enter'){
281
+ send({type:'navigate', url:urlIn.value.trim()});
282
+ vp.focus();
283
+ }
284
+ e.stopPropagation();
285
+ });
286
+ urlIn.onclick = e => e.stopPropagation();
287
+
288
+ // ── Nav buttons ───────────────────────────────────────────────────
289
+ bBack.onclick = () => send({type:'back'});
290
+ bFwd.onclick = () => send({type:'forward'});
291
+ bRel.onclick = () => send({type:'reload'});
292
+
293
+ // ── Resize ────────────────────────────────────────────────────────
294
+ new ResizeObserver(() => {
295
+ vp.width = vp.parentElement.clientWidth;
296
+ vp.height = vp.parentElement.clientHeight;
297
+ }).observe(vp.parentElement);
298
+
299
+ connect();
300
+ </script>
301
+ </body>
302
+ </html>