Theseus: in-tab collision prompt + toggle-style switcher + auto-switch
Three UX fixes based on operator feedback (2026-08-02):
1) 'Open with…' is now a FULL-PAGE in-tab interstitial (was a modal window).
- loadBns loads collision.html via loadFile with query params instead of
opening a BrowserWindow.
- collision.html is navigation-based: buttons redirect to a special
bns://collision-choose/?host=…&choice=…&remember=…&resturl=… URL.
- serveBns handles that URL: persists remember=name/tld choice, then returns
a meta-refresh to the real target. For BCDN it appends ?_collision=bcnr
one-shot marker; for ICANN it redirects to https://<host><path>.
- loadBns strips and honors the ?_collision one-shot marker so BCDN
redirects don't re-trigger the prompt.
- Old modal path (collisionPromptOnce) removed.
2) Auto-switch from the address-bar chip: clicking BCDN/ICANN chip sets a
transient per-tab collisionOverride that loadBns consumes ONCE for this
navigation only, bypassing the soft-mode prompt entirely.
3) Address-bar toggle: chips stay visible during 'resolving' (no more
flash-and-reappear); active chip is colored (acid green for BCDN, blue for
ICANN — matching collision.html's palette); inactive is greyed with border,
clickable to switch. Feels like a proper toggle.
Settings > Registries copy: operator's exact wording adopted (title stays
'Registries', body uses 'BCNR-unique' term for TLDs that only exist on BCNR).
This commit is contained in:
parent
75e468ae39
commit
bf54c245b7
4 changed files with 212 additions and 112 deletions
33
chrome.html
33
chrome.html
|
|
@ -70,14 +70,17 @@
|
|||
.secbadge.warn svg .shk { stroke: #f6ad55; } .secbadge.warn svg .bdy { fill: #f6ad55; }
|
||||
input { padding: 7px 6px; border-radius: 999px; border: none; background: transparent; color: inherit; font-size: 13.5px; outline: none; }
|
||||
#url { flex: 1; }
|
||||
/* registry indicator at the END of the address bar — one or two chips */
|
||||
/* registry toggle at the END of the address bar — one or two chips */
|
||||
#reg { display: inline-flex; gap: 4px; align-items: center; }
|
||||
.reg { font-size: 10.5px; letter-spacing: .03em; padding: 2px 9px; border-radius: 999px; white-space: nowrap;
|
||||
background: rgba(214,255,61,.13); color: #d6ff3d; border: 1px solid #d6ff3d33; cursor: default; user-select: none; }
|
||||
.reg.icann { background: rgba(76,158,255,.13); color: #4c9eff; border-color: rgba(76,158,255,.28); }
|
||||
/* collision candidate: two chips shown together; inactive one is dim + clickable */
|
||||
.reg.switch { cursor: pointer; opacity: .55; }
|
||||
.reg.switch:hover { opacity: 1; }
|
||||
cursor: default; user-select: none; transition: opacity .15s, background .15s, color .15s, border-color .15s; }
|
||||
/* Active BCDN — acid glow green */
|
||||
.reg.bcdn.active { background: rgba(214,255,61,.15); color: #d6ff3d; border: 1px solid #d6ff3d55; }
|
||||
/* Active ICANN — blue */
|
||||
.reg.icann.active { background: rgba(76,158,255,.15); color: #4c9eff; border: 1px solid #4c9eff55; }
|
||||
/* Inactive chip in the toggle — greyed, clickable to switch */
|
||||
.reg.switch { background: transparent; color: #6b7280; border: 1px solid #ffffff1c; cursor: pointer; }
|
||||
.reg.switch:hover { color: #b9c2d0; border-color: #ffffff33; }
|
||||
.star { border: none; background: transparent; cursor: pointer; color: var(--dim); font-size: 15px; padding: 2px 4px; border-radius: 6px; }
|
||||
.star:hover { background: var(--line2); color: var(--ink); } .star.on { color: #ffd23d; }
|
||||
/* search box: engine icon (click = pick engine) + a wide typing area */
|
||||
|
|
@ -258,24 +261,26 @@
|
|||
}
|
||||
function setReg(d) {
|
||||
const r = $("reg");
|
||||
if (!d || d.kind === "home" || d.kind === "resolving") { r.hidden = true; r.innerHTML = ""; return; }
|
||||
if (!d || d.kind === "home") { r.hidden = true; r.innerHTML = ""; return; }
|
||||
// Keep chips visible during "resolving" so the switch feels like a toggle,
|
||||
// not a disappear-and-reappear (user complaint 2026-08-02).
|
||||
if (d.kind === "resolving") { r.hidden = false; return; }
|
||||
r.hidden = false;
|
||||
r.className = "reg-wrap"; // container; individual chips carry their own classes
|
||||
// Two chips when this host is a collision candidate — could be BCDN OR ICANN.
|
||||
// The active one is highlighted; the other is clickable to switch.
|
||||
// Two chips (toggle) when this host is a collision candidate — could be
|
||||
// BCDN OR ICANN. Active one is coloured; inactive is greyed + clickable.
|
||||
const isCand = !!d.tld && (d.kind === "ok" || d.kind === "web") && d.bcnrNativeTld === false;
|
||||
const active = d.kind === "ok" ? "bcdn" : d.kind === "web" ? "icann" : null;
|
||||
const bcdn = `<span class="reg${active === "bcdn" ? "" : " switch"}" data-c="bcdn" title="${active === "bcdn" ? "Open with BCDN (current)" : "Switch to BCDN"}">BCDN</span>`;
|
||||
const icann = `<span class="reg icann${active === "icann" ? "" : " switch"}" data-c="icann" title="${active === "icann" ? "Open with ICANN (current)" : "Switch to ICANN"}">ICANN</span>`;
|
||||
if (isCand) {
|
||||
const bcdn = `<span class="reg bcdn ${active === "bcdn" ? "active" : "switch"}" data-c="bcdn" title="${active === "bcdn" ? "Serving from BCDN" : "Switch to BCDN"}">BCDN</span>`;
|
||||
const icann = `<span class="reg icann ${active === "icann" ? "active" : "switch"}" data-c="icann" title="${active === "icann" ? "Serving from ICANN" : "Switch to ICANN"}">ICANN</span>`;
|
||||
r.innerHTML = bcdn + icann;
|
||||
for (const chip of r.querySelectorAll(".reg.switch")) {
|
||||
chip.onclick = () => T.collisionSwitch({ choice: chip.dataset.c, remember: "no" });
|
||||
}
|
||||
} else if (d.kind === "web") {
|
||||
r.innerHTML = `<span class="reg icann">ICANN</span>`;
|
||||
r.innerHTML = `<span class="reg icann active">ICANN</span>`;
|
||||
} else {
|
||||
r.innerHTML = `<span class="reg">BCDN</span>`;
|
||||
r.innerHTML = `<span class="reg bcdn active">BCDN</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
186
collision.html
186
collision.html
|
|
@ -7,104 +7,158 @@
|
|||
<style>
|
||||
:root {
|
||||
color-scheme: dark light;
|
||||
--bg:#0f1116; --fg:#e5e7eb; --muted:#9ca3af; --border:#2a2f3a;
|
||||
--btn:#1f2430; --btn-hover:#2a3040; --accent:#f5c518; --icann:#4c9eff;
|
||||
--bg: #0f1116;
|
||||
--fg: #e5e7eb;
|
||||
--muted: #9ca3af;
|
||||
--border: #2a2f3a;
|
||||
--panel: #131722;
|
||||
--panel-hover: #181e2b;
|
||||
--bcdn: #d6ff3d; /* acid glow green (matches address-bar toggle) */
|
||||
--icann: #4c9eff; /* blue (matches address-bar toggle) */
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root { --bg:#f7f7f8; --fg:#111827; --muted:#6b7280; --border:#e5e7eb; --btn:#fff; --btn-hover:#f0f2f5; }
|
||||
:root { --bg:#f7f7f8; --fg:#111827; --muted:#5b6472; --border:#e5e7eb; --panel:#ffffff; --panel-hover:#f0f2f5; --bcdn:#7fa900; --icann:#1f6fd4; }
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin:0; height:100%; overflow:hidden; }
|
||||
body { background:var(--bg); color:var(--fg); font:15px/1.45 system-ui,-apple-system,Segoe UI,Roboto,sans-serif; display:flex; flex-direction:column; padding:26px 34px; }
|
||||
h1 { margin:0 0 6px; font-size:18px; font-weight:600; }
|
||||
.sub { color:var(--muted); font-size:13.5px; margin-bottom:22px; }
|
||||
.host { color:var(--fg); font-weight:600; }
|
||||
.choices { display:grid; grid-template-columns:1fr 1fr; gap:14px; margin-bottom:22px; }
|
||||
.choice { border:2px solid var(--border); border-radius:10px; padding:18px; background:var(--btn); text-align:left; color:var(--fg); cursor:pointer; transition: border-color .12s, background .12s; }
|
||||
.choice:hover { background:var(--btn-hover); }
|
||||
.choice.sel.bcdn { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, var(--btn)); }
|
||||
.choice.sel.icann { border-color: var(--icann); background: color-mix(in srgb, var(--icann) 12%, var(--btn)); }
|
||||
.choice .head { display:flex; align-items:center; gap:10px; margin-bottom:8px; }
|
||||
.choice .radio { flex:none; width:16px; height:16px; border-radius:50%; border:2px solid var(--border); position:relative; }
|
||||
.choice.sel.bcdn .radio { border-color: var(--accent); background: radial-gradient(circle, var(--accent) 40%, transparent 42%); }
|
||||
.choice.sel.icann .radio { border-color: var(--icann); background: radial-gradient(circle, var(--icann) 40%, transparent 42%); }
|
||||
.choice .icon { font-size:20px; line-height:1; }
|
||||
.choice .title { font-weight:600; font-size:15px; }
|
||||
.choice.bcdn .title { color: var(--accent); }
|
||||
html, body { margin: 0; min-height: 100%; }
|
||||
body {
|
||||
background: var(--bg); color: var(--fg);
|
||||
font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
padding: 40px 24px;
|
||||
}
|
||||
main { width: 100%; max-width: 720px; }
|
||||
header { margin-bottom: 28px; }
|
||||
h1 { margin: 0 0 8px; font-size: 22px; font-weight: 600; letter-spacing: -.01em; }
|
||||
.sub { color: var(--muted); font-size: 14px; }
|
||||
.host { color: var(--fg); font-weight: 600; }
|
||||
|
||||
.choices { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 28px; }
|
||||
@media (max-width: 620px) { .choices { grid-template-columns: 1fr; } }
|
||||
|
||||
.choice {
|
||||
background: var(--panel); border: 2px solid var(--border); border-radius: 12px;
|
||||
padding: 22px; color: var(--fg); text-align: left; cursor: pointer;
|
||||
transition: border-color .12s, background .12s, transform .06s;
|
||||
display: flex; flex-direction: column; gap: 8px;
|
||||
}
|
||||
.choice:hover { background: var(--panel-hover); }
|
||||
.choice:active { transform: scale(.99); }
|
||||
.choice.sel.bcdn { border-color: var(--bcdn); box-shadow: 0 0 0 1px var(--bcdn), 0 0 22px -8px var(--bcdn); }
|
||||
.choice.sel.icann { border-color: var(--icann); box-shadow: 0 0 0 1px var(--icann), 0 0 22px -10px var(--icann); }
|
||||
|
||||
.choice .head { display: flex; align-items: center; gap: 12px; }
|
||||
.choice .radio {
|
||||
flex: none; width: 18px; height: 18px; border-radius: 50%;
|
||||
border: 2px solid var(--border); position: relative;
|
||||
}
|
||||
.choice.sel.bcdn .radio { border-color: var(--bcdn); background: radial-gradient(circle, var(--bcdn) 40%, transparent 42%); }
|
||||
.choice.sel.icann .radio { border-color: var(--icann); background: radial-gradient(circle, var(--icann) 40%, transparent 42%); }
|
||||
.choice .icon { font-size: 22px; line-height: 1; }
|
||||
.choice .title { font-weight: 600; font-size: 16px; }
|
||||
.choice.bcdn .title { color: var(--bcdn); }
|
||||
.choice.icann .title { color: var(--icann); }
|
||||
.choice .desc { font-size:13px; color:var(--muted); }
|
||||
fieldset { border:0; padding:0; margin:0 0 22px; display:flex; flex-direction:column; gap:8px; }
|
||||
fieldset legend { color:var(--muted); font-size:12.5px; padding:0; margin-bottom:6px; text-transform:uppercase; letter-spacing:.03em; }
|
||||
fieldset .row { display:flex; align-items:center; gap:9px; font-size:13.5px; padding:2px 0; }
|
||||
fieldset .row input { accent-color:var(--accent); width:15px; height:15px; }
|
||||
fieldset .row label { color:var(--fg); cursor:pointer; }
|
||||
.actions { display:flex; justify-content:flex-end; gap:10px; margin-top:auto; }
|
||||
button.act { border:1px solid var(--border); background:var(--btn); color:var(--fg); padding:10px 20px; border-radius:8px; font-size:14px; cursor:pointer; }
|
||||
button.act:hover { background: var(--btn-hover); }
|
||||
button.act.primary { background:var(--accent); color:#111; border-color:var(--accent); font-weight:600; }
|
||||
button.act.primary:hover { filter:brightness(1.05); }
|
||||
kbd { border:1px solid var(--border); border-radius:3px; padding:0 5px; font:11px monospace; color:var(--muted); margin-left:6px; }
|
||||
.choice .desc { font-size: 13.5px; color: var(--muted); }
|
||||
|
||||
fieldset { border: 0; padding: 0; margin: 0 0 28px; display: flex; flex-direction: column; gap: 10px; }
|
||||
fieldset legend {
|
||||
color: var(--muted); font-size: 12px; padding: 0; margin-bottom: 8px;
|
||||
text-transform: uppercase; letter-spacing: .06em;
|
||||
}
|
||||
fieldset .row { display: flex; align-items: center; gap: 10px; font-size: 14px; padding: 3px 0; }
|
||||
fieldset .row input { accent-color: var(--bcdn); width: 16px; height: 16px; }
|
||||
fieldset .row label { color: var(--fg); cursor: pointer; }
|
||||
|
||||
.actions { display: flex; justify-content: flex-end; gap: 12px; }
|
||||
button.act {
|
||||
border: 1px solid var(--border); background: var(--panel); color: var(--fg);
|
||||
padding: 11px 22px; border-radius: 8px; font-size: 14px; cursor: pointer;
|
||||
transition: background .1s, filter .1s;
|
||||
}
|
||||
button.act:hover { background: var(--panel-hover); }
|
||||
button.act.primary { background: var(--bcdn); color: #111; border-color: var(--bcdn); font-weight: 600; }
|
||||
button.act.primary:hover { filter: brightness(1.06); }
|
||||
kbd { border: 1px solid var(--border); border-radius: 3px; padding: 0 5px; font: 11px monospace; color: var(--muted); margin-left: 6px; }
|
||||
</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>
|
||||
<main>
|
||||
<header>
|
||||
<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>
|
||||
</header>
|
||||
|
||||
<div class="choices" id="choices">
|
||||
<div class="choice bcdn sel" data-choice="bcdn" tabindex="0">
|
||||
<div class="head">
|
||||
<span class="radio"></span><span class="icon">⛓</span>
|
||||
<span class="title">BCNR / BCDN</span>
|
||||
<div class="choices">
|
||||
<div class="choice bcdn sel" data-choice="bcnr" tabindex="0">
|
||||
<div class="head">
|
||||
<span class="radio"></span><span class="icon">⛓</span>
|
||||
<span class="title">BCNR / BCDN</span>
|
||||
</div>
|
||||
<div class="desc">On-chain registration. Owned by the holder of a digital name certificate. The registry served by the blockchain root.</div>
|
||||
</div>
|
||||
<div class="desc">On-chain registration. Owned by the holder of a digital name certificate. The registry served by the blockchain root.</div>
|
||||
</div>
|
||||
<div class="choice icann" data-choice="icann" tabindex="0">
|
||||
<div class="head">
|
||||
<span class="radio"></span><span class="icon">🌐</span>
|
||||
<span class="title">ICANN / IANA</span>
|
||||
<div class="choice icann" data-choice="icann" tabindex="0">
|
||||
<div class="head">
|
||||
<span class="radio"></span><span class="icon">🌐</span>
|
||||
<span class="title">ICANN / IANA</span>
|
||||
</div>
|
||||
<div class="desc">DNS service coordinated by the ICANN. The registry served by the incumbent root.</div>
|
||||
</div>
|
||||
<div class="desc">DNS service coordinated by the ICANN. The registry served by the incumbent root.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend>Remember this choice</legend>
|
||||
<div class="row"><input type="radio" name="remember" id="rem-no" value="no" checked><label for="rem-no">Ask again next time</label></div>
|
||||
<div class="row"><input type="radio" name="remember" id="rem-name" value="name"><label for="rem-name">Always use 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 for all <span id="tld"></span> names</label></div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Remember this choice</legend>
|
||||
<div class="row"><input type="radio" name="remember" id="rem-no" value="no" checked><label for="rem-no">Ask again next time</label></div>
|
||||
<div class="row"><input type="radio" name="remember" id="rem-name" value="name"><label for="rem-name">Always use 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 for all <span id="tld"></span> names</label></div>
|
||||
</fieldset>
|
||||
|
||||
<div class="actions">
|
||||
<button class="act" id="cancel">Cancel<kbd>Esc</kbd></button>
|
||||
<button class="act primary" id="confirm">Open<kbd>Enter</kbd></button>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="act" id="cancel">Cancel<kbd>Esc</kbd></button>
|
||||
<button class="act primary" id="confirm">Open<kbd>Enter</kbd></button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const qs = new URLSearchParams(location.search);
|
||||
const host = qs.get("host") || ""; const tld = qs.get("tld") || "";
|
||||
document.getElementById("host").textContent = host;
|
||||
const host = qs.get("host") || "";
|
||||
const tld = qs.get("tld") || "";
|
||||
const resturl = qs.get("resturl") || "/";
|
||||
document.getElementById("host").textContent = host;
|
||||
document.getElementById("host2").textContent = host;
|
||||
document.getElementById("tld").textContent = "." + tld;
|
||||
document.getElementById("tld").textContent = "." + tld;
|
||||
document.title = "Open " + host + " with…";
|
||||
|
||||
let choice = "bcdn"; // default preselected
|
||||
const bcdnEl = document.querySelector('.choice[data-choice="bcdn"]');
|
||||
let choice = "bcnr";
|
||||
const bcdnEl = document.querySelector('.choice[data-choice="bcnr"]');
|
||||
const icannEl = document.querySelector('.choice[data-choice="icann"]');
|
||||
function select(c) {
|
||||
choice = c;
|
||||
bcdnEl.classList.toggle("sel", c === "bcdn");
|
||||
bcdnEl.classList.toggle("sel", c === "bcnr");
|
||||
icannEl.classList.toggle("sel", c === "icann");
|
||||
// primary button colour follows the selected choice
|
||||
const p = document.getElementById("confirm");
|
||||
p.style.background = c === "bcnr"
|
||||
? "var(--bcdn)"
|
||||
: "var(--icann)";
|
||||
p.style.borderColor = p.style.background;
|
||||
p.style.color = c === "bcnr" ? "#111" : "#fff";
|
||||
}
|
||||
select("bcnr"); // initial paint
|
||||
for (const el of [bcdnEl, icannEl]) {
|
||||
el.addEventListener("click", () => select(el.dataset.choice));
|
||||
el.addEventListener("keydown", (e) => { if (e.key === " " || e.key === "Enter") { e.preventDefault(); select(el.dataset.choice); } });
|
||||
}
|
||||
function remember() { return document.querySelector('input[name="remember"]:checked')?.value || "no"; }
|
||||
function confirm() {
|
||||
// Map "bcdn" (user-facing) -> "bcnr" (internal key used by main.js/collisions.json)
|
||||
window.collisionApi.choose(host, { choice: choice === "bcdn" ? "bcnr" : "icann", remember: remember() });
|
||||
const q = new URLSearchParams({ host, tld, choice, remember: remember(), resturl });
|
||||
// Navigate to the special bns:// endpoint that main.js's serveBns intercepts:
|
||||
// it applies the remember choice and redirects to the real target.
|
||||
location.href = "bns://collision-choose/?" + q.toString();
|
||||
}
|
||||
function cancel() {
|
||||
if (history.length > 1) history.back();
|
||||
else location.href = "about:blank";
|
||||
}
|
||||
function cancel() { window.collisionApi.choose(host, { choice: "cancel", remember: "no" }); }
|
||||
document.getElementById("confirm").addEventListener("click", confirm);
|
||||
document.getElementById("cancel").addEventListener("click", cancel);
|
||||
document.addEventListener("keydown", (e) => {
|
||||
|
|
|
|||
101
main.js
101
main.js
|
|
@ -37,7 +37,9 @@ const SEARCH_ENGINES = {
|
|||
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) },
|
||||
// SearXNG is federated (dozens of public instances at searx.space); any single
|
||||
// default becomes stale as instances rate-limit / die (searx.be is anti-bot-locked).
|
||||
// Users who want SearXNG add their preferred instance via the custom URL form.
|
||||
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
|
||||
|
|
@ -207,25 +209,9 @@ function refreshBcnrTlds(index) {
|
|||
} catch {}
|
||||
}
|
||||
|
||||
// One-shot modal "Open with…" prompt window. Resolves with the user's pick.
|
||||
let collisionWin = null;
|
||||
function collisionPromptOnce({ host, tld }) {
|
||||
return new Promise((resolve) => {
|
||||
if (collisionWin && !collisionWin.isDestroyed()) { try { collisionWin.close(); } catch {} }
|
||||
const parent = BrowserWindow.getFocusedWindow() || win;
|
||||
collisionWin = new BrowserWindow({
|
||||
parent, modal: true, width: 640, height: 520, resizable: false, minimizable: false,
|
||||
maximizable: false, autoHideMenuBar: true, title: "Open with…", show: false,
|
||||
webPreferences: { preload: path.join(__dirname, "collision-preload.js"), sandbox: true, contextIsolation: true },
|
||||
});
|
||||
collisionWin.loadFile(path.join(__dirname, "collision.html"), { query: { host, tld } });
|
||||
collisionWin.once("ready-to-show", () => collisionWin.show());
|
||||
let done = false;
|
||||
const finish = (payload) => { if (done) return; done = true; try { collisionWin?.close(); } catch {} resolve(payload); };
|
||||
ipcMain.once(`collision-choose:${host}`, (_e, payload) => finish(payload));
|
||||
collisionWin.on("closed", () => { finish({ choice: "cancel", remember: "no" }); collisionWin = null; });
|
||||
});
|
||||
}
|
||||
// (The old modal-based collisionPromptOnce() was removed 2026-08-02 — the
|
||||
// prompt is now an in-tab full-page interstitial loaded from collision.html,
|
||||
// wired via the bns://collision-choose/ handler in serveBns().)
|
||||
function rememberCollision(host, tld, choice, remember) {
|
||||
if (choice !== "bcnr" && choice !== "icann") return;
|
||||
if (remember === "name") collisions.byName[String(host).toLowerCase()] = choice;
|
||||
|
|
@ -545,6 +531,36 @@ async function serveBns(request) {
|
|||
const url = new URL(request.url);
|
||||
const host = url.hostname.toLowerCase();
|
||||
const reqPath = decodeURIComponent(url.pathname) || "/";
|
||||
|
||||
// Special host: apply a soft-mode "Open with…" choice submitted by
|
||||
// collision.html, then redirect the tab to the actual target. Persistent
|
||||
// memory (byName/byTld) is written HERE; the one-shot BCDN choice rides
|
||||
// along via a ?_collision=bcnr param that loadBns consumes.
|
||||
if (host === "collision-choose") {
|
||||
const p = url.searchParams;
|
||||
const target = String(p.get("host") || "").toLowerCase();
|
||||
const cTld = String(p.get("tld") || "").toLowerCase();
|
||||
const cChoice = p.get("choice");
|
||||
const cRem = p.get("remember") || "no";
|
||||
const rest = p.get("resturl") || "/";
|
||||
if (cChoice === "bcnr" || cChoice === "icann") {
|
||||
rememberCollision(target, cTld, cChoice, cRem);
|
||||
}
|
||||
let dest;
|
||||
if (cChoice === "icann") {
|
||||
dest = "https://" + target + rest;
|
||||
} else {
|
||||
// BCNR/BCDN: bounce back through the bns:// resolver with a one-shot
|
||||
// marker so loadBns won't re-prompt for this specific load.
|
||||
const sep = rest.includes("?") ? "&" : "?";
|
||||
dest = "bns://" + target + rest + sep + "_collision=bcnr";
|
||||
}
|
||||
const html = `<!doctype html><meta charset="utf-8"><title>Opening…</title>` +
|
||||
`<meta http-equiv="refresh" content="0;url=${dest.replace(/"/g, """)}">` +
|
||||
`<body style="background:#0f1116;color:#9ca3af;font:14px system-ui;padding:24px">Opening ${target}…</body>`;
|
||||
return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } });
|
||||
}
|
||||
|
||||
let rec = entries.get(host);
|
||||
if (!rec) { try { await resolveHost(host); } catch {} rec = entries.get(host); }
|
||||
if (!rec) return new Response("NXDOMAIN: " + host, { status: 404, headers: { "content-type": "text/plain" } });
|
||||
|
|
@ -890,6 +906,18 @@ async function loadBns(t, id, host, rest, tld) {
|
|||
if (id === activeId) pushNav(t.prov);
|
||||
emitTabs();
|
||||
};
|
||||
// Strip and capture the one-shot ?_collision=bcnr param that collision-choose
|
||||
// adds when redirecting back after the user picked BCDN in soft mode. Ignored
|
||||
// (harmless) if absent.
|
||||
let urlChoice = null;
|
||||
try {
|
||||
const u = new URL(rest, `bns://${host}/`);
|
||||
if (u.searchParams.has("_collision")) {
|
||||
urlChoice = u.searchParams.get("_collision");
|
||||
u.searchParams.delete("_collision");
|
||||
rest = u.pathname + (u.search ? u.search : "");
|
||||
}
|
||||
} catch {}
|
||||
let entry;
|
||||
try { entry = await resolveHost(host); }
|
||||
catch { setLoading(t, false); return fallbackToWeb("BCNR unreachable"); }
|
||||
|
|
@ -902,17 +930,24 @@ async function loadBns(t, id, host, rest, tld) {
|
|||
// *might* also exist on ICANN; the policy decides which to load.
|
||||
// Full model: SilentMode/Argus/DESIGN-collision-modes.md.
|
||||
if (!isBcnrNativeTld(tld)) {
|
||||
// Transient per-tab override (from the chip switcher) — one-shot, consumed here.
|
||||
const transient = t.collisionOverride; t.collisionOverride = null;
|
||||
const policy = settings.collisionPolicy || "bcnr-first";
|
||||
let choice = overrideFor(host, tld); // per-name > per-TLD > null
|
||||
// Precedence: urlChoice (one-shot from collision-choose) > transient (chip
|
||||
// switcher) > persistent per-name/per-TLD > global policy.
|
||||
let choice = urlChoice || transient || overrideFor(host, tld);
|
||||
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);
|
||||
// In-tab full-page "Open with…" prompt (loaded from disk; buttons post
|
||||
// back through bns://collision-choose/ which serveBns handles above).
|
||||
setLoading(t, false);
|
||||
const q = new URLSearchParams({ host, tld, resturl: rest }).toString();
|
||||
await t.view.webContents.loadFile(path.join(__dirname, "collision.html"), { search: q });
|
||||
t.prov = { host, kind: "resolving", tld, registry };
|
||||
if (id === activeId) pushNav(t.prov);
|
||||
emitTabs();
|
||||
return;
|
||||
} else choice = "bcnr"; // bcnr-first — the default
|
||||
}
|
||||
if (choice === "icann") { setLoading(t, false); return fallbackToWeb("collision → ICANN"); }
|
||||
|
|
@ -951,9 +986,15 @@ ipcMain.handle("popover-resize", (_e, h) => { popH = Math.max(90, Math.min(380,
|
|||
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
|
||||
// Accept both "bcdn" (client-facing product name) and "bcnr" (internal key).
|
||||
const raw = arg?.choice;
|
||||
const choice = (raw === "bcdn" || raw === "bcnr") ? "bcnr"
|
||||
: (raw === "icann") ? "icann"
|
||||
: (t.prov.kind === "ok" ? "icann" : "bcnr"); // flip the current one
|
||||
// Transient per-tab override consumed once by loadBns — no soft-mode re-prompt
|
||||
// for this specific chip-triggered switch.
|
||||
t.collisionOverride = choice;
|
||||
// Optional persistent memory ("Always use…" from the popover switcher).
|
||||
rememberCollision(host, tld, choice, arg?.remember || "no");
|
||||
return navigateTab(t.id, host + (t.prov.path || "/"));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -197,10 +197,10 @@
|
|||
<!-- NAMING -->
|
||||
<section id="naming" hidden>
|
||||
<h1>Registries</h1>
|
||||
<p class="lede">How Theseus picks between <b>BCDN</b> (Bitcoin Cash Domain Names, on-chain) and <b>ICANN</b> (traditional DNS) when a name exists in both registries.</p>
|
||||
<p class="lede">How Theseus picks between the <b>BCNR / BCDN</b> and <b>ICANN / IANA</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 collision only happens when a BCDN name is registered under a TLD that ICANN also serves (e.g. <code>.de</code>). BCNR-native TLDs (<code>.bch</code>, <code>.p2p</code>, …) never conflict — they only exist on BCDN.</div></div>
|
||||
<div class="d">A name only exists in both registries when its TLD isn't BCNR-unique (e.g. <code>.de</code>). BCNR-unique TLDs (that only exist on BCNR) 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>BCDN first</b> <span class="pmuted">— BCDN wins conflicts; falls back to ICANN for anything BCDN doesn't have.</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; BCDN fills gaps for non-ICANN TLDs.</span></span></label>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue