Settings has a new "Allow weak passwords" checkbox, off in a new profile. While it is off, AddWallet refuses a password shorter than 12 characters, as before. While it is on, AddWallet accepts any password that is not empty. Key derivation is unchanged: one Argon2id derivation at libsodium's interactive setting. README's Key Decisions now states those parameters and why one light derivation is enough: the password's length is what protects the stored secret. The screens say "password", as README's wording rules require, so the setting is "Allow weak passwords". Model: opus-5-5
588 lines
22 KiB
JavaScript
588 lines
22 KiB
JavaScript
const {
|
||
$,
|
||
showView,
|
||
updateDebugBanner,
|
||
showFlash,
|
||
escapeHtml,
|
||
displaySymbol,
|
||
flashCopyFeedback,
|
||
goBack,
|
||
pushCurrentView,
|
||
} = require("./helpers");
|
||
const { applyTheme } = require("../theme");
|
||
const {
|
||
DUST_THRESHOLD_MESSAGE,
|
||
parseDustThresholdGwei,
|
||
} = require("../dustThreshold");
|
||
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||
const { onChainSwitch } = require("../../shared/chainSwitch");
|
||
const { restorableStack } = require("../../shared/persistedState");
|
||
const { dropUntrackedTokens } = require("../../shared/balances");
|
||
const { resetRenderedViews, markViewRendered } = require("../viewRouter");
|
||
const {
|
||
log,
|
||
debugFetch,
|
||
urlOrigin,
|
||
setRuntimeDebug,
|
||
} = require("../../shared/log");
|
||
const deleteWallet = require("./deleteWallet");
|
||
const showPhrase = require("./showPhrase");
|
||
const { walletHasRecoveryPhrase } = require("../../shared/wallet");
|
||
const {
|
||
BUILD_VERSION,
|
||
BUILD_LICENSE,
|
||
BUILD_AUTHOR,
|
||
BUILD_COMMIT,
|
||
BUILD_DATE,
|
||
GITEA_COMMIT_URL,
|
||
} = require("../../shared/buildInfo");
|
||
|
||
const { notify, sendMessage } = require("../../shared/browserApi");
|
||
|
||
let ctx;
|
||
|
||
let versionClickCount = 0;
|
||
let versionClickTimer = null;
|
||
|
||
// One row per site origin, however many addresses it appears under, each with
|
||
// an [x] that hands it to onRemove.
|
||
function renderSiteList(containerId, origins, onRemove) {
|
||
const container = $(containerId);
|
||
const unique = [...new Set(origins)];
|
||
if (unique.length === 0) {
|
||
container.innerHTML = '<p class="text-xs text-muted">None</p>';
|
||
return;
|
||
}
|
||
let html = "";
|
||
unique.forEach((origin) => {
|
||
html += `<div class="flex justify-between items-center text-xs py-1 border-b border-border-light">`;
|
||
// An origin the URL parser produced cannot carry a delimiter, so
|
||
// this is escaped for the rule rather than for a known hole — the
|
||
// rule being that nothing reaches innerHTML unescaped.
|
||
html += `<span>${escapeHtml(origin)}</span>`;
|
||
html += `<button class="btn-remove-site border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer" data-origin="${escapeHtml(origin)}">[x]</button>`;
|
||
html += `</div>`;
|
||
});
|
||
container.innerHTML = html;
|
||
container.querySelectorAll(".btn-remove-site").forEach((btn) => {
|
||
btn.addEventListener("click", () => onRemove(btn.dataset.origin));
|
||
});
|
||
}
|
||
|
||
// Drop a site origin from a remembered site list under every address.
|
||
function forgetOrigin(siteMap, origin) {
|
||
for (const addr of Object.keys(siteMap)) {
|
||
siteMap[addr] = siteMap[addr].filter((o) => o !== origin);
|
||
if (siteMap[addr].length === 0) {
|
||
delete siteMap[addr];
|
||
}
|
||
}
|
||
}
|
||
|
||
// Removing a site from Allowed Sites or Connected Sites disconnects it: it is
|
||
// no longer allowed under any address, and the background ends its
|
||
// connections approved without "Remember" and tells its open tabs. The
|
||
// background answers once those have ended, so the lists drawn after it no
|
||
// longer show the site.
|
||
async function removeAllowedSite(origin) {
|
||
forgetOrigin(state.allowedSites, origin);
|
||
await saveState();
|
||
await sendMessage({ type: "AUTISTMASK_REMOVE_SITE", origin });
|
||
await renderSiteLists();
|
||
}
|
||
|
||
// Removing a denied site only forgets the refusal; it connects nothing.
|
||
async function removeDeniedSite(origin) {
|
||
forgetOrigin(state.deniedSites, origin);
|
||
await saveState();
|
||
await renderSiteLists();
|
||
}
|
||
|
||
function renderTrackedTokens() {
|
||
const container = $("settings-tracked-tokens");
|
||
if (state.trackedTokens.length === 0) {
|
||
container.innerHTML = '<p class="text-xs text-muted">None</p>';
|
||
return;
|
||
}
|
||
let html = "";
|
||
state.trackedTokens.forEach((token, idx) => {
|
||
const sym = escapeHtml(displaySymbol(token.symbol));
|
||
const label = token.name
|
||
? escapeHtml(token.name) + " (" + sym + ")"
|
||
: sym;
|
||
html += `<div class="flex justify-between items-center text-xs py-1 border-b border-border-light">`;
|
||
html += `<span>${label}</span>`;
|
||
html += `<button class="btn-remove-token border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer" data-idx="${idx}">[x]</button>`;
|
||
html += `</div>`;
|
||
});
|
||
container.innerHTML = html;
|
||
container.querySelectorAll(".btn-remove-token").forEach((btn) => {
|
||
btn.addEventListener("click", async () => {
|
||
const idx = parseInt(btn.dataset.idx, 10);
|
||
state.trackedTokens.splice(idx, 1);
|
||
await saveState();
|
||
renderTrackedTokens();
|
||
});
|
||
});
|
||
}
|
||
|
||
function renderWalletListSettings() {
|
||
const container = $("settings-wallet-list");
|
||
if (state.wallets.length === 0) {
|
||
container.innerHTML = '<p class="text-xs text-muted">No wallets.</p>';
|
||
return;
|
||
}
|
||
let html = "";
|
||
state.wallets.forEach((wallet, idx) => {
|
||
const name = escapeHtml(wallet.name || "Wallet " + (idx + 1));
|
||
html += `<div class="flex justify-between items-center text-xs py-1 border-b border-border-light">`;
|
||
html += `<span class="settings-wallet-name cursor-pointer underline decoration-dashed" data-idx="${idx}">${name}</span>`;
|
||
html += `<span class="flex items-center gap-1 flex-shrink-0">`;
|
||
// Key and xprv wallets have no recovery phrase, so they are never
|
||
// offered the action at all.
|
||
if (walletHasRecoveryPhrase(wallet)) {
|
||
html += `<button class="btn-show-phrase border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer" data-idx="${idx}" title="Show recovery phrase">[recovery phrase]</button>`;
|
||
}
|
||
html += `<button class="btn-delete-wallet border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer" data-idx="${idx}">[x]</button>`;
|
||
html += `</span>`;
|
||
html += `</div>`;
|
||
});
|
||
container.innerHTML = html;
|
||
container.querySelectorAll(".btn-delete-wallet").forEach((btn) => {
|
||
btn.addEventListener("click", () => {
|
||
const idx = parseInt(btn.dataset.idx, 10);
|
||
pushCurrentView();
|
||
deleteWallet.show(idx);
|
||
});
|
||
});
|
||
|
||
container.querySelectorAll(".btn-show-phrase").forEach((btn) => {
|
||
btn.addEventListener("click", () => {
|
||
const idx = parseInt(btn.dataset.idx, 10);
|
||
// No pushCurrentView() here: showPhrase.show() refuses
|
||
// non-HD wallets and pushes only when it navigates.
|
||
showPhrase.show(idx);
|
||
});
|
||
});
|
||
|
||
// Inline rename on click
|
||
container.querySelectorAll(".settings-wallet-name").forEach((span) => {
|
||
span.addEventListener("click", () => {
|
||
const idx = parseInt(span.dataset.idx, 10);
|
||
const wallet = state.wallets[idx];
|
||
const input = document.createElement("input");
|
||
input.type = "text";
|
||
input.className =
|
||
"border border-border p-0 text-xs bg-bg text-fg w-full";
|
||
input.value = wallet.name || "Wallet " + (idx + 1);
|
||
span.replaceWith(input);
|
||
input.focus();
|
||
input.select();
|
||
const finish = async () => {
|
||
const val = input.value.trim();
|
||
if (val && val !== wallet.name) {
|
||
wallet.name = val;
|
||
await saveState();
|
||
}
|
||
renderWalletListSettings();
|
||
};
|
||
input.addEventListener("blur", finish);
|
||
input.addEventListener("keydown", (e) => {
|
||
if (e.key === "Enter") input.blur();
|
||
if (e.key === "Escape") {
|
||
input.value = wallet.name || "Wallet " + (idx + 1);
|
||
input.blur();
|
||
}
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
function show() {
|
||
$("settings-rpc").value = state.rpcUrl;
|
||
$("settings-blockscout").value = state.blockscoutUrl;
|
||
$("settings-network").value = state.networkId;
|
||
// Set on every show, not only at init: Welcome can change these two.
|
||
$("settings-use-blockscout").checked = state.useBlockscout;
|
||
$("settings-check-recipients-on-etherscan").checked =
|
||
state.checkRecipientsOnEtherscan;
|
||
renderTrackedTokens();
|
||
renderSiteLists();
|
||
renderWalletListSettings();
|
||
|
||
// Populate About well
|
||
$("about-license").textContent = BUILD_LICENSE;
|
||
// Show only the name part of the author field (strip email)
|
||
const authorName = BUILD_AUTHOR.replace(/\s*<[^>]+>/, "");
|
||
$("about-author").textContent = authorName;
|
||
$("about-version").textContent = BUILD_VERSION;
|
||
$("about-release-date").textContent = BUILD_DATE;
|
||
$("about-commit-link").textContent = BUILD_COMMIT;
|
||
$("about-commit-link").href = GITEA_COMMIT_URL;
|
||
|
||
// Reset version click counter each time settings opens
|
||
versionClickCount = 0;
|
||
|
||
// Show debug well if debug mode is already enabled
|
||
$("settings-debug-well").classList.toggle("hidden", !state.debugMode);
|
||
$("settings-debug-mode").checked = state.debugMode;
|
||
|
||
showView("settings");
|
||
}
|
||
|
||
async function renderSiteLists() {
|
||
renderSiteList(
|
||
"settings-allowed-sites",
|
||
Object.values(state.allowedSites).flat(),
|
||
removeAllowedSite,
|
||
);
|
||
renderSiteList(
|
||
"settings-denied-sites",
|
||
Object.values(state.deniedSites).flat(),
|
||
removeDeniedSite,
|
||
);
|
||
// Sites allowed without "Remember" are held only in session storage, which
|
||
// the background keeps, so it is asked for them.
|
||
renderSiteList(
|
||
"settings-connected-sites",
|
||
await sendMessage({ type: "AUTISTMASK_GET_CONNECTED_SITES" }),
|
||
removeAllowedSite,
|
||
);
|
||
}
|
||
|
||
// A network switch, made by the user here or saved by the background once the
|
||
// user approved a site's request (src/popup/index.js), leaves every screen on
|
||
// the page showing the previous network. Two functions leave the page as
|
||
// README's Screen Map describes:
|
||
//
|
||
// - cutViewStack(), before the switch is saved, so that save keeps the cut for
|
||
// a popup closed straight after
|
||
// - showNewSettings(), once `state` holds the new network
|
||
//
|
||
// The stack is cut at the first screen that cannot be rendered from saved
|
||
// state, such as Send or AddToken, and at AddressToken unless it shows the
|
||
// native token: any other token there was picked on the previous network, so
|
||
// it is no longer selected either. Without a wallet the stack starts at
|
||
// Welcome, which that cut would drop, and Welcome shows nothing from a network.
|
||
// A change to "Use Blockscout" cuts the stack the same way (setUseBlockscout()
|
||
// below).
|
||
function cutViewStack() {
|
||
if (!state.hasWallet) return;
|
||
if (state.selectedToken !== "ETH") {
|
||
const tokenView = state.viewStack.indexOf("address-token");
|
||
if (tokenView !== -1) {
|
||
state.viewStack = state.viewStack.slice(0, tokenView);
|
||
}
|
||
state.selectedToken = null;
|
||
}
|
||
state.viewStack = restorableStack(state.viewStack, state.currentView);
|
||
}
|
||
|
||
// Sets "Use Blockscout" to `on` on this page. It is called inside a save,
|
||
// holding the lock, for the user's change here or another page's
|
||
// (followStoredSettings() in src/popup/index.js).
|
||
//
|
||
// Send's token dropdown was filled under the old setting, and Back cannot draw
|
||
// Send again. So the stack is cut, as for a network switch. Turning the setting
|
||
// off also drops every token the user has not tracked.
|
||
function setUseBlockscout(on) {
|
||
state.useBlockscout = on;
|
||
cutViewStack();
|
||
if (!on) dropUntrackedTokens(state.wallets, state.trackedTokens);
|
||
}
|
||
|
||
// Draws the page again after a network switch, or after another page changed
|
||
// "Use Blockscout" or "Check recipients on Etherscan". Every screen counts as
|
||
// not yet rendered, so "Back" renders again any screen that a reopened popup
|
||
// can render. Settings on top is brought up to date in place. Any other screen
|
||
// is drawn again as a reopened popup draws it, which is Home for one that
|
||
// cannot be rendered from saved state. Without a wallet any other screen is
|
||
// Welcome or AddWallet. Back only unhides Welcome, so its two checkboxes are
|
||
// set here.
|
||
function showNewSettings() {
|
||
resetRenderedViews();
|
||
$("welcome-use-blockscout").checked = state.useBlockscout;
|
||
$("welcome-check-recipients-on-etherscan").checked =
|
||
state.checkRecipientsOnEtherscan;
|
||
if (state.currentView === "settings") {
|
||
markViewRendered("settings");
|
||
$("settings-network").value = state.networkId;
|
||
$("settings-rpc").value = state.rpcUrl;
|
||
$("settings-blockscout").value = state.blockscoutUrl;
|
||
$("settings-use-blockscout").checked = state.useBlockscout;
|
||
$("settings-check-recipients-on-etherscan").checked =
|
||
state.checkRecipientsOnEtherscan;
|
||
renderTrackedTokens();
|
||
} else if (state.hasWallet) {
|
||
ctx.restoreView();
|
||
}
|
||
updateDebugBanner(state.currentView);
|
||
}
|
||
|
||
function init(_ctx) {
|
||
ctx = _ctx;
|
||
deleteWallet.init(ctx);
|
||
showPhrase.init();
|
||
|
||
$("btn-save-rpc").addEventListener("click", async () => {
|
||
const url = $("settings-rpc").value.trim();
|
||
if (!url) {
|
||
showFlash("Please enter an RPC URL.");
|
||
return;
|
||
}
|
||
showFlash("Testing endpoint...");
|
||
try {
|
||
const resp = await debugFetch(url, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
jsonrpc: "2.0",
|
||
id: 1,
|
||
method: "eth_chainId",
|
||
params: [],
|
||
}),
|
||
});
|
||
const json = await resp.json();
|
||
if (json.error) {
|
||
log.errorf("RPC validation error:", json.error);
|
||
showFlash("Endpoint returned an error.");
|
||
return;
|
||
}
|
||
const net = currentNetwork();
|
||
if (json.result !== net.chainId) {
|
||
showFlash("Wrong network: expected " + net.name + ".");
|
||
return;
|
||
}
|
||
} catch {
|
||
// Cancelled by the popup closing, not failed: see pageClosed in
|
||
// src/popup/index.js.
|
||
if (ctx.pageClosed.aborted) return;
|
||
// Not the error's message: fetch puts the whole URL, password and
|
||
// key included, in the message of the error it throws for a URL
|
||
// with a user name and password or one it cannot parse.
|
||
log.errorf("RPC validation fetch failed:", urlOrigin(url));
|
||
showFlash("Could not reach endpoint.");
|
||
return;
|
||
}
|
||
state.rpcUrl = url;
|
||
await saveState();
|
||
showFlash("Saved.");
|
||
});
|
||
|
||
$("btn-save-blockscout").addEventListener("click", async () => {
|
||
const url = $("settings-blockscout").value.trim();
|
||
if (!url) {
|
||
showFlash("Please enter a Blockscout API URL.");
|
||
return;
|
||
}
|
||
showFlash("Testing endpoint...");
|
||
try {
|
||
const resp = await debugFetch(url + "/stats");
|
||
if (!resp.ok) {
|
||
showFlash("Endpoint returned HTTP " + resp.status + ".");
|
||
return;
|
||
}
|
||
} catch {
|
||
if (ctx.pageClosed.aborted) return;
|
||
// Not the error's message, as for the RPC check above.
|
||
log.errorf("Blockscout validation failed:", urlOrigin(url));
|
||
showFlash("Could not reach endpoint.");
|
||
return;
|
||
}
|
||
state.blockscoutUrl = url;
|
||
await saveState();
|
||
showFlash("Saved.");
|
||
});
|
||
|
||
// Each change is made inside the save, holding the lock. So
|
||
// followStoredSettings() in src/popup/index.js never mistakes this page's
|
||
// unsaved change for one that another page saved.
|
||
//
|
||
// The screens under Settings were drawn under the old setting. So each
|
||
// change makes every screen but Settings count as not yet drawn, as a
|
||
// network switch does. Back then draws again any screen that a reopened
|
||
// popup can draw. Back only unhides Welcome, so its boxes are set here too.
|
||
$("settings-use-blockscout").addEventListener("change", async () => {
|
||
const on = $("settings-use-blockscout").checked;
|
||
await saveState(() => setUseBlockscout(on));
|
||
$("welcome-use-blockscout").checked = on;
|
||
resetRenderedViews();
|
||
markViewRendered("settings");
|
||
});
|
||
|
||
$("settings-check-recipients-on-etherscan").addEventListener(
|
||
"change",
|
||
async () => {
|
||
const on = $("settings-check-recipients-on-etherscan").checked;
|
||
await saveState(() => {
|
||
state.checkRecipientsOnEtherscan = on;
|
||
});
|
||
$("welcome-check-recipients-on-etherscan").checked = on;
|
||
resetRenderedViews();
|
||
markViewRendered("settings");
|
||
},
|
||
);
|
||
|
||
const networkSelect = $("settings-network");
|
||
networkSelect.addEventListener("change", async () => {
|
||
// Whether `state` already has the network picked is asked only once
|
||
// the switch holds the lock: while an earlier pick waits for it,
|
||
// `state` still has the network before that pick, and a pick back to
|
||
// that network must still be made.
|
||
const newId = networkSelect.value;
|
||
cutViewStack();
|
||
const net = await onChainSwitch(newId);
|
||
if (!net) return;
|
||
// Open pages are told by the background, as after a site's approved
|
||
// switch request.
|
||
notify({ type: "AUTISTMASK_NETWORK_CHANGED", chainId: net.chainId });
|
||
showNewSettings();
|
||
showFlash("Switched to " + net.name + ".");
|
||
});
|
||
|
||
$("settings-show-zero-balances").checked = state.showZeroBalanceTokens;
|
||
$("settings-show-zero-balances").addEventListener("change", async () => {
|
||
state.showZeroBalanceTokens = $("settings-show-zero-balances").checked;
|
||
await saveState();
|
||
});
|
||
|
||
$("settings-theme").value = state.theme;
|
||
$("settings-theme").addEventListener("change", async () => {
|
||
state.theme = $("settings-theme").value;
|
||
await saveState();
|
||
applyTheme(state.theme);
|
||
});
|
||
|
||
$("settings-hide-spoofed-symbols").checked = state.hideSpoofedSymbols;
|
||
$("settings-hide-spoofed-symbols").addEventListener("change", async () => {
|
||
state.hideSpoofedSymbols = $("settings-hide-spoofed-symbols").checked;
|
||
await saveState();
|
||
});
|
||
|
||
$("settings-hide-low-holders").checked = state.hideLowHolderTokens;
|
||
$("settings-hide-low-holders").addEventListener("change", async () => {
|
||
state.hideLowHolderTokens = $("settings-hide-low-holders").checked;
|
||
await saveState();
|
||
});
|
||
|
||
$("settings-hide-fraud-contracts").checked = state.hideFraudContracts;
|
||
$("settings-hide-fraud-contracts").addEventListener("change", async () => {
|
||
state.hideFraudContracts = $("settings-hide-fraud-contracts").checked;
|
||
await saveState();
|
||
});
|
||
|
||
$("settings-hide-dust").checked = state.hideDustTransactions;
|
||
$("settings-hide-dust").addEventListener("change", async () => {
|
||
state.hideDustTransactions = $("settings-hide-dust").checked;
|
||
await saveState();
|
||
});
|
||
|
||
$("settings-dust-threshold").value = state.dustThresholdGwei;
|
||
$("settings-dust-threshold").addEventListener("change", async () => {
|
||
const val = parseDustThresholdGwei($("settings-dust-threshold").value);
|
||
// Rejected input is never coerced. The field is put back to the
|
||
// stored threshold so it never shows a value the wallet is not
|
||
// using, and the message says what the field wants so the snap-back
|
||
// is explained rather than silent.
|
||
if (val === null) {
|
||
showFlash(DUST_THRESHOLD_MESSAGE);
|
||
} else {
|
||
state.dustThresholdGwei = val;
|
||
await saveState();
|
||
}
|
||
$("settings-dust-threshold").value = state.dustThresholdGwei;
|
||
});
|
||
|
||
$("settings-utc-timestamps").checked = state.utcTimestamps;
|
||
$("settings-utc-timestamps").addEventListener("change", async () => {
|
||
state.utcTimestamps = $("settings-utc-timestamps").checked;
|
||
await saveState();
|
||
});
|
||
|
||
$("settings-allow-weak-passwords").checked = state.allowWeakPasswords;
|
||
$("settings-allow-weak-passwords").addEventListener("change", async () => {
|
||
state.allowWeakPasswords = $("settings-allow-weak-passwords").checked;
|
||
await saveState();
|
||
});
|
||
|
||
$("btn-main-add-wallet").addEventListener("click", ctx.showAddWalletView);
|
||
|
||
$("btn-settings-add-token").addEventListener(
|
||
"click",
|
||
ctx.showSettingsAddTokenView,
|
||
);
|
||
|
||
// Bright saturated colors for easter egg flashes (clicks 6–10)
|
||
const easterEggColors = [
|
||
"#ff0055", // hot pink
|
||
"#00cc44", // vivid green
|
||
"#3366ff", // electric blue
|
||
"#ff9900", // bright orange
|
||
"#aa00ff", // vivid purple
|
||
];
|
||
|
||
// Easter egg: click version 10 times to reveal the debug well.
|
||
// Each click does a copy-flash animation. After 5 clicks, each
|
||
// additional click flashes a different bright saturated color.
|
||
$("about-version").addEventListener("click", () => {
|
||
versionClickCount++;
|
||
clearTimeout(versionClickTimer);
|
||
// Reset counter if user stops clicking for 3 seconds
|
||
versionClickTimer = setTimeout(() => {
|
||
versionClickCount = 0;
|
||
}, 3000);
|
||
|
||
const el = $("about-version");
|
||
|
||
if (versionClickCount > 5) {
|
||
// Colored flash for clicks 6–10
|
||
const colorIdx = versionClickCount - 6;
|
||
const color = easterEggColors[colorIdx % easterEggColors.length];
|
||
el.classList.remove("copy-flash-fade");
|
||
el.style.backgroundColor = color;
|
||
el.style.color = "#ffffff";
|
||
setTimeout(() => {
|
||
el.style.backgroundColor = "";
|
||
el.style.color = "";
|
||
el.classList.add("copy-flash-fade");
|
||
setTimeout(() => {
|
||
el.classList.remove("copy-flash-fade");
|
||
}, 275);
|
||
}, 75);
|
||
} else {
|
||
// Standard copy-flash for clicks 1–5
|
||
flashCopyFeedback(el);
|
||
}
|
||
|
||
if (versionClickCount >= 10) {
|
||
versionClickCount = 0;
|
||
clearTimeout(versionClickTimer);
|
||
$("settings-debug-well").classList.remove("hidden");
|
||
}
|
||
});
|
||
|
||
// Debug mode toggle — update runtime flag, persist, and re-render banner
|
||
$("settings-debug-mode").addEventListener("change", async () => {
|
||
state.debugMode = $("settings-debug-mode").checked;
|
||
setRuntimeDebug(state.debugMode);
|
||
await saveState();
|
||
updateDebugBanner(state.currentView);
|
||
});
|
||
|
||
// Sync runtime debug flag on init
|
||
setRuntimeDebug(state.debugMode);
|
||
|
||
$("btn-settings-back").addEventListener("click", () => {
|
||
goBack();
|
||
});
|
||
}
|
||
|
||
module.exports = {
|
||
init,
|
||
show,
|
||
renderSiteLists,
|
||
cutViewStack,
|
||
setUseBlockscout,
|
||
showNewSettings,
|
||
};
|