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
|
// Sidebar width in px. Adjusted by dragging the grip on the panel's left
|
||||||
// edge; persisted across launches. Clamped to [200, 800] on load.
|
// edge; persisted across launches. Clamped to [200, 800] on load.
|
||||||
sidebarWidth: 340,
|
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
|
// 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
|
// remaining space; `compact`/`medium` cap it so the extension dock has
|
||||||
// room to grow. The search box is fixed-width; hidden removes it.
|
// 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
|
// nothing until the user actively opens it, so the perf cost of an unused
|
||||||
// add-on is nil.
|
// add-on is nil.
|
||||||
let sidebar, sidebarVisible = false, sidebarActivePanelId = null;
|
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.
|
// 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
|
// The value below is the default; settings.sidebarWidth overrides it once
|
||||||
// loadSettings() runs and persists any drag adjustment made by the user.
|
// loadSettings() runs and persists any drag adjustment made by the user.
|
||||||
|
|
@ -2718,16 +2728,24 @@ function layout() {
|
||||||
const chromeH = fsTab ? 0 : CHROME_H;
|
const chromeH = fsTab ? 0 : CHROME_H;
|
||||||
chrome.setBounds({ x: 0, y: 0, width, height: chromeH });
|
chrome.setBounds({ x: 0, y: 0, width, height: chromeH });
|
||||||
const bodyH = Math.max(0, 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
|
// 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.
|
// shrink to fit alongside it. When hidden, tabs get the full width.
|
||||||
const sideW = sidebarVisible && !fsTab ? sidebarW : 0;
|
const sideW = sidebarVisible && !fsTab ? sidebarW : 0;
|
||||||
const tabW = Math.max(0, width - sideW);
|
const tabW = Math.max(0, width - leftW - sideW);
|
||||||
for (const t of tabs) t.view.setBounds({ x: 0, y: chromeH, width: tabW, height: bodyH });
|
for (const t of tabs) t.view.setBounds({ x: leftW, y: chromeH, width: tabW, height: bodyH });
|
||||||
if (sidebar) sidebar.setBounds({ x: tabW, y: chromeH, width: sideW, 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
|
// Approval overlay sits exactly over the tab area — the page underneath
|
||||||
// keeps running; only pointer input is intercepted.
|
// keeps running; only pointer input is intercepted.
|
||||||
if (approvalPop) approvalPop.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: 0, y: chromeH, width: tabW, height: bodyH });
|
if (jsDialogPop) jsDialogPop.setBounds({ x: leftW, y: chromeH, width: tabW, height: bodyH });
|
||||||
positionPopover();
|
positionPopover();
|
||||||
positionEnginePicker();
|
positionEnginePicker();
|
||||||
positionDownloads();
|
positionDownloads();
|
||||||
|
|
@ -3768,6 +3786,14 @@ function createWindow() {
|
||||||
win.contentView.addChildView(sidebar);
|
win.contentView.addChildView(sidebar);
|
||||||
styleScrollbars(sidebar.webContents);
|
styleScrollbars(sidebar.webContents);
|
||||||
sidebar.setVisible(false);
|
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
|
// Add-on approval overlay (approval-modal capability). Transparent view
|
||||||
// over the tab area, loaded once, shown per request.
|
// over the tab area, loaded once, shown per request.
|
||||||
approvalPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "approval-preload.js") } });
|
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;
|
const wc = t && !t.settings && !t.addonId && t.url ? t.view.webContents : null;
|
||||||
try { wc && wc.reload(); } catch {}
|
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
|
// Broadcast to every renderer that watches the live settings — the chrome
|
||||||
// (toolbar sizes, URL-bar language chip) and every open settings tab (so
|
// (toolbar sizes, URL-bar language chip) and every open settings tab (so
|
||||||
// the General Website-language row and the Privacy Anti-fingerprinting
|
// the General Website-language row and the Privacy Anti-fingerprinting
|
||||||
|
|
@ -6263,6 +6295,29 @@ ipcMain.handle("settings-set", (_e, key, val) => {
|
||||||
broadcastSettings();
|
broadcastSettings();
|
||||||
return settings;
|
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() {
|
function broadcastSettings() {
|
||||||
try { chrome?.webContents.send("settings-update", settings); } catch {}
|
try { chrome?.webContents.send("settings-update", settings); } catch {}
|
||||||
for (const t of tabs) {
|
for (const t of tabs) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "theseus-navigator",
|
"name": "theseus-navigator",
|
||||||
"version": "0.3.66",
|
"version": "0.3.67",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
||||||
"author": "Silent Mode",
|
"author": "Silent Mode",
|
||||||
|
|
@ -93,6 +93,8 @@
|
||||||
"webapps.js",
|
"webapps.js",
|
||||||
"js-dialog.html",
|
"js-dialog.html",
|
||||||
"js-dialog-preload.js",
|
"js-dialog-preload.js",
|
||||||
|
"quicklinks.html",
|
||||||
|
"quicklinks-preload.js",
|
||||||
"engine-icons/**/*",
|
"engine-icons/**/*",
|
||||||
"lib/**/*",
|
"lib/**/*",
|
||||||
"package.json",
|
"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>
|
<button id="updCheck" class="btn">Check for updates</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<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>
|
<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">
|
<div class="themeCards" id="theme" role="radiogroup" aria-label="Theme">
|
||||||
|
|
@ -1003,13 +1019,61 @@
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
const TOGGLES = ["restoreSession", "backgroundThrottle", "blockCamera", "blockMicrophone", "hideMediaDevices", "gpc",
|
const TOGGLES = ["restoreSession", "backgroundThrottle", "blockCamera", "blockMicrophone", "hideMediaDevices", "gpc",
|
||||||
"clearCookiesOnQuit", "clearCacheOnQuit", "clearStorageOnQuit", "clearHistoryOnQuit"];
|
"clearCookiesOnQuit", "clearCacheOnQuit", "clearStorageOnQuit", "clearHistoryOnQuit",
|
||||||
|
"quickLinksShow"];
|
||||||
C.get().then((s) => {
|
C.get().then((s) => {
|
||||||
for (const k of TOGGLES) {
|
for (const k of TOGGLES) {
|
||||||
const el = document.getElementById(k); if (!el) continue;
|
const el = document.getElementById(k); if (!el) continue;
|
||||||
el.checked = !!s[k];
|
el.checked = !!s[k];
|
||||||
el.addEventListener("change", () => C.set(k, el.checked));
|
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
|
// ---- website language (General page) — friendly wrapper over the
|
||||||
// same languageMode/languageValue setting the Anti-fingerprinting Language
|
// same languageMode/languageValue setting the Anti-fingerprinting Language
|
||||||
// row edits. "Auto" = languageMode="show" (follow OS); anything else
|
// row edits. "Auto" = languageMode="show" (follow OS); anything else
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue