var PAGE_WEBVIEW_LABEL = "page"; var PAGE_WEBVIEW_LAYER = 0; var CHROME_WEBVIEW_LAYER = 10; var form = document.querySelector("#browser-form"); var addressInput = document.querySelector("#url-input"); var addressBar = document.querySelector("#address-bar"); var addressIcon = document.querySelector("#address-icon"); var suggestions = document.querySelector("#suggestions"); var backButton = document.querySelector("#back"); var forwardButton = document.querySelector("#forward"); var reloadButton = document.querySelector("#reload"); var statusText = document.querySelector("#status-text"); var emptyState = document.querySelector("#empty-state"); var errorState = document.querySelector("#error-state"); var errorTitle = document.querySelector("#error-title"); var errorDetail = document.querySelector("#error-detail"); var errorRetry = document.querySelector("#error-retry"); var toolbar = document.querySelector("#toolbar"); var pageWebView = null; var currentUrl = ""; var navHistory = []; var historyIndex = -1; var visitedUrls = []; var resizeHandle = 0; var resizeInFlight = false; var resizeRequested = false; var viewportPollHandle = 0; var isLoading = false; var statusTimer = null; var zoomLevel = 1.0; var ZOOM_STEP = 0.1; var ZOOM_MIN = 0.25; var ZOOM_MAX = 5.0; var suggestActive = -1; var suggestFiltered = []; // The main WebView is later resized to chrome height, so keep the original // full-window viewport for positioning the child page WebView. var browserViewport = { width: Math.max(1, Math.floor(window.innerWidth)), height: Math.max(1, Math.floor(window.innerHeight)), }; var browserViewportRevision = 0; var lastNativeResizeAt = 0; var nativeViewportInset = null; function setStatus(message, autohide) { clearTimeout(statusTimer); statusText.textContent = message; if (autohide) { statusTimer = setTimeout(function () { statusText.textContent = ""; }, autohide); } } function setLoading(loading) { isLoading = loading; if (loading) { addressBar.classList.remove("load-complete"); addressBar.classList.add("loading"); } else { addressBar.classList.remove("loading"); addressBar.classList.add("load-complete"); setTimeout(function () { addressBar.classList.remove("load-complete"); }, 600); } } function updateSecurityIndicator(url) { if (url.startsWith("https://")) { addressBar.classList.add("secure"); } else { addressBar.classList.remove("secure"); } } function normalizeUrl(value) { var trimmed = value.trim(); if (!trimmed) return "https://example.com"; if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) return trimmed; if (/^[a-z0-9]([a-z0-9-]*\.)+[a-z]{2,}/i.test(trimmed)) return "https://" + trimmed; return "https://" + trimmed; } function canonicalUrlKey(value) { var trimmed = String(value || "").trim(); if (!trimmed) return ""; try { return new URL(trimmed).href; } catch (_) { return trimmed.toLowerCase(); } } function toolbarHeight() { var toolbarRect = toolbar.getBoundingClientRect(); return Math.ceil(toolbarRect.height); } function chromeHeight() { var height = toolbarHeight(); if (!suggestions.hidden) { var suggestionsRect = suggestions.getBoundingClientRect(); height = Math.max(height, Math.ceil(suggestionsRect.bottom + 8)); } return height; } function pageFrame() { var top = toolbarHeight(); top = Math.min(top, Math.max(0, browserViewport.height - 1)); return { x: 0, y: top, width: browserViewport.width, height: Math.max(1, Math.floor(browserViewport.height - top)), }; } function chromeFrame() { var height = chromeHeight(); return { x: 0, y: 0, width: browserViewport.width, height: Math.max(1, Math.min(height, browserViewport.height)), }; } function updateBrowserViewport(width, height) { var nextWidth = Math.max(1, Math.floor(width)); var nextHeight = Math.max(1, Math.floor(height)); if (browserViewport.width === nextWidth && browserViewport.height === nextHeight) return false; browserViewport = { width: nextWidth, height: nextHeight }; browserViewportRevision++; return true; } function mainWindowInfo(windows) { if (!Array.isArray(windows)) return null; for (var i = 0; i < windows.length; i++) { if (windows[i] && (windows[i].id === 1 || windows[i].label === "main")) return windows[i]; } return windows[0] || null; } async function syncBrowserViewport() { if (lastNativeResizeAt && Date.now() - lastNativeResizeAt < 1000) return false; var startedAtRevision = browserViewportRevision; try { var info = mainWindowInfo(await window.zero.windows.list()); if (!info) return false; if (browserViewportRevision !== startedAtRevision) return false; var nativeWidth = Math.max(1, Math.floor(info.width)); var nativeHeight = Math.max(1, Math.floor(info.height)); if (!nativeViewportInset) { nativeViewportInset = { width: nativeWidth - browserViewport.width, height: nativeHeight - browserViewport.height, }; } return updateBrowserViewport( nativeWidth - nativeViewportInset.width, nativeHeight - nativeViewportInset.height ); } catch (error) { console.error("Failed to sync browser viewport", error); return false; } } async function updateChromeOverlay() { var frame = chromeFrame(); await window.zero.webviews.setFrame({ label: "main", frame: frame }); try { await window.zero.webviews.setLayer({ label: "main", layer: CHROME_WEBVIEW_LAYER }); } catch (error) { console.warn("Main WebView layering is not supported on this backend", error); } } function updateHistoryButtons() { backButton.disabled = historyIndex <= 0; forwardButton.disabled = historyIndex < 0 || historyIndex >= navHistory.length - 1; } function remember(url) { if (navHistory[historyIndex] === url) return; navHistory = navHistory.slice(0, historyIndex + 1); navHistory.push(url); historyIndex = navHistory.length - 1; updateHistoryButtons(); } function updateAddressFromPage(url, options) { options = options || {}; if (!url || canonicalUrlKey(url) === canonicalUrlKey(currentUrl)) return; currentUrl = url; addressInput.value = url; updateSecurityIndicator(url); hideError(); trackVisited(url); if (options.record !== false) remember(url); } function trackVisited(url) { var key = canonicalUrlKey(url); if (!key) return; for (var i = 0; i < visitedUrls.length; i++) { if (canonicalUrlKey(visitedUrls[i]) === key) { visitedUrls.splice(i, 1); break; } } visitedUrls.unshift(url); if (visitedUrls.length > 200) visitedUrls.length = 200; } function hostFromUrl(url) { try { return new URL(url).hostname; } catch (_) { return url; } } // ── Suggestions ── function escapeHtml(str) { return str.replace(/&/g, "&").replace(//g, ">"); } function highlightMatch(text, query) { if (!query) return escapeHtml(text); var lower = text.toLowerCase(); var qLower = query.toLowerCase(); var idx = lower.indexOf(qLower); if (idx === -1) return escapeHtml(text); var before = text.slice(0, idx); var match = text.slice(idx, idx + query.length); var after = text.slice(idx + query.length); return escapeHtml(before) + "" + escapeHtml(match) + "" + escapeHtml(after); } function filterSuggestions(query) { if (!query) return []; var q = query.toLowerCase(); var results = []; var seen = Object.create(null); for (var i = 0; i < visitedUrls.length; i++) { var url = visitedUrls[i]; var key = canonicalUrlKey(url); var matches = url.toLowerCase().indexOf(q) !== -1 || key.toLowerCase().indexOf(q) !== -1; if (seen[key] || !matches) continue; seen[key] = true; results.push(url); if (results.length >= 8) break; } return results; } function renderSuggestions(query) { suggestFiltered = filterSuggestions(query); suggestActive = -1; if (suggestFiltered.length === 0) { suggestions.hidden = true; scheduleResize(); return; } var html = ""; for (var i = 0; i < suggestFiltered.length; i++) { var url = suggestFiltered[i]; var host = hostFromUrl(url); html += '