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 @@ +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.
+Choose a theme for the browser. System follows your operating system's light/dark setting.