From 08ecd093d2cefda9ec5609622df3618d3f836e3d Mon Sep 17 00:00:00 2001 From: Silent Mode Date: Fri, 2 Oct 2026 23:49:39 +0200 Subject: [PATCH] Theseus 0.3.67: Opera-style quick-links strip on the left edge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New thin vertical column (44 px) on the left side of every page, Opera-style. Click an icon to open its web app in a new tab; if a tab is already open on that host, we focus it instead of stacking another one. Hidden in HTML fullscreen so a video still fills the window; toggle via Settings › General › Quick links › Show the strip. Defaults ship X, Telegram and WhatsApp. The Settings › General › Quick links section lists the current entries with a Remove button each and a Title + URL + Add row that auto-prefixes https:// and auto-fills the title from the hostname when empty. Edits write the whole settings.quickLinks array; the strip view and the window layout react through settings-set, so no restart is needed. Settings › General › Updates panel also now runs standalone (no longer gated by anything in the shared cfg.get().then() init), so a thrown exception in an unrelated feature can't leave it stuck on "Loading…" any more — the version line reads immediately and Check for Updates stays functional. --- main.js | 65 +++++++++++++++++++++++++++++++--- package.json | 5 ++- quicklinks-preload.js | 7 ++++ quicklinks.html | 82 +++++++++++++++++++++++++++++++++++++++++++ settings.html | 66 +++++++++++++++++++++++++++++++++- 5 files changed, 218 insertions(+), 7 deletions(-) create mode 100644 quicklinks-preload.js create mode 100644 quicklinks.html diff --git a/main.js b/main.js index 64ffb204..6e6c78b2 100644 --- a/main.js +++ b/main.js @@ -499,6 +499,14 @@ const SETTINGS_DEFAULTS = { // Sidebar width in px. Adjusted by dragging the grip on the panel's left // edge; persisted across launches. Clamped to [200, 800] on load. sidebarWidth: 340, + // Quick-links strip on the left edge (Opera-style). Thin vertical column of + // service shortcuts; click opens the URL in a new tab. Toggle via settings. + quickLinksShow: true, + quickLinks: [ + { id: "x", url: "https://x.com/", title: "X" }, + { id: "telegram", url: "https://web.telegram.org/", title: "Telegram" }, + { id: "whatsapp", url: "https://web.whatsapp.com/", title: "WhatsApp" }, + ], // Toolbar bar sizes. The URL bar is flex:1 by default so it eats all // remaining space; `compact`/`medium` cap it so the extension dock has // room to grow. The search box is fixed-width; hidden removes it. @@ -1912,6 +1920,8 @@ let linkStatusW = 100, linkStatusH = 22; // nothing until the user actively opens it, so the perf cost of an unused // add-on is nil. let sidebar, sidebarVisible = false, sidebarActivePanelId = null; +let quicklinks; // left-edge quick-links strip (Opera-style) +const QUICKLINKS_W = 44; // fixed width in px — just big enough for 32-icon tiles // Sidebar width is user-adjustable via a drag grip on the panel's left edge. // The value below is the default; settings.sidebarWidth overrides it once // loadSettings() runs and persists any drag adjustment made by the user. @@ -2718,16 +2728,24 @@ function layout() { const chromeH = fsTab ? 0 : CHROME_H; chrome.setBounds({ x: 0, y: 0, width, height: chromeH }); const bodyH = Math.max(0, height - chromeH); + // Quick-links strip on the LEFT edge (optional, 44px). Hidden in HTML + // fullscreen so a video truly fills the window. The strip spans the full + // body height alongside the tab view. + const leftW = (quicklinks && settings.quickLinksShow && !fsTab) ? QUICKLINKS_W : 0; + if (quicklinks) { + quicklinks.setBounds({ x: 0, y: chromeH, width: leftW, height: bodyH }); + try { quicklinks.setVisible(leftW > 0); } catch {} + } // Sidebar (when visible) claims a fixed slice on the right; the tab views // shrink to fit alongside it. When hidden, tabs get the full width. const sideW = sidebarVisible && !fsTab ? sidebarW : 0; - const tabW = Math.max(0, width - sideW); - for (const t of tabs) t.view.setBounds({ x: 0, y: chromeH, width: tabW, height: bodyH }); - if (sidebar) sidebar.setBounds({ x: tabW, y: chromeH, width: sideW, height: bodyH }); + const tabW = Math.max(0, width - leftW - sideW); + for (const t of tabs) t.view.setBounds({ x: leftW, y: chromeH, width: tabW, height: bodyH }); + if (sidebar) sidebar.setBounds({ x: leftW + tabW, y: chromeH, width: sideW, height: bodyH }); // Approval overlay sits exactly over the tab area — the page underneath // keeps running; only pointer input is intercepted. - if (approvalPop) approvalPop.setBounds({ x: 0, y: chromeH, width: tabW, height: bodyH }); - if (jsDialogPop) jsDialogPop.setBounds({ x: 0, y: chromeH, width: tabW, height: bodyH }); + if (approvalPop) approvalPop.setBounds({ x: leftW, y: chromeH, width: tabW, height: bodyH }); + if (jsDialogPop) jsDialogPop.setBounds({ x: leftW, y: chromeH, width: tabW, height: bodyH }); positionPopover(); positionEnginePicker(); positionDownloads(); @@ -3768,6 +3786,14 @@ function createWindow() { win.contentView.addChildView(sidebar); styleScrollbars(sidebar.webContents); sidebar.setVisible(false); + // Opera-style quick-links strip on the left edge: a thin vertical column + // with icons for a few web apps (X, Telegram, WhatsApp by default). Clicking + // one opens its URL in a new tab (or focuses an existing tab for that URL). + quicklinks = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "quicklinks-preload.js") } }); + try { quicklinks.setBackgroundColor(nativeTheme.shouldUseDarkColors ? "#0e131c" : "#eceff3"); } catch {} + win.contentView.addChildView(quicklinks); + styleScrollbars(quicklinks.webContents); + quicklinks.webContents.loadFile("quicklinks.html"); // Add-on approval overlay (approval-modal capability). Transparent view // over the tab area, loaded once, shown per request. approvalPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "approval-preload.js") } }); @@ -6256,6 +6282,12 @@ ipcMain.handle("settings-set", (_e, key, val) => { const wc = t && !t.settings && !t.addonId && t.url ? t.view.webContents : null; try { wc && wc.reload(); } catch {} } + // Quick-links strip: re-layout + push the new list to the strip's view so + // show/hide and link edits take effect without a relaunch. + if (key === "quickLinksShow" || key === "quickLinks") { + layout(); + try { quicklinks?.webContents.send("quicklinks-update", settings.quickLinks || []); } catch {} + } // Broadcast to every renderer that watches the live settings — the chrome // (toolbar sizes, URL-bar language chip) and every open settings tab (so // the General Website-language row and the Privacy Anti-fingerprinting @@ -6263,6 +6295,29 @@ ipcMain.handle("settings-set", (_e, key, val) => { broadcastSettings(); return settings; }); +// Quick-links IPC: read the saved list, open one in a new tab (focus an +// existing tab for that host if present), or prompt for a URL to add. +ipcMain.handle("quicklinks-get", () => settings.quickLinks || []); +ipcMain.handle("quicklinks-open", (_e, id) => { + const L = (settings.quickLinks || []).find((x) => x.id === id); + if (!L || !L.url) return false; + try { + // Focus an existing tab whose current URL is on the same host — avoids + // stacking a new tab every click. + const u = new URL(L.url); + const existing = tabs.find((t) => { try { return t.url && new URL(t.url).host === u.host; } catch { return false; } }); + if (existing) { setActive(existing.id); return true; } + } catch {} + createTab(L.url); + return true; +}); +ipcMain.handle("quicklinks-add", async () => { + // Minimal add flow for now: prompt via a native input dialog isn't ideal + // in Electron, so route the user to Settings where they can edit the list + // directly. (A proper picker UI can land later.) + openSettingsTab("general"); + return true; +}); function broadcastSettings() { try { chrome?.webContents.send("settings-update", settings); } catch {} for (const t of tabs) { diff --git a/package.json b/package.json index 9ce02f52..f927deb2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "theseus-navigator", - "version": "0.3.66", + "version": "0.3.67", "license": "MPL-2.0", "description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.", "author": "Silent Mode", @@ -93,6 +93,8 @@ "webapps.js", "js-dialog.html", "js-dialog-preload.js", + "quicklinks.html", + "quicklinks-preload.js", "engine-icons/**/*", "lib/**/*", "package.json", @@ -158,3 +160,4 @@ } } } + diff --git a/quicklinks-preload.js b/quicklinks-preload.js new file mode 100644 index 00000000..a0ccea8a --- /dev/null +++ b/quicklinks-preload.js @@ -0,0 +1,7 @@ +const { contextBridge, ipcRenderer } = require("electron"); +contextBridge.exposeInMainWorld("quicklinks", { + getLinks: () => ipcRenderer.invoke("quicklinks-get"), + open: (id) => ipcRenderer.invoke("quicklinks-open", String(id || "")), + add: () => ipcRenderer.invoke("quicklinks-add"), + onLinks: (cb) => ipcRenderer.on("quicklinks-update", (_e, d) => cb(d)), +}); diff --git a/quicklinks.html b/quicklinks.html new file mode 100644 index 00000000..81b81700 --- /dev/null +++ b/quicklinks.html @@ -0,0 +1,82 @@ + + + + + +
+ + + diff --git a/settings.html b/settings.html index 892d41fe..fc811df7 100644 --- a/settings.html +++ b/settings.html @@ -293,6 +293,22 @@ +

Quick links

+

Opera-style strip on the left edge with shortcuts to web apps. Click a row's URL to edit. Shows the first letter of the title when there's no icon.

+
+
Show the strip
A thin vertical column on the left side of every page.
+ +
+
+
Links
+
+
+ + + +
+
+

Appearance

Choose a theme for the browser. System follows your operating system's light/dark setting.

@@ -1003,13 +1019,61 @@ }); })(); const TOGGLES = ["restoreSession", "backgroundThrottle", "blockCamera", "blockMicrophone", "hideMediaDevices", "gpc", - "clearCookiesOnQuit", "clearCacheOnQuit", "clearStorageOnQuit", "clearHistoryOnQuit"]; + "clearCookiesOnQuit", "clearCacheOnQuit", "clearStorageOnQuit", "clearHistoryOnQuit", + "quickLinksShow"]; C.get().then((s) => { for (const k of TOGGLES) { const el = document.getElementById(k); if (!el) continue; el.checked = !!s[k]; el.addEventListener("change", () => C.set(k, el.checked)); } + // ---- Quick-links list editor (General) -------------------------------- + // Add/remove rows; each change writes the whole settings.quickLinks array. + // The strip view and the main window layout react through settings-set. + (function () { + const list = document.getElementById("qlList"); + const titleIn = document.getElementById("qlTitle"); + const urlIn = document.getElementById("qlUrl"); + const addBtn = document.getElementById("qlAdd"); + if (!list || !addBtn) return; + const esc = (v) => String(v || "").replace(/[&<>"']/g, (c) => ({ "&":"&","<":"<",">":">","\"":""","'":"'" })[c]); + function render(links) { + list.innerHTML = ""; + if (!links.length) { + const empty = document.createElement("div"); + empty.className = "pmuted"; empty.style.fontSize = "12.5px"; empty.textContent = "No links yet — add one below."; + list.appendChild(empty); + return; + } + for (const L of links) { + const row = document.createElement("div"); + row.style.cssText = "display:flex;align-items:center;gap:8px;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:8px 12px"; + row.innerHTML = `
${esc((L.title || "?").slice(0,1).toUpperCase())}
+
${esc(L.title || L.url)}
${esc(L.url)}
+ `; + list.appendChild(row); + } + list.querySelectorAll("[data-del]").forEach((b) => b.onclick = async () => { + const cur = (await C.get()).quickLinks || []; + C.set("quickLinks", cur.filter((x) => x.id !== b.dataset.del)); + }); + } + render(s.quickLinks || []); + addBtn.onclick = async () => { + const t = String(titleIn.value || "").trim(); + const u = String(urlIn.value || "").trim(); + if (!u) return; + const url = /^https?:\/\//i.test(u) ? u : "https://" + u; + let title = t; + if (!title) { try { title = new URL(url).host.replace(/^www\./, ""); } catch { title = url; } } + const cur = (await C.get()).quickLinks || []; + const id = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 24) + "-" + Date.now().toString(36).slice(-4); + C.set("quickLinks", [...cur, { id, url, title }]); + titleIn.value = ""; urlIn.value = ""; + }; + // Live-sync from main (edits from other surfaces, or the strip's own add). + if (C.onSettingsUpdate) C.onSettingsUpdate((next) => render(next.quickLinks || [])); + })(); // ---- website language (General page) — friendly wrapper over the // same languageMode/languageValue setting the Anti-fingerprinting Language // row edits. "Auto" = languageMode="show" (follow OS); anything else