diff --git a/chrome.html b/chrome.html index d3b597a..5ca6baa 100644 --- a/chrome.html +++ b/chrome.html @@ -13,7 +13,12 @@ } @media (prefers-color-scheme: light) { :root { - --bg:#e9ecf1; --surface:#ffffff; --surface2:#f1f3f7; --active:#dbe1ec; + /* Chrome strip is a soft neutral grey — flat white was too bright next + to the browsed page, and pure #ffffff tabs disappeared into the + window title bar on light Windows themes. --bg tints the whole + top strip; --surface (inactive tab) sits a hair above it; --active + (foreground tab) pops a bit brighter so the current tab reads. */ + --bg:#e6e8ec; --surface:#f2f4f7; --surface2:#eaecf0; --active:#ffffff; --ink:#1a1f28; --mut:#3c4453; --dim:#697280; --faint:#98a1b0; --line:rgba(0,0,0,.18); --line2:rgba(0,0,0,.11); --hover:rgba(0,0,0,.06); /* Darker acid for light backgrounds — same hue family, ~5.5:1 @@ -293,8 +298,10 @@ padding: 6px 12px; border: 1px solid color-mix(in srgb, var(--acid) 20%, transparent); border-radius: 8px; cursor: pointer; background: rgb(from var(--acid) r g b / .08); font-size: 13px; } .logo:hover { background: rgb(from var(--acid) r g b / .16); } .logo .gear { font-size: 12px; opacity: .8; } - /* favorites bar — new-tab page only, single compact row */ - .bookmarks { display: flex; align-items: center; gap: 4px; padding: 3px 10px 5px; height: 26px; overflow: hidden; } + /* favorites bar — new-tab page only, single compact row. Chips are + tight so many fit in the row; the whole strip is 22px tall (down + from 26) so the chrome view gains a bit of vertical space. */ + .bookmarks { display: flex; align-items: center; gap: 3px; padding: 2px 10px 4px; height: 22px; overflow: hidden; } /* Ctrl+F find-in-page bar. Sits below the address bar (above tabs body), ~32px tall so it's part of the chrome view height like .tordisc / .bcnrbar. syncHeight in the tabs handler picks it up automatically. */ @@ -307,11 +314,14 @@ .findbar .findbtn { border: 1px solid var(--line); background: var(--surface2); color: var(--ink); border-radius: 6px; padding: 3px 8px; font-size: 13px; cursor: pointer; } .findbar .findbtn:hover { background: var(--surface); border-color: color-mix(in srgb, var(--acid) 35%, transparent); } - .bm { display: flex; align-items: center; gap: 6px; max-width: 180px; padding: 3px 9px; border-radius: 6px; - background: var(--surface2); border: 1px solid var(--hover); color: var(--mut); font-size: 12px; cursor: pointer; white-space: nowrap; } + .bm { display: flex; align-items: center; gap: 5px; max-width: 130px; padding: 2px 7px; border-radius: 5px; + background: var(--surface2); border: 1px solid var(--hover); color: var(--mut); font-size: 11px; cursor: grab; white-space: nowrap; } .bm:hover { background: var(--active); color: var(--ink); } - .bm .bmfav { width: 14px; height: 14px; flex: none; object-fit: contain; border-radius: 2px; } - .bm .bt { overflow: hidden; text-overflow: ellipsis; } + .bm.dragging { opacity: .45; cursor: grabbing; } + .bm.dropbefore { box-shadow: -2px 0 0 var(--acid); } + .bm.dropafter { box-shadow: 2px 0 0 var(--acid); } + .bm .bmfav { width: 12px; height: 12px; flex: none; object-fit: contain; border-radius: 2px; } + .bm .bt { overflow: hidden; text-overflow: ellipsis; max-width: 96px; } /* In-chrome prompt modal (window.prompt is disabled in Electron BrowserViews, so bookmark rename etc. use this instead). */ .promptmask { position: fixed; inset: 0; background: rgba(0,0,0,.45); z-index: 10000; @@ -344,15 +354,21 @@ @media (prefers-color-scheme: light) { .ctxmenu { background: #ffffff; border-color: rgba(0,0,0,.15); } .ctxmenu .mi:hover { background: rgba(0,0,0,.05); } - /* BCH dark #253A49 as a light-mode accent so the toolbar isn't - all-white-on-white. .logo (⛓ Theseus) becomes a dark chip; the - download / update pill picks it up so both stand out. */ - .logo { background: #253A49; color: #F8FDFF; border-color: #253A49; } - .logo:hover { background: #1a2c39; } - .logo .gear { color: #F8FDFF; opacity: .85; } - .upchip { background: #253A49; color: #F8FDFF; border-color: #253A49; } - .upchip .upcore:hover { background: #1a2c39; } - .upchip .updismiss { border-left-color: rgba(255,255,255,.20); color: #F8FDFF; } + /* The old light-mode chips used the BCH dark #253A49 as their fill. + At small font sizes on light Windows the white-on-dark-navy label + fell below legible, so both .logo (⛓ Theseus) and .tor switch to + a light-grey chip with dark ink — same footprint, readable text. + Update pill picks up the same treatment. */ + .logo { background: #eef1f5; color: #253A49; border-color: rgba(0,0,0,.14); } + .logo:hover { background: #e2e6ec; } + .logo .gear { color: #253A49; opacity: .85; } + .tor { background: #eef1f5; color: #253A49; border-color: rgba(0,0,0,.14); } + .tor:hover { background: #e2e6ec; } + .tor.connecting { background: #fbe9b5; color: #5a3f00; border-color: rgba(0,0,0,.14); } + .tor.on { background: #e6d9f5; color: #3f1f6b; border-color: rgba(0,0,0,.14); } + .upchip { background: #eef1f5; color: #253A49; border-color: rgba(0,0,0,.14); } + .upchip .upcore:hover { background: #e2e6ec; } + .upchip .updismiss { border-left-color: rgba(0,0,0,.14); color: #253A49; } } .tordisc { font-size: 11.5px; color: #d9c7f2; background: #2a1c40; border-top: 1px solid #6b3fa055; padding: 5px 14px; } .tordisc a { color: var(--acid); } @@ -793,7 +809,7 @@ const fav = b.favicon ? `` : ``; - return `
${fav}${(b.title||b.url).replace(/
`; + return `
${fav}${(b.title||b.url).replace(/
`; }).join("") : `No saved pages yet — click ☆ or right-click here to add one.`; box.querySelectorAll(".bm").forEach((el) => el.onclick = (e) => { @@ -801,6 +817,40 @@ if (e.target.dataset.x !== undefined) T.removeBookmark(bookmarks[i].url); else T.navigate(bookmarks[i].url); }); + // Drag-reorder chips — same shape as the tab strip. Drop side is chosen + // by whether the pointer is on the left/right half of the target chip. + // dragUrl is the identity we hand to bookmark-move; renderBookmarks + // fires again after the reorder, which rebinds these handlers cleanly. + let dragUrl = null; + box.querySelectorAll(".bm").forEach((row) => { + row.addEventListener("dragstart", (e) => { + dragUrl = row.dataset.url; + try { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", dragUrl); } catch {} + row.classList.add("dragging"); + }); + row.addEventListener("dragend", () => { + row.classList.remove("dragging"); + box.querySelectorAll(".bm").forEach((r) => r.classList.remove("dropbefore", "dropafter")); + dragUrl = null; + }); + row.addEventListener("dragover", (e) => { + if (!dragUrl || row.dataset.url === dragUrl) return; + e.preventDefault(); e.dataTransfer.dropEffect = "move"; + const r = row.getBoundingClientRect(); + const before = (e.clientX - r.left) < r.width / 2; + row.classList.toggle("dropbefore", before); + row.classList.toggle("dropafter", !before); + }); + row.addEventListener("dragleave", () => row.classList.remove("dropbefore", "dropafter")); + row.addEventListener("drop", (e) => { + e.preventDefault(); + const targetUrl = row.dataset.url; + if (!dragUrl || dragUrl === targetUrl) return; + const r = row.getBoundingClientRect(); + const before = (e.clientX - r.left) < r.width / 2; + T.moveBookmark && T.moveBookmark(dragUrl, targetUrl, before ? "before" : "after"); + }); + }); // Backfill: older bookmarks stored before favicon-support have no icon. // If we're currently on that URL and have a favicon in hand, patch it in // once so the next render draws it. diff --git a/main.js b/main.js index 71bba56..ed28e1a 100644 --- a/main.js +++ b/main.js @@ -1984,15 +1984,51 @@ function installDownloadTracker() { emitUpdateAvailable(); }); item.once("done", (_ev, state) => { - if (state === "completed") { - updateDownloadPath = item.getSavePath() || dst; - updateDownloadState = "ready"; - console.log(`[update] silent fetch complete: ${updateDownloadPath}`); - } else { + if (state !== "completed") { updateDownloadState = "failed"; console.warn(`[update] silent fetch ${state}`); + emitUpdateAvailable(); + return; } - emitUpdateAvailable(); + // NEVER mark "ready" without verifying the file hashes to what the + // manifest promised. Electron's DownloadItem has been observed to + // fire done/completed on truncated payloads (bad Content-Length, + // CDN cache truncation, mid-stream TLS reset the runtime swallowed), + // and 0.3.31's in-app updater then spawned a half-file as setup — + // NSIS integrity check failed silently and the browser was gone. + const savedPath = item.getSavePath() || dst; + const expected = String(updateAvailable && updateAvailable.setupHash || "").toLowerCase(); + if (!expected) { + updateDownloadState = "failed"; + console.warn(`[update] no manifest hash for ${savedPath} — refusing to arm install`); + try { fs.unlinkSync(savedPath); } catch {} + emitUpdateAvailable(); + return; + } + const crypto = require("node:crypto"); + const hash = crypto.createHash("sha256"); + const rs = fs.createReadStream(savedPath); + rs.on("data", (c) => hash.update(c)); + rs.once("error", (e) => { + updateDownloadState = "failed"; + console.warn(`[update] hash read failed: ${e.message}`); + try { fs.unlinkSync(savedPath); } catch {} + emitUpdateAvailable(); + }); + rs.once("end", () => { + const got = hash.digest("hex").toLowerCase(); + if (got !== expected) { + updateDownloadState = "failed"; + console.warn(`[update] SHA-256 mismatch: got ${got}, want ${expected} — refusing to arm install`); + try { fs.unlinkSync(savedPath); } catch {} + emitUpdateAvailable(); + return; + } + updateDownloadPath = savedPath; + updateDownloadState = "ready"; + console.log(`[update] silent fetch complete + verified: ${savedPath}`); + emitUpdateAvailable(); + }); }); return; } @@ -3725,6 +3761,22 @@ ipcMain.handle("bookmark-remove", (_e, url) => { saveBookmarks(); emitBookmarks(); return bookmarks; }); +// Reorder: pull `fromUrl` out of the list and reinsert it before or after +// `targetUrl`. Renderer picks the side by which half of the target chip the +// pointer is on, same convention the tab strip uses. A missing entry or a +// self-drop is a no-op, so noisy drag events don't corrupt the list. +ipcMain.handle("bookmark-move", (_e, fromUrl, targetUrl, place) => { + if (typeof fromUrl !== "string" || typeof targetUrl !== "string" || fromUrl === targetUrl) return bookmarks; + const from = bookmarks.findIndex((b) => b.url === fromUrl); + if (from < 0) return bookmarks; + const [moved] = bookmarks.splice(from, 1); + let to = bookmarks.findIndex((b) => b.url === targetUrl); + if (to < 0) { bookmarks.splice(from, 0, moved); return bookmarks; } + if (place === "after") to += 1; + bookmarks.splice(to, 0, moved); + saveBookmarks(); emitBookmarks(); + return bookmarks; +}); ipcMain.handle("settings-get", () => settings); ipcMain.handle("settings-set", (_e, key, val) => { if (key in SETTINGS_DEFAULTS) { settings[key] = val; saveSettings(); } diff --git a/preload.js b/preload.js index c7458e0..782bee8 100644 --- a/preload.js +++ b/preload.js @@ -48,6 +48,7 @@ contextBridge.exposeInMainWorld("theseus", { addBookmark: (bm) => ipcRenderer.invoke("bookmark-add", bm), updateBookmark: (url, patch) => ipcRenderer.invoke("bookmark-update", url, patch), removeBookmark: (url) => ipcRenderer.invoke("bookmark-remove", url), + moveBookmark: (fromUrl, targetUrl, place) => ipcRenderer.invoke("bookmark-move", fromUrl, targetUrl, place), onBookmarks: (cb) => ipcRenderer.on("bookmarks", (_e, d) => cb(d)), onNav: (cb) => ipcRenderer.on("nav", (_e, d) => cb(d)), onTor: (cb) => ipcRenderer.on("tor", (_e, d) => cb(d)),