diff --git a/link-status-preload.js b/link-status-preload.js new file mode 100644 index 0000000..c19ecd6 --- /dev/null +++ b/link-status-preload.js @@ -0,0 +1,5 @@ +const { contextBridge, ipcRenderer } = require("electron"); +contextBridge.exposeInMainWorld("linkstatus", { + onUrl: (cb) => ipcRenderer.on("link-status-url", (_e, url) => cb(url)), + resize: (w, h) => ipcRenderer.invoke("link-status-resize", w, h), +}); diff --git a/link-status.html b/link-status.html new file mode 100644 index 0000000..9f82665 --- /dev/null +++ b/link-status.html @@ -0,0 +1,22 @@ + +
+ + + diff --git a/main.js b/main.js index 3765905..5e15af1 100644 --- a/main.js +++ b/main.js @@ -916,6 +916,12 @@ let apW = 520; let apH = 60; // site has matching credentials. let pwFillPop, pwfVisible = false, pwfPos = { x: 8, y: 90 }; const PWF_W = 280; let pwfH = 80; +// Link-hover status bar — small pill at the bottom-left of the window +// showing the href when the mouse hovers a link (Chrome / Firefox style). +// Hidden when hover leaves. Fed by webContents.update-target-url on every +// tab; the pill auto-sizes to its text. +let linkStatus, linkStatusVisible = false; +let linkStatusW = 100, linkStatusH = 22; // In-memory download list. Not persisted: closing the browser clears history // (the files are still on disk; only the list of "recent downloads" is dropped). const downloads = []; let nextDlId = 1; const dlItems = new Map(); // id -> DownloadItem @@ -951,6 +957,7 @@ function layout() { positionDownloads(); positionAddressPicker(); positionPwFill(); + if (linkStatusVisible) positionLinkStatus(); } function positionPopover() { if (!popover) return; @@ -1013,6 +1020,26 @@ function positionPwFill() { const x = Math.max(6, Math.min(pwfPos.x, width - PWF_W - 6)); pwFillPop.setBounds({ x, y: pwfPos.y, width: PWF_W, height: pwfH }); } +function positionLinkStatus() { + if (!linkStatus || !win) return; + const { width, height } = win.getContentBounds(); + const w = Math.min(Math.max(120, linkStatusW), Math.max(200, width - 20)); + const h = Math.max(20, linkStatusH); + linkStatus.setBounds({ x: 0, y: Math.max(0, height - h), width: w, height: h }); +} +function showLinkStatus(url) { + if (!linkStatus) return; + const s = String(url || ""); + if (!s) { + if (linkStatusVisible) { linkStatus.setVisible(false); linkStatusVisible = false; } + return; + } + positionLinkStatus(); + // Raise the pill above any tab view that was added after it. + try { win.contentView.removeChildView(linkStatus); win.contentView.addChildView(linkStatus); } catch {} + linkStatus.setVisible(true); linkStatusVisible = true; + try { linkStatus.webContents.send("link-status-url", s); } catch {} +} function showPwFill(show, matches) { if (!pwFillPop) return; if (show) { @@ -1134,6 +1161,7 @@ function setActive(id) { activeId = id; if (popVisible) showPopover(false); // don't carry a stale popover across tabs if (epVisible) showEnginePicker(false); + if (linkStatusVisible) showLinkStatus(""); // clear any lingering hover pill for (const t of tabs) t.view.setVisible(t.id === id); const t = activeTab(); if (t?.prov) pushNav(t.prov); @@ -1197,6 +1225,9 @@ function createTab(initial, opts = {}) { wc.on("did-navigate-in-page", () => { refreshTabUrl(tab); emitTabs(); historyAdd(tab.url, tab.title); }); wc.on("did-start-loading", () => setLoading(tab, true)); wc.on("did-stop-loading", () => setLoading(tab, false)); + // Firefox / Chrome-style bottom-left link preview: fires with the href + // when the pointer enters/leaves an anchor. Empty string = no hover. + wc.on("update-target-url", (_e, url) => { if (tab.id === activeId) showLinkStatus(url); }); wc.on("will-navigate", (e, u) => { try { // Skip our own programmatic loads. fallbackToWeb calls loadURL("https://