Browser / static /browser.html
abcd118q's picture
Update static/browser.html
b7de548 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Browser</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#dee1e6;
--tab-active:#ffffff;
--blue:#1a73e8;
--text:#202124;
--icon:#5f6368;
--hover:rgba(0,0,0,0.08);
--green:#188038;
}
html,body{
height:100vh;overflow:hidden;
font-family:-apple-system,'Segoe UI',Roboto,sans-serif;
font-size:13px;background:#202124;
display:flex;flex-direction:column;
}
/* ── Tab strip ── */
#tab-strip{
display:flex;align-items:flex-end;
background:var(--bg);
padding:6px 0 0 6px;height:36px;
-webkit-app-region:drag;user-select:none;
}
.tab{
display:flex;align-items:center;gap:6px;
padding:0 8px 0 10px;height:28px;
background:var(--tab-active);
border-radius:8px 8px 0 0;
min-width:120px;max-width:200px;flex:0 0 auto;
position:relative;cursor:default;
}
.tab-fav{width:16px;height:16px;border-radius:2px;flex-shrink:0}
.tab-lbl{
flex:1;overflow:hidden;white-space:nowrap;
text-overflow:ellipsis;color:var(--text);font-size:12px;
}
.tab-x{
width:16px;height:16px;border:none;background:transparent;
border-radius:50%;cursor:pointer;color:var(--icon);
font-size:16px;line-height:1;display:flex;
align-items:center;justify-content:center;flex-shrink:0;
}
.tab-x:hover{background:rgba(0,0,0,0.12)}
/* ── Toolbar ── */
#toolbar{
display:flex;align-items:center;gap:2px;
background:var(--bg);padding:4px 8px;height:40px;
}
.nb{
width:32px;height:32px;border:none;background:transparent;
border-radius:50%;cursor:pointer;display:flex;
align-items:center;justify-content:center;
color:var(--icon);flex-shrink:0;
}
.nb:hover:not([disabled]){background:var(--hover)}
.nb[disabled]{opacity:.38;cursor:default}
.nb svg{width:18px;height:18px;fill:var(--icon)}
/* Omnibox */
#omni{
flex:1;display:flex;align-items:center;
background:#fff;border-radius:100px;
height:32px;padding:0 10px;gap:6px;
border:1.5px solid transparent;
transition:border-color .15s,box-shadow .15s;
}
#omni:focus-within{
border-color:var(--blue);
box-shadow:0 0 0 3px rgba(26,115,232,.2);
}
#lock{display:flex;align-items:center;flex-shrink:0}
#lock svg{width:13px;height:13px;fill:var(--green)}
#url{
flex:1;border:none;outline:none;
font-size:13px;color:var(--text);
background:transparent;min-width:0;
}
#star{flex-shrink:0;cursor:pointer;color:var(--icon);line-height:0}
#star svg{width:16px;height:16px;fill:var(--icon)}
/* ── Viewport ── */
#vp-wrap{
flex:1;background:#fff;position:relative;overflow:hidden;
}
#vp{
width:100%;height:100%;display:block;
cursor:default;outline:none;
}
/* Loading */
#overlay{
position:absolute;inset:0;background:#fff;
display:flex;flex-direction:column;
align-items:center;justify-content:center;
gap:14px;z-index:9;transition:opacity .3s;
}
#overlay.gone{opacity:0;pointer-events:none}
.spin{
width:36px;height:36px;border:3px solid #dee1e6;
border-top-color:var(--blue);border-radius:50%;
animation:sp .7s linear infinite;
}
@keyframes sp{to{transform:rotate(360deg)}}
/* Status */
#status{
position:absolute;bottom:8px;right:10px;
background:rgba(0,0,0,.5);color:#fff;
font-size:11px;padding:2px 8px;border-radius:10px;
z-index:10;pointer-events:none;transition:opacity .5s;
}
#status.ok{opacity:0}
</style>
</head>
<body>
<div id="tab-strip">
<div class="tab">
<img class="tab-fav" id="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">
<span class="tab-lbl" id="t-title">New Tab</span>
<button class="tab-x">βœ•</button>
</div>
</div>
<div id="toolbar">
<button class="nb" id="b-back" disabled title="Go back">
<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>
</button>
<button class="nb" id="b-fwd" disabled title="Go forward">
<svg viewBox="0 0 24 24"><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"/></svg>
</button>
<button class="nb" id="b-rel" title="Reload">
<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>
</button>
<div id="omni">
<span id="lock">
<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>
</span>
<input id="url" type="text" placeholder="Search or type a URL"
autocomplete="off" spellcheck="false">
<span id="star">
<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>
</span>
</div>
<button class="nb" title="Chrome menu" style="margin-left:2px">
<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>
</button>
</div>
<div id="vp-wrap">
<canvas id="vp" tabindex="0"></canvas>
<div id="overlay">
<div class="spin"></div>
<span style="color:#5f6368">Connecting to browser…</span>
</div>
<div id="status">●</div>
</div>
<script>
const vp = document.getElementById('vp');
const ctx = vp.getContext('2d');
const urlIn = document.getElementById('url');
const tTitle = document.getElementById('t-title');
const fav = document.getElementById('fav');
const overlay = document.getElementById('overlay');
const statusEl= document.getElementById('status');
const bBack = document.getElementById('b-back');
const bFwd = document.getElementById('b-fwd');
const bRel = document.getElementById('b-rel');
const lock = document.getElementById('lock');
const BW = 1280, BH = 800;
// ── WebSocket ─────────────────────────────────────────────────────
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
// FIXED WEBSOCKET URL HERE
const WS = `${proto}//${location.host}/ws`;
let ws = null;
function setStatus(msg, ok=false){
statusEl.textContent = msg;
statusEl.className = ok ? 'ok' : '';
}
function connect(){
ws = new WebSocket(WS);
ws.onopen = () => { setStatus('●', true); overlay.classList.add('gone'); };
ws.onclose = () => { setStatus('Reconnecting…'); setTimeout(connect, 2000); };
ws.onerror = () => setStatus('Error');
ws.onmessage = async ({data}) => {
const msg = JSON.parse(data);
if (msg.type === 'frame'){
const ab = Uint8Array.from(atob(msg.data), c=>c.charCodeAt(0));
const bmp = await createImageBitmap(new Blob([ab],{type:'image/jpeg'}));
const cw = vp.parentElement.clientWidth;
const ch = vp.parentElement.clientHeight;
if (vp.width!==cw) vp.width=cw;
if (vp.height!==ch) vp.height=ch;
ctx.drawImage(bmp, 0, 0, cw, ch);
bmp.close();
} else if (msg.type === 'nav'){
urlIn.value = msg.url||'';
document.title = tTitle.textContent = msg.title||'New Tab';
// favicon
try {
const u = new URL(msg.url);
fav.src = u.origin+'/favicon.ico';
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";
} catch{}
// lock colour
const secure = msg.url?.startsWith('https');
lock.style.color = secure ? '#188038' : '#5f6368';
lock.querySelector('svg').style.fill = lock.style.color;
}
};
}
const send = obj => ws?.readyState===1 && ws.send(JSON.stringify(obj));
// ── Coords ────────────────────────────────────────────────────────
const coord = e => {
const r = vp.getBoundingClientRect();
return {
x: Math.round((e.clientX-r.left) * (BW/r.width)),
y: Math.round((e.clientY-r.top) * (BH/r.height))
};
};
// ── Mouse ─────────────────────────────────────────────────────────
vp.onmousedown = e => { vp.focus(); send({type:'mousedown', ...coord(e)}); };
vp.onmouseup = e => send({type:'mouseup', ...coord(e)});
vp.onclick = e => send({type:'click', ...coord(e)});
vp.ondblclick = e => send({type:'dblclick', ...coord(e)});
vp.onmousemove = e => send({type:'mousemove', ...coord(e)});
vp.onwheel = e => { e.preventDefault(); send({type:'wheel',dx:e.deltaX,dy:e.deltaY}); };
vp.oncontextmenu= e => e.preventDefault();
// ── Keyboard ──────────────────────────────────────────────────────
const SPECIALS = new Set([
'Enter','Backspace','Delete','Tab','Escape',
'ArrowLeft','ArrowRight','ArrowUp','ArrowDown',
'Home','End','PageUp','PageDown',
'F1','F2','F3','F4','F5','F6','F7','F8','F9','F10','F11','F12',
'Insert','CapsLock'
]);
vp.addEventListener('keydown', e => {
e.preventDefault();
const k = e.key;
if (e.ctrlKey || e.metaKey || e.altKey || SPECIALS.has(k)){
send({type:'keydown', key:k,
ctrl:e.ctrlKey, shift:e.shiftKey, alt:e.altKey, meta:e.metaKey});
} else if (k.length===1){
send({type:'type', text:k});
}
});
// ── URL bar ───────────────────────────────────────────────────────
urlIn.addEventListener('keydown', e => {
if (e.key==='Enter'){
send({type:'navigate', url:urlIn.value.trim()});
vp.focus();
}
e.stopPropagation();
});
urlIn.onclick = e => e.stopPropagation();
// ── Nav buttons ───────────────────────────────────────────────────
bBack.onclick = () => send({type:'back'});
bFwd.onclick = () => send({type:'forward'});
bRel.onclick = () => send({type:'reload'});
// ── Resize ────────────────────────────────────────────────────────
new ResizeObserver(() => {
vp.width = vp.parentElement.clientWidth;
vp.height = vp.parentElement.clientHeight;
}).observe(vp.parentElement);
connect();
</script>
</body>
</html>