Collision modes (BCNR/ICANN) + root TLD cert + VPS electrum-source indexer
Theseus soft-mode UX: 'Open with...' modal on collision, per-name/per-TLD overrides, live per-tab switcher in the site-info popover, and a Naming section in Settings for policy + reset. Backed by an on-chain root TLD certificate (tlds.bch) that resolver-web.js discovers via fetchBcnrTlds()/isBcnrNativeTld(). Companion pieces: - Argus/src/indexer/ELECTRUM-SOURCE-README.md — the featherweight VPS variant (no BCHN node, no Fulcrum) now live as bns-indexer.service. - Argus/DESIGN-root-tld-cert.md — clarified: NOT a governance workflow, just ordinary key management (single wallet MVP -> 2-of-3 multisig). List gates registration / surgical NRPT / soft-mode classifier — never resolution. - ROADMAP-IDEAS.md — recorded SiaGit/GitHub.sia + user-friendly Sia UI ideas. Full spec: Argus/DESIGN-collision-modes.md (already tracked).
This commit is contained in:
parent
14fabac7fc
commit
d0db4ac495
9 changed files with 568 additions and 54 deletions
7
collision-preload.js
Normal file
7
collision-preload.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// Minimal, sandboxed IPC bridge for the "Open with…" collision prompt window.
|
||||
// Only exposes the exact call the page needs: send the user's choice back to
|
||||
// the main process (which awaits it via ipcMain.once).
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
contextBridge.exposeInMainWorld("collisionApi", {
|
||||
choose(host, payload) { ipcRenderer.send(`collision-choose:${host}`, payload); },
|
||||
});
|
||||
94
collision.html
Normal file
94
collision.html
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Open with…</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark light;
|
||||
--bg:#0f1116; --fg:#e5e7eb; --muted:#9ca3af; --border:#2a2f3a;
|
||||
--btn:#1f2430; --btn-hover:#2a3040; --accent:#f5c518; --icann:#4c9eff;
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root { --bg:#f7f7f8; --fg:#111827; --muted:#6b7280; --border:#e5e7eb; --btn:#fff; --btn-hover:#f0f2f5; }
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin:0; height:100%; }
|
||||
body { background:var(--bg); color:var(--fg); font:14px/1.4 system-ui,-apple-system,Segoe UI,Roboto,sans-serif; display:flex; flex-direction:column; padding:16px 18px; }
|
||||
h1 { margin:0 0 4px; font-size:15px; font-weight:600; }
|
||||
.sub { color:var(--muted); font-size:12.5px; margin-bottom:14px; }
|
||||
.host { color:var(--fg); font-weight:600; }
|
||||
.choices { display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-bottom:12px; }
|
||||
.choice { border:1px solid var(--border); border-radius:8px; padding:12px; cursor:pointer; background:var(--btn); text-align:left; color:var(--fg); }
|
||||
.choice:hover { background:var(--btn-hover); }
|
||||
.choice .icon { font-size:20px; }
|
||||
.choice .title { font-weight:600; margin:6px 0 2px; }
|
||||
.choice .desc { font-size:11.5px; color:var(--muted); }
|
||||
.choice.bcnr .icon, .choice.bcnr .title { color:var(--accent); }
|
||||
.choice.icann .icon, .choice.icann .title { color:var(--icann); }
|
||||
fieldset { border:0; padding:0; margin:0 0 10px; }
|
||||
fieldset .row { display:flex; align-items:center; gap:8px; padding:4px 0; font-size:12.5px; color:var(--muted); }
|
||||
fieldset .row input { accent-color:var(--accent); }
|
||||
.actions { display:flex; justify-content:flex-end; gap:8px; margin-top:auto; }
|
||||
button.cancel { background:transparent; color:var(--muted); border:0; padding:8px 12px; cursor:pointer; }
|
||||
button.cancel:hover { color:var(--fg); }
|
||||
kbd { border:1px solid var(--border); border-radius:3px; padding:0 4px; font:11px monospace; color:var(--muted); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1><span class="host" id="host"></span> exists in two registries</h1>
|
||||
<div class="sub">Choose which one to open. You can change this later in Settings.</div>
|
||||
|
||||
<div class="choices">
|
||||
<button class="choice bcnr" data-choice="bcnr" title="Open the on-chain name">
|
||||
<div class="icon">⛓</div>
|
||||
<div class="title">Bitcoin Cash Name Registry</div>
|
||||
<div class="desc">On-chain registration (BCNR). Owned by the holder of a name certificate on the Bitcoin Cash Blockchain.</div>
|
||||
</button>
|
||||
<button class="choice icann" data-choice="icann" title="Open the ICANN-served address">
|
||||
<div class="icon">🌐</div>
|
||||
<div class="title">ICANN / IANA</div>
|
||||
<div class="desc">DNS service coordinated by the ICANN. The registry served by the incumbent root.</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<div class="row"><input type="radio" name="remember" id="rem-no" value="no" checked><label for="rem-no">Ask again</label></div>
|
||||
<div class="row"><input type="radio" name="remember" id="rem-name" value="name"><label for="rem-name">Always use this choice for <span class="host" id="host2"></span></label></div>
|
||||
<div class="row"><input type="radio" name="remember" id="rem-tld" value="tld"><label for="rem-tld">Always use this choice for all <span id="tld2"></span> names</label></div>
|
||||
</fieldset>
|
||||
|
||||
<div class="actions">
|
||||
<button class="cancel" id="cancel">Cancel <kbd>Esc</kbd></button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const qs = new URLSearchParams(location.search);
|
||||
const host = qs.get("host") || ""; const tld = qs.get("tld") || "";
|
||||
document.getElementById("host").textContent = host;
|
||||
document.getElementById("host2").textContent = host;
|
||||
document.getElementById("tld").textContent = "." + tld;
|
||||
document.getElementById("tld2").textContent = "." + tld;
|
||||
document.title = "Open " + host + " with…";
|
||||
|
||||
function remember() {
|
||||
const r = document.querySelector('input[name="remember"]:checked');
|
||||
return r ? r.value : "no";
|
||||
}
|
||||
for (const btn of document.querySelectorAll(".choice")) {
|
||||
btn.addEventListener("click", () => {
|
||||
window.collisionApi.choose(host, { choice: btn.dataset.choice, remember: remember() });
|
||||
});
|
||||
}
|
||||
document.getElementById("cancel").addEventListener("click", () => {
|
||||
window.collisionApi.choose(host, { choice: "cancel", remember: "no" });
|
||||
});
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") window.collisionApi.choose(host, { choice: "cancel", remember: "no" });
|
||||
if (e.key === "1") window.collisionApi.choose(host, { choice: "bcnr", remember: remember() });
|
||||
if (e.key === "2") window.collisionApi.choose(host, { choice: "icann", remember: remember() });
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
214
main.js
214
main.js
|
|
@ -4,7 +4,7 @@
|
|||
// h, Sia s3, direct ip, redirect u). Tabs, nav controls, a search box, a home
|
||||
// page, and optional Tor onion routing. No system daemon; the app is the trust
|
||||
// boundary.
|
||||
const { app, BrowserWindow, WebContentsView, ipcMain, protocol, session, Menu, clipboard, nativeTheme } = require("electron");
|
||||
const { app, BrowserWindow, WebContentsView, ipcMain, protocol, session, Menu, clipboard, nativeTheme, shell } = require("electron");
|
||||
const path = require("path");
|
||||
const http = require("http");
|
||||
const https = require("https");
|
||||
|
|
@ -21,27 +21,29 @@ const RES_DIR = app.isPackaged ? process.resourcesPath : __dirname;
|
|||
const RESOLVER = app.isPackaged
|
||||
? path.join(RES_DIR, "resolver-web.mjs")
|
||||
: path.join(__dirname, "..", "Argus", "src", "lib", "resolver-web.js");
|
||||
// Built-in search engines. Users can also add their own (settings.customEngines,
|
||||
// Built-in engines. Users can also add their own (settings.customEngines,
|
||||
// each { id, name, url } where the url contains "%s" for the query).
|
||||
// Catalog of built-in engines (users pick which to enable + can add their own).
|
||||
// fav = the domain to load a real favicon from; sym = emoji fallback.
|
||||
// kind = "search" (traditional search engines) or "llm" (AI answer engines).
|
||||
// The picker + settings render each kind in its own section; kind stays a
|
||||
// display-only grouping (URL routing is the same for both).
|
||||
const SEARCH_ENGINES = {
|
||||
duckduckgo: { name: "DuckDuckGo", sym: "🦆", fav: "duckduckgo.com", url: (q) => "https://duckduckgo.com/?q=" + encodeURIComponent(q) },
|
||||
google: { name: "Google", sym: "🔵", fav: "www.google.com", url: (q) => "https://www.google.com/search?q=" + encodeURIComponent(q) },
|
||||
brave: { name: "Brave", sym: "🦁", fav: "search.brave.com", url: (q) => "https://search.brave.com/search?q=" + encodeURIComponent(q) },
|
||||
bing: { name: "Bing", sym: "🔎", fav: "www.bing.com", url: (q) => "https://www.bing.com/search?q=" + encodeURIComponent(q) },
|
||||
startpage: { name: "Startpage", sym: "🛡️", fav: "www.startpage.com", url: (q) => "https://www.startpage.com/sp/search?query=" + encodeURIComponent(q) },
|
||||
yandex: { name: "Yandex", sym: "🔴", fav: "yandex.com", url: (q) => "https://yandex.com/search/?text=" + encodeURIComponent(q) },
|
||||
ecosia: { name: "Ecosia", sym: "🌱", fav: "www.ecosia.org", url: (q) => "https://www.ecosia.org/search?q=" + encodeURIComponent(q) },
|
||||
mojeek: { name: "Mojeek", sym: "🧭", fav: "www.mojeek.com", url: (q) => "https://www.mojeek.com/search?q=" + encodeURIComponent(q) },
|
||||
searxng: { name: "SearXNG", sym: "🧩", fav: "searx.be", url: (q) => "https://searx.be/search?q=" + encodeURIComponent(q) },
|
||||
wikipedia: { name: "Wikipedia", sym: "📖", fav: "en.wikipedia.org", url: (q) => "https://en.wikipedia.org/wiki/Special:Search?search=" + encodeURIComponent(q) },
|
||||
perplexity: { name: "Perplexity", sym: "🧠", fav: "www.perplexity.ai", url: (q) => "https://www.perplexity.ai/search?q=" + encodeURIComponent(q) },
|
||||
// AI / LLM answer engines (accept a URL query and answer it directly).
|
||||
chatgpt: { name: "ChatGPT", sym: "🤖", fav: "chatgpt.com", url: (q) => "https://chatgpt.com/?q=" + encodeURIComponent(q) },
|
||||
claude: { name: "Claude", sym: "✳️", fav: "claude.ai", url: (q) => "https://claude.ai/new?q=" + encodeURIComponent(q) },
|
||||
phind: { name: "Phind", sym: "🧑💻", fav: "www.phind.com", url: (q) => "https://www.phind.com/search?q=" + encodeURIComponent(q) },
|
||||
you: { name: "You.com", sym: "🟣", fav: "you.com", url: (q) => "https://you.com/search?q=" + encodeURIComponent(q) + "&tbm=youchat" },
|
||||
duckduckgo: { kind: "search", name: "DuckDuckGo", sym: "🦆", fav: "duckduckgo.com", url: (q) => "https://duckduckgo.com/?q=" + encodeURIComponent(q) },
|
||||
google: { kind: "search", name: "Google", sym: "🔵", fav: "www.google.com", url: (q) => "https://www.google.com/search?q=" + encodeURIComponent(q) },
|
||||
brave: { kind: "search", name: "Brave", sym: "🦁", fav: "search.brave.com", url: (q) => "https://search.brave.com/search?q=" + encodeURIComponent(q) },
|
||||
bing: { kind: "search", name: "Bing", sym: "🔎", fav: "www.bing.com", url: (q) => "https://www.bing.com/search?q=" + encodeURIComponent(q) },
|
||||
startpage: { kind: "search", name: "Startpage", sym: "🛡️", fav: "www.startpage.com", url: (q) => "https://www.startpage.com/sp/search?query=" + encodeURIComponent(q) },
|
||||
yandex: { kind: "search", name: "Yandex", sym: "🔴", fav: "yandex.com", url: (q) => "https://yandex.com/search/?text=" + encodeURIComponent(q) },
|
||||
ecosia: { kind: "search", name: "Ecosia", sym: "🌱", fav: "www.ecosia.org", url: (q) => "https://www.ecosia.org/search?q=" + encodeURIComponent(q) },
|
||||
mojeek: { kind: "search", name: "Mojeek", sym: "🧭", fav: "www.mojeek.com", url: (q) => "https://www.mojeek.com/search?q=" + encodeURIComponent(q) },
|
||||
searxng: { kind: "search", name: "SearXNG", sym: "🧩", fav: "searx.be", url: (q) => "https://searx.be/search?q=" + encodeURIComponent(q) },
|
||||
wikipedia: { kind: "search", name: "Wikipedia", sym: "📖", fav: "en.wikipedia.org", url: (q) => "https://en.wikipedia.org/wiki/Special:Search?search=" + encodeURIComponent(q) },
|
||||
// AI / LLM answer engines that ANSWER the URL query without requiring a login.
|
||||
// ChatGPT / Claude / You.com's youchat all bounce to sign-in before running
|
||||
// ?q=, so they'd fail silently as a "search engine" — omitted deliberately.
|
||||
perplexity: { kind: "llm", name: "Perplexity", sym: "🧠", fav: "www.perplexity.ai", url: (q) => "https://www.perplexity.ai/search?q=" + encodeURIComponent(q) },
|
||||
phind: { kind: "llm", name: "Phind", sym: "🧑💻", fav: "www.phind.com", url: (q) => "https://www.phind.com/search?q=" + encodeURIComponent(q) },
|
||||
};
|
||||
// Engines enabled by default (shown in the toolbar dropdown). The rest are in the
|
||||
// catalog and can be turned on from Settings. Custom + detected engines are always on.
|
||||
|
|
@ -55,9 +57,9 @@ function isEnabled(id) { return (settings.enabledEngines || DEFAULT_ENABLED).inc
|
|||
// enabledEngines; custom engines are always enabled.
|
||||
function allEngines() {
|
||||
const list = Object.entries(SEARCH_ENGINES).map(([id, e]) =>
|
||||
({ id, name: e.name, sym: e.sym, favicon: faviconUrl(e.fav), builtin: true, enabled: isEnabled(id) }));
|
||||
({ id, name: e.name, sym: e.sym, favicon: faviconUrl(e.fav), kind: e.kind || "search", builtin: true, enabled: isEnabled(id) }));
|
||||
for (const c of settings.customEngines || [])
|
||||
list.push({ id: c.id, name: c.name, sym: c.sym || "🔍", favicon: customFavicon(c.url), builtin: false, enabled: true });
|
||||
list.push({ id: c.id, name: c.name, sym: c.sym || "🔍", favicon: customFavicon(c.url), kind: c.kind || "search", builtin: false, enabled: true });
|
||||
// Apply the user's custom order; ids not in engineOrder keep their natural order (stable sort).
|
||||
const order = settings.engineOrder || [];
|
||||
return list.slice().sort((a, b) => {
|
||||
|
|
@ -518,7 +520,7 @@ async function ensureIndex(force = false) {
|
|||
if (!force && sharedIndex && Date.now() - indexBuiltAt < INDEX_TTL) return sharedIndex;
|
||||
if (indexBuilding) return indexBuilding; // dedupe concurrent builds
|
||||
indexBuilding = buildIndex({ WebSocket: currentWS(), directIP: true, electrum: electrumPool })
|
||||
.then((idx) => { sharedIndex = idx; indexBuiltAt = Date.now(); return idx; })
|
||||
.then((idx) => { sharedIndex = idx; indexBuiltAt = Date.now(); refreshBcnrTlds(idx); return idx; })
|
||||
.finally(() => { indexBuilding = null; });
|
||||
// If we have a stale index, don't block on the rebuild — serve stale, refresh async.
|
||||
return (sharedIndex && !force) ? sharedIndex : indexBuilding;
|
||||
|
|
@ -584,6 +586,13 @@ const POP_W = 360; let popH = 210; // popH is updated to fit the popover's conte
|
|||
// engine favicons, like Firefox — a native <select> can't render images).
|
||||
let enginePicker, epVisible = false, epPos = { x: 8, y: 90 };
|
||||
const EP_W = 250; let epH = 320;
|
||||
// Downloads popover — a third floating overlay VIEW showing in-flight and
|
||||
// recently-finished downloads. Anchored under the toolbar's download button.
|
||||
let downloadsPop, dlVisible = false, dlPos = { x: 8, y: 90 };
|
||||
const DL_W = 340; let dlH = 240;
|
||||
// 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
|
||||
const tabs = []; // { id, view, title, url, prov }
|
||||
let activeId = null, tabSeq = 0;
|
||||
const tabById = (id) => tabs.find((t) => t.id === id);
|
||||
|
|
@ -591,9 +600,17 @@ const activeTab = () => tabById(activeId);
|
|||
|
||||
// Provenance goes to BOTH the top chrome (registry badge + site-info panel) and
|
||||
// the bottom status line, so the resolver detail lives on the bottom bar.
|
||||
// Enrich prov with a "collision candidate?" flag so the popover switcher can
|
||||
// know whether flipping BCNR<->ICANN is meaningful. A BCNR-native TLD (in
|
||||
// tlds.bch) is NOT a collision candidate — the whole TLD is BCNR's.
|
||||
function decorate(prov) {
|
||||
if (!prov || !prov.tld) return prov;
|
||||
return { ...prov, bcnrNativeTld: isBcnrNativeTld(prov.tld) };
|
||||
}
|
||||
function pushNav(prov) {
|
||||
chrome?.webContents.send("nav", prov);
|
||||
if (popVisible) popover?.webContents.send("site-info", prov);
|
||||
const p = decorate(prov);
|
||||
chrome?.webContents.send("nav", p);
|
||||
if (popVisible) popover?.webContents.send("site-info", p);
|
||||
}
|
||||
|
||||
function layout() {
|
||||
|
|
@ -604,6 +621,7 @@ function layout() {
|
|||
for (const t of tabs) t.view.setBounds({ x: 0, y: CHROME_H, width, height: bodyH });
|
||||
positionPopover();
|
||||
positionEnginePicker();
|
||||
positionDownloads();
|
||||
}
|
||||
function positionPopover() {
|
||||
if (!popover) return;
|
||||
|
|
@ -619,7 +637,7 @@ function showPopover(show) {
|
|||
win.contentView.removeChildView(popover);
|
||||
win.contentView.addChildView(popover);
|
||||
popover.setVisible(true); popVisible = true;
|
||||
popover.webContents.send("site-info", activeTab()?.prov || { kind: "home" });
|
||||
popover.webContents.send("site-info", decorate(activeTab()?.prov) || { kind: "home" });
|
||||
} else { popover.setVisible(false); popVisible = false; }
|
||||
}
|
||||
function positionEnginePicker() {
|
||||
|
|
@ -638,6 +656,66 @@ function showEnginePicker(show) {
|
|||
enginePicker.webContents.send("engines", { engines: enabledEnginesList(), current: settings.searchEngine, detected: activeTab()?.detected || null });
|
||||
} else { enginePicker.setVisible(false); epVisible = false; }
|
||||
}
|
||||
function positionDownloads() {
|
||||
if (!downloadsPop) return;
|
||||
const { width } = win.getContentBounds();
|
||||
const x = Math.max(6, Math.min(dlPos.x, width - DL_W - 6));
|
||||
downloadsPop.setBounds({ x, y: dlPos.y, width: DL_W, height: dlH });
|
||||
}
|
||||
function showDownloads(show) {
|
||||
if (!downloadsPop) return;
|
||||
if (show) {
|
||||
positionDownloads();
|
||||
win.contentView.removeChildView(downloadsPop);
|
||||
win.contentView.addChildView(downloadsPop);
|
||||
downloadsPop.setVisible(true); dlVisible = true;
|
||||
downloadsPop.webContents.send("downloads", downloadsPublic());
|
||||
} else { downloadsPop.setVisible(false); dlVisible = false; }
|
||||
}
|
||||
// Public view of a download — no DownloadItem refs leak to the renderer.
|
||||
const downloadsPublic = () => downloads.map((d) => ({ ...d }));
|
||||
function emitDownloads() {
|
||||
const pub = downloadsPublic();
|
||||
try { chrome?.webContents.send("downloads", pub); } catch {}
|
||||
if (dlVisible) try { downloadsPop?.webContents.send("downloads", pub); } catch {}
|
||||
}
|
||||
// Attach the will-download listener to the SHARED default session. Every tab's
|
||||
// WebContents inherits it, so we catch downloads regardless of which tab
|
||||
// initiated them (including anchor clicks with `download`, form posts serving
|
||||
// attachments, and manual save-as gestures).
|
||||
function installDownloadTracker() {
|
||||
session.defaultSession.on("will-download", (_e, item /*, wc */) => {
|
||||
const id = nextDlId++;
|
||||
const rec = {
|
||||
id,
|
||||
filename: item.getFilename(),
|
||||
url: item.getURL(),
|
||||
mime: item.getMimeType(),
|
||||
total: item.getTotalBytes() || 0,
|
||||
received: 0,
|
||||
state: "progressing", // progressing | paused | completed | cancelled | interrupted
|
||||
savePath: "",
|
||||
startedAt: Date.now(),
|
||||
};
|
||||
downloads.unshift(rec);
|
||||
dlItems.set(id, item);
|
||||
emitDownloads();
|
||||
item.on("updated", (_ev, state) => {
|
||||
rec.state = state; // "progressing" | "interrupted"
|
||||
rec.received = item.getReceivedBytes();
|
||||
rec.total = item.getTotalBytes() || rec.total;
|
||||
rec.savePath = item.getSavePath() || rec.savePath;
|
||||
emitDownloads();
|
||||
});
|
||||
item.once("done", (_ev, state) => {
|
||||
rec.state = state; // "completed" | "cancelled" | "interrupted"
|
||||
rec.received = item.getReceivedBytes();
|
||||
rec.savePath = item.getSavePath() || rec.savePath;
|
||||
dlItems.delete(id);
|
||||
emitDownloads();
|
||||
});
|
||||
});
|
||||
}
|
||||
function setActive(id) {
|
||||
activeId = id;
|
||||
if (popVisible) showPopover(false); // don't carry a stale popover across tabs
|
||||
|
|
@ -754,6 +832,12 @@ function createWindow() {
|
|||
win.contentView.addChildView(enginePicker);
|
||||
enginePicker.webContents.loadFile("engine-picker.html");
|
||||
enginePicker.setVisible(false);
|
||||
// Floating downloads panel — shows active + recent downloads.
|
||||
downloadsPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "downloads-preload.js") } });
|
||||
try { downloadsPop.setBackgroundColor("#00000000"); } catch {}
|
||||
win.contentView.addChildView(downloadsPop);
|
||||
downloadsPop.webContents.loadFile("downloads.html");
|
||||
downloadsPop.setVisible(false);
|
||||
chrome.webContents.once("did-finish-load", () => {
|
||||
const saved = settings.restoreSession ? loadSession() : [];
|
||||
if (saved.length) saved.forEach((u) => createTab(u)); else createTab();
|
||||
|
|
@ -811,6 +895,29 @@ async function loadBns(t, id, host, rest, tld) {
|
|||
catch { setLoading(t, false); return fallbackToWeb("BCNR unreachable"); }
|
||||
t.url = host + (rest === "/" ? "" : rest);
|
||||
if (!entry) { setLoading(t, false); return fallbackToWeb("no BCNR record"); }
|
||||
|
||||
// ---- Collision policy (BCNR ↔ ICANN) --------------------------------------
|
||||
// BCNR has the name; if the TLD is BCNR-native we're done (whole TLD is BCNR's,
|
||||
// no collision possible). Otherwise it's a collision candidate — the same name
|
||||
// *might* also exist on ICANN; the policy decides which to load.
|
||||
// Full model: SilentMode/Argus/DESIGN-collision-modes.md.
|
||||
if (!isBcnrNativeTld(tld)) {
|
||||
const policy = settings.collisionPolicy || "bcnr-first";
|
||||
let choice = overrideFor(host, tld); // per-name > per-TLD > null
|
||||
if (!choice) {
|
||||
if (policy === "icann-first") choice = "icann";
|
||||
else if (policy === "soft") {
|
||||
setLoading(t, false); // hide the loading indicator while asking
|
||||
const pick = await collisionPromptOnce({ host, tld });
|
||||
if (pick.choice === "cancel") { emitTabs(); return; } // user cancelled
|
||||
choice = pick.choice;
|
||||
rememberCollision(host, tld, choice, pick.remember);
|
||||
setLoading(t, true);
|
||||
} else choice = "bcnr"; // bcnr-first — the default
|
||||
}
|
||||
if (choice === "icann") { setLoading(t, false); return fallbackToWeb("collision → ICANN"); }
|
||||
}
|
||||
|
||||
await t.view.webContents.loadURL(`bns://${host}${rest}`);
|
||||
const src = entry.records.h ? "on-chain (chain)" : entry.records.s3 ? "Sia network" : entry.records.ip ? "direct server" : entry.records.u ? "redirect" : "record";
|
||||
t.prov = { host, kind: "ok", source: src, category: entry.category, records: Object.keys(entry.records), tld, registry };
|
||||
|
|
@ -837,6 +944,30 @@ ipcMain.handle("toggle-site-info", (_e, rect) => {
|
|||
});
|
||||
ipcMain.handle("close-site-info", () => showPopover(false));
|
||||
ipcMain.handle("popover-resize", (_e, h) => { popH = Math.max(90, Math.min(380, Math.round(h) || 210)); if (popVisible) positionPopover(); });
|
||||
|
||||
// ---- Collision-mode: per-tab live switcher + policy control -----------------
|
||||
// Flip the active tab between BCNR and ICANN for its current host, optionally
|
||||
// remembering the choice per-name / per-TLD (like the OS "Open with…" flow).
|
||||
ipcMain.handle("collision-switch", async (_e, arg) => {
|
||||
const t = activeTab(); if (!t?.prov?.host) return null;
|
||||
const host = t.prov.host, tld = tldOf(host);
|
||||
const choice = arg?.choice === "bcnr" || arg?.choice === "icann"
|
||||
? arg.choice
|
||||
: (t.prov.kind === "ok" ? "icann" : "bcnr"); // flip the current one
|
||||
rememberCollision(host, tld, choice, arg?.remember || "no");
|
||||
return navigateTab(t.id, host + (t.prov.path || "/"));
|
||||
});
|
||||
ipcMain.handle("collision-state", () => ({
|
||||
policy: settings.collisionPolicy,
|
||||
byName: collisions.byName,
|
||||
byTld: collisions.byTld,
|
||||
bcnrTlds,
|
||||
}));
|
||||
ipcMain.handle("collision-set-policy", (_e, p) => {
|
||||
if (["bcnr-first", "icann-first", "soft"].includes(p)) { settings.collisionPolicy = p; saveSettings(); }
|
||||
return settings.collisionPolicy;
|
||||
});
|
||||
ipcMain.handle("collision-reset", () => { collisions = { byName: {}, byTld: {} }; saveCollisions(); return true; });
|
||||
ipcMain.handle("search-engines", () => ({ engines: allEngines(), current: settings.searchEngine }));
|
||||
ipcMain.handle("set-search-engine", (_e, id) => {
|
||||
if (allEngines().some((e) => e.id === id)) { settings.searchEngine = id; saveSettings(); emitEngines(); }
|
||||
|
|
@ -888,6 +1019,39 @@ ipcMain.handle("pick-engine", (_e, id) => {
|
|||
showEnginePicker(false);
|
||||
});
|
||||
ipcMain.handle("picker-open-settings", () => { showEnginePicker(false); const ex = tabs.find((t) => t.settings); if (ex) return setActive(ex.id); createTab(null, { settings: true }); });
|
||||
// ---- Downloads --------------------------------------------------------------
|
||||
ipcMain.handle("downloads-get", () => downloadsPublic());
|
||||
ipcMain.handle("toggle-downloads", (_e, rect) => {
|
||||
if (dlVisible) return showDownloads(false);
|
||||
if (rect) dlPos = { x: Math.round(rect.x), y: Math.round(rect.y) };
|
||||
showDownloads(true);
|
||||
});
|
||||
ipcMain.handle("close-downloads", () => showDownloads(false));
|
||||
ipcMain.handle("downloads-resize", (_e, h) => { dlH = Math.max(80, Math.min(480, Math.round(h) || 240)); if (dlVisible) positionDownloads(); });
|
||||
ipcMain.handle("download-open", (_e, id) => {
|
||||
const d = downloads.find((x) => x.id === id);
|
||||
if (d?.state === "completed" && d.savePath) shell.openPath(d.savePath).catch(() => {});
|
||||
});
|
||||
ipcMain.handle("download-show", (_e, id) => {
|
||||
const d = downloads.find((x) => x.id === id);
|
||||
if (d?.savePath) { try { shell.showItemInFolder(d.savePath); } catch {} }
|
||||
});
|
||||
ipcMain.handle("download-cancel", (_e, id) => {
|
||||
const item = dlItems.get(id); if (item) { try { item.cancel(); } catch {} }
|
||||
});
|
||||
// Only clear finished downloads; a progressing one is cancelled first.
|
||||
ipcMain.handle("download-clear", (_e, id) => {
|
||||
const i = downloads.findIndex((x) => x.id === id);
|
||||
if (i < 0) return;
|
||||
if (downloads[i].state === "progressing") { const item = dlItems.get(id); if (item) { try { item.cancel(); } catch {} } }
|
||||
downloads.splice(i, 1); dlItems.delete(id);
|
||||
emitDownloads();
|
||||
});
|
||||
ipcMain.handle("downloads-clear-all", () => {
|
||||
// Keep any still-progressing ones; drop everything else.
|
||||
for (let i = downloads.length - 1; i >= 0; i--) if (downloads[i].state !== "progressing") downloads.splice(i, 1);
|
||||
emitDownloads();
|
||||
});
|
||||
// OpenSearch "scan": add the search engine the current page advertises.
|
||||
ipcMain.handle("add-detected-engine", () => {
|
||||
const d = activeTab()?.detected;
|
||||
|
|
@ -942,9 +1106,11 @@ if (!process.env.THESEUS_NO_AUTOSTART) {
|
|||
loadSettings();
|
||||
applyTheme();
|
||||
loadBookmarks();
|
||||
loadCollisions();
|
||||
applyPermissions();
|
||||
applyAcceptLanguage();
|
||||
protocol.handle("bns", serveBns);
|
||||
installDownloadTracker();
|
||||
createWindow();
|
||||
ensureIndex().catch(() => {}); // warm the chain index so the first .bch load is fast
|
||||
app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@
|
|||
"popover-preload.js",
|
||||
"engine-picker.html",
|
||||
"engine-picker-preload.js",
|
||||
"downloads.html",
|
||||
"downloads-preload.js",
|
||||
"collision.html",
|
||||
"collision-preload.js",
|
||||
"package.json",
|
||||
|
|
|
|||
|
|
@ -3,4 +3,7 @@ contextBridge.exposeInMainWorld("pop", {
|
|||
onData: (cb) => ipcRenderer.on("site-info", (_e, d) => cb(d)),
|
||||
close: () => ipcRenderer.invoke("close-site-info"),
|
||||
resize: (h) => ipcRenderer.invoke("popover-resize", h),
|
||||
// Live switcher: flip the active tab between BCNR and ICANN for this host,
|
||||
// optionally remembering the choice per-name / per-TLD.
|
||||
switchTo: (choice, remember) => ipcRenderer.invoke("collision-switch", { choice, remember }),
|
||||
});
|
||||
|
|
|
|||
47
popover.html
47
popover.html
|
|
@ -22,6 +22,17 @@
|
|||
.details { border-top: 1px solid #ffffff12; padding: 11px 16px 13px; display: grid; grid-template-columns: auto 1fr; gap: 7px 14px; font-size: 12px; }
|
||||
.details .k { color: #7f8aa0; } .details .v { color: #e7eaf1; }
|
||||
.details .v code { font-family: ui-monospace, monospace; color: #bfeae4; word-break: break-all; }
|
||||
/* Collision switcher — only shown when the host could be BCNR OR ICANN */
|
||||
.switcher { border-top: 1px solid #ffffff12; padding: 10px 16px 12px; font-size: 12px; }
|
||||
.switcher .title { color: #7f8aa0; margin-bottom: 6px; }
|
||||
.switcher .row { display: flex; gap: 6px; }
|
||||
.switcher button { flex: 1; background: transparent; color: #e7eaf1; border: 1px solid #ffffff26; border-radius: 6px; padding: 6px 8px; cursor: pointer; font-size: 12px; }
|
||||
.switcher button:hover { background: #ffffff10; }
|
||||
.switcher button.active { background: #f5c51820; border-color: #f5c518; color: #f5c518; cursor: default; }
|
||||
.switcher button.active.icann { background: #4c9eff20; border-color: #4c9eff; color: #4c9eff; }
|
||||
.switcher .rem { display: flex; gap: 10px; margin-top: 8px; color: #7f8aa0; }
|
||||
.switcher .rem label { display: flex; gap: 4px; align-items: center; cursor: pointer; }
|
||||
.switcher .rem input { accent-color: #f5c518; }
|
||||
/* light theme (placed last so it wins over the dark base rules) */
|
||||
@media (prefers-color-scheme: light) {
|
||||
.card { background: #ffffff; border-color: rgba(0,0,0,.15); color: #1a1f28; }
|
||||
|
|
@ -31,6 +42,12 @@
|
|||
.details .v code { color: #0a7d73; }
|
||||
.hero.neutral .ico svg { stroke: #697280; } .hero.neutral .ico .bdy { fill: #697280; }
|
||||
.hero .ico .kh { fill: #ffffff; }
|
||||
.switcher { border-top-color: rgba(0,0,0,.10); }
|
||||
.switcher .title, .switcher .rem { color: #7b8494; }
|
||||
.switcher button { color: #1a1f28; border-color: rgba(0,0,0,.15); }
|
||||
.switcher button:hover { background: rgba(0,0,0,.05); }
|
||||
.switcher button.active { background: #f5c51830; }
|
||||
.switcher button.active.icann { background: #4c9eff30; }
|
||||
}
|
||||
</style></head>
|
||||
<body>
|
||||
|
|
@ -41,6 +58,18 @@
|
|||
<div><div class="status" id="status"></div><div class="host" id="host"></div><div class="sub" id="sub"></div></div>
|
||||
</div>
|
||||
<div class="details" id="details"></div>
|
||||
<div class="switcher" id="switcher" style="display:none">
|
||||
<div class="title">Open this name with</div>
|
||||
<div class="row">
|
||||
<button id="sw-bcnr" data-choice="bcnr">⛓ BCNR</button>
|
||||
<button id="sw-icann" data-choice="icann" class="icann">🌐 ICANN</button>
|
||||
</div>
|
||||
<div class="rem">
|
||||
<label><input type="radio" name="sw-rem" value="no" checked> Just this once</label>
|
||||
<label><input type="radio" name="sw-rem" value="name"> Remember for <b id="sw-host"></b></label>
|
||||
<label><input type="radio" name="sw-rem" value="tld"> All <b id="sw-tld"></b></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
|
@ -78,7 +107,25 @@
|
|||
}
|
||||
hero.className = cls; $("ico").innerHTML = ico; $("status").textContent = status; $("sub").innerHTML = sub;
|
||||
$("details").innerHTML = details; $("details").style.display = details ? "grid" : "none";
|
||||
// ---- collision switcher: only visible when this host is a collision candidate
|
||||
// (loaded via BCNR or ICANN under a non-BCNR-native TLD) so flipping is meaningful.
|
||||
const sw = $("switcher"), isCand = d && d.host && d.tld && (d.kind === "ok" || d.kind === "web") && !d.bcnrNativeTld;
|
||||
if (isCand) {
|
||||
const active = d.kind === "ok" ? "bcnr" : "icann";
|
||||
$("sw-bcnr").classList.toggle("active", active === "bcnr");
|
||||
$("sw-icann").classList.toggle("active", active === "icann");
|
||||
$("sw-host").textContent = d.host;
|
||||
$("sw-tld").textContent = "." + d.tld;
|
||||
sw.style.display = "block";
|
||||
} else { sw.style.display = "none"; }
|
||||
requestAnimationFrame(() => { try { window.pop.resize(document.querySelector(".card").offsetHeight); } catch (e) {} });
|
||||
});
|
||||
// Live switcher — click the OTHER registry to flip the tab; the active one is a no-op.
|
||||
for (const id of ["sw-bcnr", "sw-icann"]) $(id).onclick = (e) => {
|
||||
const b = e.currentTarget; if (b.classList.contains("active")) return;
|
||||
const rem = document.querySelector('input[name="sw-rem"]:checked')?.value || "no";
|
||||
window.pop.switchTo(b.dataset.choice, rem);
|
||||
window.pop.close();
|
||||
};
|
||||
</script>
|
||||
</body></html>
|
||||
|
|
|
|||
|
|
@ -29,4 +29,12 @@ contextBridge.exposeInMainWorld("theseus", {
|
|||
onTor: (cb) => ipcRenderer.on("tor", (_e, d) => cb(d)),
|
||||
onTabs: (cb) => ipcRenderer.on("tabs", (_e, d) => cb(d)),
|
||||
onBcnrOffer: (cb) => ipcRenderer.on("bcnr-offer", (_e, d) => cb(d)),
|
||||
// Collision-mode (BCNR ↔ ICANN) live switcher for the active tab
|
||||
collisionSwitch: (arg) => ipcRenderer.invoke("collision-switch", arg),
|
||||
collisionState: () => ipcRenderer.invoke("collision-state"),
|
||||
// Downloads — the toolbar button subscribes to `downloads` to update its
|
||||
// badge, and toggleDownloads opens/closes the floating panel.
|
||||
getDownloads: () => ipcRenderer.invoke("downloads-get"),
|
||||
toggleDownloads: (rect) => ipcRenderer.invoke("toggle-downloads", rect),
|
||||
onDownloads: (cb) => ipcRenderer.on("downloads", (_e, d) => cb(d)),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,4 +7,8 @@ contextBridge.exposeInMainWorld("cfg", {
|
|||
removeEngine: (id) => ipcRenderer.invoke("remove-engine", id),
|
||||
setEngineEnabled: (id, on) => ipcRenderer.invoke("set-engine-enabled", id, on),
|
||||
setEngineOrder: (ids) => ipcRenderer.invoke("set-engine-order", ids),
|
||||
// Collision-mode: BCNR/ICANN policy + per-name/per-TLD overrides
|
||||
collisionState: () => ipcRenderer.invoke("collision-state"),
|
||||
setCollisionPolicy: (p) => ipcRenderer.invoke("collision-set-policy", p),
|
||||
resetCollisions: () => ipcRenderer.invoke("collision-reset"),
|
||||
});
|
||||
|
|
|
|||
243
settings.html
243
settings.html
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head><meta charset="utf-8"><title>Theseus — Settings</title>
|
||||
<style>
|
||||
:root{ color-scheme: light dark; --bg:#0b0e14; --panel:#141a24; --line:rgba(255,255,255,.09);
|
||||
:root{ --bg:#0b0e14; --panel:#141a24; --line:rgba(255,255,255,.09);
|
||||
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d; }
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;height:100vh;background:var(--bg);color:var(--ink);font:15px/1.6 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}
|
||||
|
|
@ -25,16 +25,57 @@
|
|||
.row .txt{flex:1}
|
||||
.row .t{font-weight:600}
|
||||
.row .d{color:var(--mut);font-size:13px;margin-top:2px}
|
||||
.ctl{display:flex;flex-direction:column;gap:6px;align-items:flex-end;flex:none}
|
||||
/* control column — mode select + optional value field on the same row so the
|
||||
dropdown menus don't get cut off underneath, wraps only when narrow */
|
||||
.ctl{display:flex;flex-direction:row;flex-wrap:wrap;gap:6px;align-items:center;justify-content:flex-end;flex:none;max-width:60%}
|
||||
.ctl .coords{display:flex;gap:6px}
|
||||
select,.ctl input{background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:13px;outline:none}
|
||||
select,.ctl input{background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:13px;outline:none;min-width:140px}
|
||||
select:focus,.ctl input:focus{border-color:#4b7bec}
|
||||
.ctl input{width:150px} .ctl .coords input{width:92px}
|
||||
.ctl input{width:170px} .ctl .coords input{width:92px;min-width:auto}
|
||||
/* Chromium's native <select> popup uses the page's color-scheme; when it's
|
||||
"light dark" (both accepted) Chromium picks by the OS, so a dark-theme app
|
||||
on a light OS shows a light popup. main.js's applyTheme() maps to
|
||||
nativeTheme.themeSource, which drives prefers-color-scheme — so pinning
|
||||
color-scheme via that media query keeps the popup in sync automatically. */
|
||||
:root { color-scheme: dark; }
|
||||
@media (prefers-color-scheme: light) { :root { color-scheme: light; } }
|
||||
/* Explicit option styling — Chromium respects it in the popup on Windows. */
|
||||
select option { background: #1b2330; color: var(--ink); }
|
||||
@media (prefers-color-scheme: light) { select option { background: #f1f3f7; color: #1a1f28; } }
|
||||
code{font-family:ui-monospace,monospace;font-size:12px;background:#0e131b;border:1px solid var(--line);border-radius:5px;padding:1px 5px;color:#bfeae4}
|
||||
.addeng{display:flex;gap:6px} .addeng input{flex:1;background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:8px 10px;font-size:13px;outline:none}
|
||||
.addeng input:focus{border-color:#4b7bec}
|
||||
.btn{border:1px solid #d6ff3d55;background:rgba(214,255,61,.12);color:#eaffb0;border-radius:8px;padding:8px 14px;font-size:13px;cursor:pointer}
|
||||
.btn:hover{background:rgba(214,255,61,.22)}
|
||||
/* segmented control — small toggle used elsewhere (kept for reuse) */
|
||||
.segseg{display:inline-flex;background:#10151f;border:1px solid var(--line);border-radius:8px;padding:2px;gap:2px}
|
||||
.segseg .seg{background:transparent;color:var(--mut);border:none;border-radius:6px;padding:6px 12px;font-size:13px;cursor:pointer;font:inherit;line-height:1.2}
|
||||
.segseg .seg:hover{color:var(--ink)}
|
||||
.segseg .seg.on{background:#1c2432;color:var(--ink);box-shadow:inset 0 0 0 1px var(--line)}
|
||||
/* theme cards — three visual previews (System / Light / Dark) */
|
||||
.themeCards{display:flex;gap:14px;margin:6px 0 4px;flex-wrap:wrap}
|
||||
.themeCards .tc{background:transparent;border:2px solid var(--line);border-radius:10px;padding:10px;
|
||||
display:flex;flex-direction:column;align-items:center;gap:8px;cursor:pointer;color:var(--ink);
|
||||
font:inherit;font-size:13px;min-width:150px;transition:border-color .12s}
|
||||
.themeCards .tc:hover{border-color:#4b7bec55}
|
||||
.themeCards .tc.on{border-color:#4b7bec;box-shadow:0 0 0 1px #4b7bec inset}
|
||||
.themeCards .mock{width:130px;height:82px;border-radius:6px;overflow:hidden;display:block;position:relative;
|
||||
border:1px solid rgba(255,255,255,.08)}
|
||||
.themeCards .mock .mm-chrome{position:absolute;left:0;right:0;top:0;height:22px;display:block}
|
||||
.themeCards .mock .mm-chrome::before{content:"";position:absolute;left:8px;top:6px;width:8px;height:8px;border-radius:50%;background:#f6768a}
|
||||
.themeCards .mock .mm-chrome::after {content:"";position:absolute;left:22px;top:6px;width:8px;height:8px;border-radius:50%;background:#f6c15c;box-shadow:14px 0 0 #6ec27d}
|
||||
.themeCards .mock .mm-body{position:absolute;left:0;right:0;top:22px;bottom:0;display:block}
|
||||
.themeCards .mock-light .mm-chrome{background:#e9ecf2}
|
||||
.themeCards .mock-light .mm-body {background:#ffffff;background-image:linear-gradient(#0000000c 1px,transparent 1px);background-size:100% 12px;background-position:0 10px}
|
||||
.themeCards .mock-dark .mm-chrome{background:#141b28}
|
||||
.themeCards .mock-dark .mm-body {background:#0b0e14;background-image:linear-gradient(#ffffff10 1px,transparent 1px);background-size:100% 12px;background-position:0 10px}
|
||||
.themeCards .mock-system .mm-chrome{background:linear-gradient(90deg,#141b28 0 50%,#e9ecf2 50% 100%)}
|
||||
.themeCards .mock-system .mm-body{background:linear-gradient(90deg,#0b0e14 0 50%,#ffffff 50% 100%)}
|
||||
.themeCards .tc-label{font-weight:500;color:var(--ink)}
|
||||
.polrow{display:flex;align-items:center;gap:10px;background:#10151f;border:1px solid var(--line);border-radius:8px;padding:8px 12px;font-size:13px;cursor:pointer}
|
||||
.polrow:hover{background:#141c28}
|
||||
.polrow input{accent-color:#d6ff3d}
|
||||
.pmuted{color:var(--mut)}
|
||||
.ceng{display:flex;align-items:center;gap:8px;background:#10151f;border:1px solid var(--line);border-radius:8px;padding:6px 10px;margin-bottom:6px;font-size:13px}
|
||||
.ceng .cs{font-size:14px;flex:none}
|
||||
/* engine checklist */
|
||||
|
|
@ -48,6 +89,19 @@
|
|||
.eng .grip:active{cursor:grabbing}
|
||||
.eng.dragging{opacity:.45}
|
||||
.eng.over{border-color:var(--acid);box-shadow:0 -2px 0 var(--acid) inset}
|
||||
.ehdr{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--dim);margin:14px 0 6px;padding-top:2px}
|
||||
.ehdr:first-child{margin-top:0}
|
||||
/* engine catalog panel — appears under the enabled list when "+ Add" is clicked */
|
||||
.engcat{margin-top:6px;padding:12px 12px 10px;background:#0e131c;border:1px solid var(--line);border-radius:10px}
|
||||
.engcat .cat{display:flex;align-items:center;gap:10px;padding:6px 8px;border-radius:6px;font-size:13px}
|
||||
.engcat .cat:hover{background:#141b26}
|
||||
.engcat .cat .eic{width:18px;height:18px;flex:none;display:grid;place-items:center}
|
||||
.engcat .cat .eic .ei{width:16px;height:16px;border-radius:3px} .engcat .cat .eic .es{font-size:14px}
|
||||
.engcat .cat .enm{flex:1}
|
||||
.engcat .cat .kind{font-size:10.5px;letter-spacing:.05em;color:var(--dim);padding:1px 6px;border:1px solid var(--line);border-radius:999px}
|
||||
.engcat .cat .add{background:transparent;border:1px solid var(--line);color:var(--ink);border-radius:6px;padding:4px 10px;font-size:12px;cursor:pointer}
|
||||
.engcat .cat .add:hover{background:rgba(214,255,61,.14);border-color:#d6ff3d55;color:#eaffb0}
|
||||
.engcat .cempty2{color:var(--dim);font-size:12.5px;padding:6px 8px}
|
||||
.sw.sm{width:38px;height:22px} .sw.sm .knob{width:15px;height:15px} .sw.sm input:checked + .track .knob{transform:translateX(16px)}
|
||||
.ceng .cn{font-weight:600} .ceng .cu{color:var(--dim);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
|
||||
.ceng .cx{cursor:pointer;color:#8b98a9;border:none;background:transparent;font-size:13px} .ceng .cx:hover{color:#f6768a}
|
||||
|
|
@ -76,6 +130,8 @@
|
|||
<nav class="side">
|
||||
<div class="brand">⛓ Theseus</div>
|
||||
<a data-sec="general" class="active">General</a>
|
||||
<a data-sec="search">Search</a>
|
||||
<a data-sec="naming">Naming</a>
|
||||
<a data-sec="performance">Performance</a>
|
||||
<a data-sec="privacy">Privacy</a>
|
||||
</nav>
|
||||
|
|
@ -84,28 +140,78 @@
|
|||
<section id="general">
|
||||
<h1>General</h1>
|
||||
<p class="lede">Changes apply immediately and are saved for next time.</p>
|
||||
<h2 class="sub" style="border-top:0;padding-top:0;margin-top:0">Startup</h2>
|
||||
<div class="row">
|
||||
<div class="txt"><div class="t">Appearance</div><div class="d">Light, dark, or follow your system theme.</div></div>
|
||||
<div class="ctl"><select id="theme"><option value="dark">Dark</option><option value="light">Light</option><option value="system">System</option></select></div>
|
||||
<div class="txt"><div class="t">Open previous windows and tabs</div><div class="d">Restore the tabs from your last session when Theseus starts.</div></div>
|
||||
<label class="sw"><input type="checkbox" id="restoreSession"><span class="track"><span class="knob"></span></span></label>
|
||||
</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">
|
||||
<button type="button" class="tc" data-val="system" role="radio" aria-checked="false">
|
||||
<span class="mock mock-system"><span class="mm-chrome"></span><span class="mm-body"></span></span>
|
||||
<span class="tc-label">System</span>
|
||||
</button>
|
||||
<button type="button" class="tc" data-val="light" role="radio" aria-checked="false">
|
||||
<span class="mock mock-light"><span class="mm-chrome"></span><span class="mm-body"></span></span>
|
||||
<span class="tc-label">Light</span>
|
||||
</button>
|
||||
<button type="button" class="tc" data-val="dark" role="radio" aria-checked="false">
|
||||
<span class="mock mock-dark"><span class="mm-chrome"></span><span class="mm-body"></span></span>
|
||||
<span class="tc-label">Dark</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<!-- SEARCH -->
|
||||
<section id="search" hidden>
|
||||
<h1>Search</h1>
|
||||
<p class="lede">Pick what your address bar and the toolbar dropdown search with.</p>
|
||||
<div class="row">
|
||||
<div class="txt"><div class="t">Default search engine</div><div class="d">Used when you type a search into the address bar.</div></div>
|
||||
<div class="txt"><div class="t">Default search engine</div><div class="d">Used when you type into the address bar.</div></div>
|
||||
<div class="ctl"><select id="searchEngine"></select></div>
|
||||
</div>
|
||||
<div class="row" style="flex-direction:column;align-items:stretch;gap:10px">
|
||||
<div class="txt"><div class="t">Search engines</div>
|
||||
<div class="d">Tick which appear in the toolbar's search dropdown. Add your own with a URL containing <code>%s</code> — e.g. <code>https://searx.be/search?q=%s</code>. Theseus also offers to add a site's own search when you visit one.</div></div>
|
||||
<div class="txt" style="display:flex;align-items:center;justify-content:space-between;gap:12px">
|
||||
<div>
|
||||
<div class="t">Additional search engines</div>
|
||||
<div class="d">These appear in the toolbar dropdown. Drag to reorder. Toggle off to move an engine back to the catalog.</div>
|
||||
</div>
|
||||
<button id="engAddBtn" class="btn" type="button">+ Add search engine</button>
|
||||
</div>
|
||||
<div id="engineList"></div>
|
||||
<div class="addeng">
|
||||
<input id="engSym" placeholder="🔍" style="max-width:52px;text-align:center;flex:none">
|
||||
<input id="engName" placeholder="Name (e.g. SearXNG)">
|
||||
<input id="engUrl" placeholder="https://example.com/search?q=%s">
|
||||
<button id="engAdd" class="btn">Add</button>
|
||||
<!-- Catalog: hidden until "Add" is clicked. Shows built-in engines the user
|
||||
hasn't enabled + a custom-URL form. -->
|
||||
<div id="engineCatalog" class="engcat" hidden>
|
||||
<div class="ehdr">Add from catalog</div>
|
||||
<div id="catalogList"></div>
|
||||
<div class="ehdr">Or add a custom URL</div>
|
||||
<div class="addeng">
|
||||
<input id="engSym" placeholder="🔍" style="max-width:52px;text-align:center;flex:none">
|
||||
<input id="engName" placeholder="Name (e.g. SearXNG)">
|
||||
<input id="engUrl" placeholder="https://example.com/search?q=%s">
|
||||
<button id="engAdd" class="btn">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="txt"><div class="t">Reopen tabs on launch</div><div class="d">Restore the tabs from your last session when Theseus starts.</div></div>
|
||||
<label class="sw"><input type="checkbox" id="restoreSession"><span class="track"><span class="knob"></span></span></label>
|
||||
</section>
|
||||
<!-- NAMING -->
|
||||
<section id="naming" hidden>
|
||||
<h1>Naming</h1>
|
||||
<p class="lede">How Theseus picks between the <b>Bitcoin Cash Name Registry (BCNR)</b> and <b>ICANN</b> when a name exists in both.</p>
|
||||
<div class="row" style="flex-direction:column;align-items:stretch;gap:10px">
|
||||
<div class="txt"><div class="t">Collision policy</div>
|
||||
<div class="d">A name only exists in both registries when its TLD isn't BCNR-native (e.g. <code>.de</code>). BCNR-native TLDs (<code>.bch</code>, <code>.p2p</code>, …) never conflict.</div></div>
|
||||
<div id="policyList" style="display:flex;flex-direction:column;gap:6px">
|
||||
<label class="polrow"><input type="radio" name="collisionPolicy" value="bcnr-first"><span><b>BCNR first</b> <span class="pmuted">— BCNR wins conflicts; falls back to ICANN.</span></span></label>
|
||||
<label class="polrow"><input type="radio" name="collisionPolicy" value="icann-first"><span><b>ICANN first</b> <span class="pmuted">— ICANN wins conflicts; BCNR fills gaps.</span></span></label>
|
||||
<label class="polrow"><input type="radio" name="collisionPolicy" value="soft"><span><b>Ask each time</b> <span class="pmuted">— an "Open with…" prompt on conflict, remembered per name.</span></span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="flex-direction:column;align-items:stretch;gap:8px">
|
||||
<div class="txt"><div class="t">Remembered choices</div>
|
||||
<div class="d">"Always use …" picks you made from the switcher or the prompt. Reset them to be asked again.</div></div>
|
||||
<div id="colSummary" class="pmuted" style="font-size:12.5px"></div>
|
||||
<div><button id="resetCollisions" class="btn">Reset remembered choices</button></div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- PERFORMANCE -->
|
||||
|
|
@ -211,7 +317,7 @@
|
|||
<script>
|
||||
const C = window.cfg;
|
||||
// sidebar navigation
|
||||
const sections = ["general", "performance", "privacy"];
|
||||
const sections = ["general", "search", "naming", "performance", "privacy"];
|
||||
document.querySelectorAll(".side a").forEach((a) => a.onclick = () => {
|
||||
document.querySelectorAll(".side a").forEach((x) => x.classList.toggle("active", x === a));
|
||||
for (const s of sections) document.getElementById(s).hidden = (s !== a.dataset.sec);
|
||||
|
|
@ -230,32 +336,70 @@
|
|||
const engIcon = (e) => e.favicon
|
||||
? `<img class="ei" src="${esc(e.favicon)}" onerror="this.replaceWith(Object.assign(document.createElement('span'),{className:'es',textContent:'${e.sym || "🔍"}'}))">`
|
||||
: `<span class="es">${e.sym || "🔍"}</span>`;
|
||||
const ENGINE_KINDS = [
|
||||
{ key: "search", label: "Search engines" },
|
||||
{ key: "llm", label: "AI answer engines" },
|
||||
];
|
||||
function renderEngines(d) {
|
||||
// default engine: pick from the ENABLED engines (those shown in the dropdown)
|
||||
const enabled = d.engines.filter((e) => e.enabled);
|
||||
sel.innerHTML = enabled.map((e) => `<option value="${e.id}">${(e.sym ? e.sym + " " : "")}${esc(e.name)}</option>`).join("");
|
||||
// group the dropdown too, so search and LLM don't intermix
|
||||
sel.innerHTML = ENGINE_KINDS.map(({ key, label }) => {
|
||||
const opts = enabled.filter((e) => (e.kind || "search") === key)
|
||||
.map((e) => `<option value="${e.id}">${(e.sym ? e.sym + " " : "")}${esc(e.name)}</option>`).join("");
|
||||
return opts ? `<optgroup label="${label}">${opts}</optgroup>` : "";
|
||||
}).join("");
|
||||
sel.value = d.current;
|
||||
// unified checklist: drag a row by its handle to reorder; built-in get an
|
||||
// enable toggle, custom get a remove ✕.
|
||||
// Main list = the engines the user has enabled (i.e. actually shown in the
|
||||
// toolbar dropdown). Everything else lives in the catalog panel below and
|
||||
// is added via "+ Add". Drag reorders within a kind; toggling off moves an
|
||||
// engine back to the catalog; custom engines get a ✕ to delete entirely.
|
||||
const list = document.getElementById("engineList");
|
||||
list.innerHTML = d.engines.map((e) => `<div class="eng" data-id="${e.id}" draggable="true">` +
|
||||
const rowFor = (e) => `<div class="eng" data-id="${e.id}" data-kind="${e.kind || "search"}" draggable="true">` +
|
||||
`<span class="grip" title="Drag to reorder">⠿</span>` +
|
||||
`<span class="eic">${engIcon(e)}</span><span class="enm">${esc(e.name)}</span>` +
|
||||
(e.builtin
|
||||
? `<label class="sw sm"><input type="checkbox" data-id="${e.id}" ${e.enabled ? "checked" : ""}><span class="track"><span class="knob"></span></span></label>`
|
||||
: `<button class="cx" data-id="${e.id}" title="Remove">✕</button>`) + `</div>`).join("");
|
||||
: `<button class="cx" data-id="${e.id}" title="Remove">✕</button>`) + `</div>`;
|
||||
list.innerHTML = ENGINE_KINDS.map(({ key, label }) => {
|
||||
const rows = enabled.filter((e) => (e.kind || "search") === key).map(rowFor).join("");
|
||||
if (!rows) return "";
|
||||
return `<div class="ehdr">${label}</div>${rows}`;
|
||||
}).join("");
|
||||
// Catalog = built-in engines the user has NOT enabled, ready to add.
|
||||
const cat = document.getElementById("catalogList");
|
||||
if (cat) {
|
||||
const off = d.engines.filter((e) => e.builtin && !e.enabled);
|
||||
const catRow = (e) => `<div class="cat" data-id="${e.id}">` +
|
||||
`<span class="eic">${engIcon(e)}</span>` +
|
||||
`<span class="enm">${esc(e.name)}</span>` +
|
||||
`<span class="kind">${(e.kind || "search") === "llm" ? "AI" : "Search"}</span>` +
|
||||
`<button class="add" data-add="${e.id}">+ Add</button></div>`;
|
||||
cat.innerHTML = off.length
|
||||
? off.map(catRow).join("")
|
||||
: `<div class="cempty2">All built-in engines are enabled. Add a custom URL below.</div>`;
|
||||
cat.querySelectorAll(".add").forEach((b) => b.onclick = () => C.setEngineEnabled(b.dataset.add, true).then(renderEngines));
|
||||
}
|
||||
list.querySelectorAll('input[type="checkbox"]').forEach((cb) => cb.onchange = () => C.setEngineEnabled(cb.dataset.id, cb.checked).then(renderEngines));
|
||||
list.querySelectorAll(".cx").forEach((b) => b.onclick = () => C.removeEngine(b.dataset.id).then(renderEngines));
|
||||
// drag-and-drop reorder
|
||||
// drag-and-drop reorder — same-kind only (dropping a Search engine into
|
||||
// the LLM section would just re-group visually on next render, so we
|
||||
// reject cross-kind drags outright).
|
||||
let dragId = null;
|
||||
let dragKind = null;
|
||||
const sameKind = (row) => row.dataset.kind === dragKind;
|
||||
list.querySelectorAll(".eng").forEach((row) => {
|
||||
row.addEventListener("dragstart", (e) => { dragId = row.dataset.id; e.dataTransfer.effectAllowed = "move"; row.classList.add("dragging"); });
|
||||
row.addEventListener("dragstart", (e) => { dragId = row.dataset.id; dragKind = row.dataset.kind; e.dataTransfer.effectAllowed = "move"; row.classList.add("dragging"); });
|
||||
row.addEventListener("dragend", () => { row.classList.remove("dragging"); list.querySelectorAll(".eng").forEach((r) => r.classList.remove("over")); });
|
||||
row.addEventListener("dragover", (e) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; if (row.dataset.id !== dragId) row.classList.add("over"); });
|
||||
row.addEventListener("dragover", (e) => {
|
||||
if (!sameKind(row)) { e.dataTransfer.dropEffect = "none"; return; }
|
||||
e.preventDefault(); e.dataTransfer.dropEffect = "move";
|
||||
if (row.dataset.id !== dragId) row.classList.add("over");
|
||||
});
|
||||
row.addEventListener("dragleave", () => row.classList.remove("over"));
|
||||
row.addEventListener("drop", (e) => {
|
||||
e.preventDefault(); row.classList.remove("over");
|
||||
if (!dragId || dragId === row.dataset.id) return;
|
||||
if (!dragId || dragId === row.dataset.id || !sameKind(row)) return;
|
||||
const ids = [...list.querySelectorAll(".eng")].map((el) => el.dataset.id);
|
||||
const from = ids.indexOf(dragId), to = ids.indexOf(row.dataset.id);
|
||||
ids.splice(from, 1); ids.splice(to, 0, dragId);
|
||||
|
|
@ -264,10 +408,19 @@
|
|||
});
|
||||
}
|
||||
sel.onchange = () => C.set("searchEngine", sel.value);
|
||||
// appearance (theme)
|
||||
// appearance (theme) — three visual cards: system | light | dark. Any
|
||||
// unrecognised saved value falls back to "system" (follow the OS).
|
||||
const th = document.getElementById("theme");
|
||||
th.value = s.theme || "dark";
|
||||
th.onchange = () => C.set("theme", th.value);
|
||||
let themeValue = ["system", "light", "dark"].includes(s.theme) ? s.theme : "system";
|
||||
const paintTheme = () => th.querySelectorAll(".tc").forEach((b) => {
|
||||
const on = b.dataset.val === themeValue;
|
||||
b.classList.toggle("on", on);
|
||||
b.setAttribute("aria-checked", on ? "true" : "false");
|
||||
});
|
||||
paintTheme();
|
||||
th.querySelectorAll(".tc").forEach((b) => b.onclick = () => {
|
||||
themeValue = b.dataset.val; paintTheme(); C.set("theme", themeValue);
|
||||
});
|
||||
// WebRTC IP policy
|
||||
const wm = document.getElementById("webrtcMode");
|
||||
wm.value = s.webrtcMode || "public_only";
|
||||
|
|
@ -282,6 +435,16 @@
|
|||
renderEngines(d);
|
||||
});
|
||||
};
|
||||
// "+ Add search engine" toggles the catalog panel below the enabled list.
|
||||
const catBtn = document.getElementById("engAddBtn");
|
||||
const catBox = document.getElementById("engineCatalog");
|
||||
if (catBtn && catBox) {
|
||||
catBtn.onclick = () => {
|
||||
const open = catBox.hidden;
|
||||
catBox.hidden = !open;
|
||||
catBtn.textContent = open ? "− Hide catalog" : "+ Add search engine";
|
||||
};
|
||||
}
|
||||
C.engines().then(renderEngines);
|
||||
// anti-fingerprinting mode selectors, with value field(s) shown on "manual"
|
||||
const bind = (mode, showValIf, apply) => {
|
||||
|
|
@ -320,6 +483,26 @@
|
|||
const la = document.getElementById("locationLat"), lo = document.getElementById("locationLon");
|
||||
la.value = c[0]; lo.value = c[1]; C.set("locationLat", String(c[0])); C.set("locationLon", String(c[1]));
|
||||
});
|
||||
|
||||
// ---- Naming section: BCNR/ICANN collision policy + remembered choices ----
|
||||
function refreshCollisions() {
|
||||
C.collisionState().then((cs) => {
|
||||
// pick the current radio
|
||||
document.querySelectorAll('input[name="collisionPolicy"]').forEach((r) => { r.checked = (r.value === cs.policy); });
|
||||
const nn = Object.keys(cs.byName || {}).length, tn = Object.keys(cs.byTld || {}).length;
|
||||
const bc = (cs.bcnrTlds || []).length;
|
||||
document.getElementById("colSummary").textContent =
|
||||
`Remembered: ${nn} name${nn === 1 ? "" : "s"}, ${tn} TLD${tn === 1 ? "" : "s"}. `
|
||||
+ `BCNR-native TLDs on chain: ${bc}.`;
|
||||
}).catch(() => {});
|
||||
}
|
||||
refreshCollisions();
|
||||
document.querySelectorAll('input[name="collisionPolicy"]').forEach((r) => {
|
||||
r.addEventListener("change", () => { if (r.checked) C.setCollisionPolicy(r.value).then(refreshCollisions); });
|
||||
});
|
||||
document.getElementById("resetCollisions").onclick = () => {
|
||||
C.resetCollisions().then(refreshCollisions);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue