From 1a584c8e50e69defe9cd8987a157120da23e4dbe Mon Sep 17 00:00:00 2001 From: Joe Lothan Date: Sun, 17 May 2026 23:50:12 -0400 Subject: [PATCH] basic frontend --- frontend/index.html | 165 +++++++++++++++++++++++++++++++ frontend/site.js | 230 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 395 insertions(+) create mode 100644 frontend/index.html create mode 100644 frontend/site.js diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..b49d604 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,165 @@ + + + + + + Every Tab + + + +
+
Loading tabs...
+ + + + + + diff --git a/frontend/site.js b/frontend/site.js new file mode 100644 index 0000000..f21ad53 --- /dev/null +++ b/frontend/site.js @@ -0,0 +1,230 @@ +// Seeded PRNG (mulberry32) +function mulberry32(seed) { + return function() { + seed |= 0; + seed = seed + 0x6D2B79F5 | 0; + let t = Math.imul(seed ^ seed >>> 15, 1 | seed); + t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; + return ((t ^ t >>> 14) >>> 0) / 4294967296; + }; +} + +const rng = mulberry32(Date.now()); +const loadedBundles = new Set(); +const container = document.getElementById("tab-container"); +const loadingEl = document.getElementById("loading"); + +// How many tabs fit in one row? +function tabsPerRow() { + const tabWidth = 160; // avg tab width in px (min 100, max 200) + return Math.ceil(window.innerWidth / tabWidth) + 4; // extra for marquee overflow +} + +// How many rows fill the viewport? +function rowsPerScreen() { + const rowHeight = 40; + return Math.ceil(window.innerHeight / rowHeight) + 4; // buffer +} + +// Pick a random bundle index we haven't loaded yet +function pickBundle() { + if (loadedBundles.size >= TOTAL_BUNDLES) return -1; + let idx; + do { + idx = Math.floor(rng() * TOTAL_BUNDLES); + } while (loadedBundles.has(idx)); + loadedBundles.add(idx); + return idx; +} + +// Fetch a bundle JSON +async function fetchBundle(idx) { + const padded = String(idx).padStart(4, "0"); + const resp = await fetch(`tabs/${padded}.json`); + if (!resp.ok) throw new Error(`Failed to fetch bundle ${padded}`); + const data = await resp.json(); + return data.entries; +} + +// Create a tab DOM element +function createTab(entry) { + const tab = document.createElement("div"); + tab.className = "tab"; + if (!entry.iframe_ok) { + tab.classList.add("tab-external"); + } + + if (entry.icon) { + const img = document.createElement("img"); + img.className = "tab-icon"; + img.src = `data:image/png;base64,${entry.icon}`; + img.alt = ""; + img.loading = "lazy"; + tab.appendChild(img); + } + + const title = document.createElement("span"); + title.className = "tab-title"; + title.textContent = entry.title || entry.host; + tab.appendChild(title); + + tab.title = entry.title || entry.host; + + // Click handler + tab.addEventListener("click", () => { + const url = `${entry.protocol || "https"}://${entry.host}`; + if (entry.iframe_ok) { + openInlineViewer(tab, entry, url); + } else { + window.open(url, "_blank", "noopener"); + } + }); + + return tab; +} + +// Create a row of tabs with marquee animation +function createRow(entries, rowIndex) { + const row = document.createElement("div"); + row.className = "tab-row"; + + const speed = 60 + (rng() * 40); // 60-100s per cycle + row.style.setProperty("--speed", `${speed}s`); + + // Random direction + const goLeft = rng() > 0.5; + row.classList.add(goLeft ? "scroll-left" : "scroll-right"); + + // Stagger start so rows aren't synchronized + row.style.animationDelay = `${-rng() * speed}s`; + + // Add tabs twice so the marquee loops seamlessly (translate -50% = one full set) + for (let copy = 0; copy < 2; copy++) { + for (const entry of entries) { + row.appendChild(createTab(entry)); + } + } + + return row; +} + +// Render entries into rows +function renderEntries(entries) { + const perRow = tabsPerRow(); + let rowIndex = container.children.length; + + for (let i = 0; i < entries.length; i += perRow) { + const rowEntries = entries.slice(i, i + perRow); + if (rowEntries.length < 3) continue; // skip tiny last rows + container.appendChild(createRow(rowEntries, rowIndex)); + rowIndex++; + } +} + +// Load enough bundles to fill the screen +async function loadMore() { + const neededRows = rowsPerScreen(); + const entriesNeeded = neededRows * tabsPerRow(); + + let entries = []; + while (entries.length < entriesNeeded) { + const idx = pickBundle(); + if (idx === -1) break; + try { + const bundleEntries = await fetchBundle(idx); + entries = entries.concat(bundleEntries); + } catch (e) { + console.error("Failed to load bundle:", e); + } + } + + if (entries.length > 0) { + renderEntries(entries); + loadingEl.style.display = "none"; + } +} + +// Infinite scroll +let loading = false; +window.addEventListener("scroll", async () => { + if (loading) return; + const scrollBottom = window.innerHeight + window.scrollY; + const docHeight = document.documentElement.scrollHeight; + + if (scrollBottom >= docHeight - 500) { + loading = true; + await loadMore(); + loading = false; + } +}); + +// Inline iframe viewer +let activeViewer = null; + +function openInlineViewer(tabEl, entry, url) { + // Close existing viewer + closeInlineViewer(); + + // Find the row this tab belongs to + const row = tabEl.closest(".tab-row"); + + // Build the viewer + const viewer = document.createElement("div"); + viewer.className = "iframe-viewer"; + + const header = document.createElement("div"); + header.className = "iframe-header"; + + if (entry.icon) { + const icon = document.createElement("img"); + icon.className = "tab-icon"; + icon.src = `data:image/png;base64,${entry.icon}`; + header.appendChild(icon); + } + + const title = document.createElement("span"); + title.className = "tab-title"; + title.textContent = entry.title || entry.host; + header.appendChild(title); + + const link = document.createElement("a"); + link.href = url; + link.target = "_blank"; + link.rel = "noopener"; + link.textContent = entry.host + " ↗"; + header.appendChild(link); + + const close = document.createElement("button"); + close.className = "iframe-close"; + close.textContent = "✕"; + close.addEventListener("click", closeInlineViewer); + header.appendChild(close); + + const iframe = document.createElement("iframe"); + iframe.sandbox = "allow-scripts allow-same-origin allow-forms"; + iframe.src = url; + + viewer.appendChild(header); + viewer.appendChild(iframe); + + // Insert after the row + row.after(viewer); + activeViewer = viewer; + + // Scroll so the viewer is visible + viewer.scrollIntoView({ behavior: "smooth", block: "start" }); +} + +function closeInlineViewer() { + if (activeViewer) { + activeViewer.remove(); + activeViewer = null; + } +} + +document.addEventListener("keydown", (e) => { + if (e.key === "Escape") closeInlineViewer(); +}); + +// Initial load +loadMore();