Theseus 0.3.67: Opera-style quick-links strip on the left edge
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.
This commit is contained in:
parent
cb4d900c1a
commit
08ecd093d2
5 changed files with 218 additions and 7 deletions
65
main.js
65
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) {
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
7
quicklinks-preload.js
Normal file
7
quicklinks-preload.js
Normal file
|
|
@ -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)),
|
||||
});
|
||||
82
quicklinks.html
Normal file
82
quicklinks.html
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head><meta charset="utf-8">
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--bg:#0e131c; --ink:#e7eaf1; --mut:#8b98a9; --line2:rgba(255,255,255,.13);
|
||||
--acid:#d6ff3d;
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root { --bg:#eceff3; --ink:#1a1f28; --mut:#5c6676; --line2:rgba(0,0,0,.10); --acid:#0AC18E; }
|
||||
}
|
||||
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
|
||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; overflow: hidden;
|
||||
user-select: none; }
|
||||
.strip { display: flex; flex-direction: column; align-items: center;
|
||||
padding: 10px 0; gap: 4px; height: 100%; box-sizing: border-box;
|
||||
border-right: 1px solid var(--line2); }
|
||||
.icon { width: 36px; height: 36px; border-radius: 8px; display: grid; place-items: center;
|
||||
cursor: pointer; background: transparent; border: none; padding: 0; color: var(--mut);
|
||||
position: relative; }
|
||||
.icon:hover { background: var(--line2); color: var(--ink); }
|
||||
.icon:active { transform: scale(0.94); }
|
||||
.icon svg { width: 20px; height: 20px; display: block; }
|
||||
.icon img { width: 22px; height: 22px; display: block; border-radius: 4px; }
|
||||
.icon .tt { position: absolute; left: 44px; top: 50%; transform: translateY(-50%);
|
||||
background: var(--bg); color: var(--ink); padding: 4px 8px; border-radius: 6px;
|
||||
border: 1px solid var(--line2); white-space: nowrap; font-size: 12px;
|
||||
pointer-events: none; opacity: 0; transition: opacity .1s; z-index: 10; }
|
||||
.icon:hover .tt { opacity: 1; transition-delay: .4s; }
|
||||
.sep { width: 24px; height: 1px; background: var(--line2); margin: 6px 0; flex: none; }
|
||||
.spacer { flex: 1 1 auto; }
|
||||
/* Add button — small plus to add a custom link. Prompts for URL via main. */
|
||||
.add { color: var(--mut); font: 500 20px/1 system-ui, sans-serif; }
|
||||
.add:hover { color: var(--acid); }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="strip" id="strip"></div>
|
||||
<script>
|
||||
const Q = window.quicklinks;
|
||||
const strip = document.getElementById("strip");
|
||||
// The links come from main (saved in settings.quickLinks). Each entry is
|
||||
// { id, url, title, icon } where icon is either a letter, an emoji, or a
|
||||
// data: URL for a bundled brand SVG.
|
||||
function render(links) {
|
||||
strip.innerHTML = "";
|
||||
for (const L of (links || [])) {
|
||||
const b = document.createElement("button");
|
||||
b.className = "icon";
|
||||
b.title = L.title || L.url;
|
||||
// Icon: a data URL (img) or a short text badge.
|
||||
if (L.icon && /^(data:|https?:|file:)/.test(L.icon)) {
|
||||
const img = document.createElement("img");
|
||||
img.src = L.icon; img.alt = "";
|
||||
img.onerror = () => { b.textContent = (L.title || "?").slice(0, 1).toUpperCase(); };
|
||||
b.appendChild(img);
|
||||
} else if (L.svg) {
|
||||
b.innerHTML = L.svg;
|
||||
} else {
|
||||
b.textContent = (L.title || "?").slice(0, 1).toUpperCase();
|
||||
}
|
||||
const tt = document.createElement("span");
|
||||
tt.className = "tt"; tt.textContent = L.title || L.url;
|
||||
b.appendChild(tt);
|
||||
b.onclick = () => Q && Q.open(L.id);
|
||||
strip.appendChild(b);
|
||||
}
|
||||
// Spacer pushes the + to the bottom.
|
||||
const sp = document.createElement("div"); sp.className = "spacer";
|
||||
strip.appendChild(sp);
|
||||
const add = document.createElement("button");
|
||||
add.className = "icon add"; add.textContent = "+"; add.title = "Add link";
|
||||
const addTt = document.createElement("span"); addTt.className = "tt"; addTt.textContent = "Add link";
|
||||
add.appendChild(addTt);
|
||||
add.onclick = () => Q && Q.add();
|
||||
strip.appendChild(add);
|
||||
}
|
||||
Q && Q.getLinks && Q.getLinks().then(render).catch(() => render([]));
|
||||
Q && Q.onLinks && Q.onLinks(render);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -293,6 +293,22 @@
|
|||
<button id="updCheck" class="btn">Check for updates</button>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="sub">Quick links</h2>
|
||||
<p class="subd">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.</p>
|
||||
<div class="row">
|
||||
<div class="txt"><div class="t">Show the strip</div><div class="d">A thin vertical column on the left side of every page.</div></div>
|
||||
<label class="sw"><input type="checkbox" id="quickLinksShow"><span class="track"><span class="knob"></span></span></label>
|
||||
</div>
|
||||
<div class="row" style="flex-direction:column;align-items:stretch;gap:8px">
|
||||
<div class="txt"><div class="t">Links</div></div>
|
||||
<div id="qlList" style="display:flex;flex-direction:column;gap:6px"></div>
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||||
<input id="qlTitle" type="text" placeholder="Title (e.g. Discord)" style="flex:1;min-width:120px;background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:13px">
|
||||
<input id="qlUrl" type="text" placeholder="https://discord.com/app" style="flex:2;min-width:200px;background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:13px">
|
||||
<button id="qlAdd" class="btn">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="sub">Appearance</h2>
|
||||
<p class="subd">Choose a theme for the browser. <b>System</b> follows your operating system's light/dark setting.</p>
|
||||
<div class="themeCards" id="theme" role="radiogroup" aria-label="Theme">
|
||||
|
|
@ -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 = `<div style="width:28px;height:28px;border-radius:6px;background:rgba(255,255,255,.06);display:grid;place-items:center;font-weight:600;color:var(--acid-text);flex:none">${esc((L.title || "?").slice(0,1).toUpperCase())}</div>
|
||||
<div style="flex:1;min-width:0"><div style="font-weight:600">${esc(L.title || L.url)}</div><div style="font-size:12px;color:var(--mut);word-break:break-all">${esc(L.url)}</div></div>
|
||||
<button class="btn ghost" data-del="${esc(L.id)}">Remove</button>`;
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue