Contractless-GUI-Wallet/frontend/js/settings_page.js

575 lines
17 KiB
JavaScript
Raw Normal View History

2026-07-01 16:01:30 +00:00
let settingsLoaded = false;
function durationSettingMs(value) {
const text = String(value || "").trim().toLowerCase();
if (!text || text === "never") {
return 0;
}
const amount = Number.parseFloat(text);
if (!Number.isFinite(amount) || amount <= 0) {
return 0;
}
if (text.includes("second")) {
return amount * 1000;
}
if (text.includes("minute")) {
return amount * 60 * 1000;
}
return 0;
}
function resetAutoLockTimer() {
if (autoLockTimer) {
clearTimeout(autoLockTimer);
autoLockTimer = null;
}
if (!walletLoaded) {
return;
}
const timeoutMs = durationSettingMs(guiRuntimeSettings.auto_lock_timeout);
if (!timeoutMs) {
return;
}
autoLockTimer = setTimeout(async () => {
if (!walletLoaded) {
return;
}
try {
await window.__TAURI__?.core?.invoke?.("lock_wallet");
} catch (_) {
// Local UI lock is still the important user-facing behavior.
}
lockWalletSession();
}, timeoutMs);
}
function applyRuntimeGuiSettings(settings) {
guiRuntimeSettings = {
...guiRuntimeSettings,
auto_lock_timeout: settings?.auto_lock_timeout || guiRuntimeSettings.auto_lock_timeout,
clipboard_clear_timeout: settings?.clipboard_clear_timeout || guiRuntimeSettings.clipboard_clear_timeout,
require_key_before_signing: settings?.require_key_before_signing !== false
};
resetAutoLockTimer();
}
async function loadRuntimeGuiSettings() {
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke) {
return;
}
try {
applyRuntimeGuiSettings(await invoke("read_gui_settings"));
} catch (_) {
resetAutoLockTimer();
}
}
function resetAutoLockOnActivity() {
if (walletLoaded) {
resetAutoLockTimer();
}
}
function scheduleClipboardClear(copiedText) {
if (clipboardClearTimer) {
clearTimeout(clipboardClearTimer);
clipboardClearTimer = null;
}
lastClipboardText = copiedText || "";
const timeoutMs = durationSettingMs(guiRuntimeSettings.clipboard_clear_timeout);
if (!timeoutMs || !lastClipboardText || !navigator.clipboard) {
return;
}
clipboardClearTimer = setTimeout(async () => {
lastClipboardText = "";
try {
await navigator.clipboard.writeText("");
} catch (_) {
// Clipboard writes can fail outside a user gesture; never prompt or block.
}
}, timeoutMs);
}
async function clearWalletClipboard() {
if (clipboardClearTimer) {
clearTimeout(clipboardClearTimer);
clipboardClearTimer = null;
}
lastClipboardText = "";
const invoke = window.__TAURI__?.core?.invoke;
if (invoke) {
try {
await invoke("clear_system_clipboard");
return;
} catch (_) {
// Fall back to the Web Clipboard API when the native bridge is unavailable.
}
}
if (!navigator.clipboard) {
return;
}
try {
await navigator.clipboard.writeText("");
} catch (_) {}
}
async function writeClipboardText(text) {
if (!text || !navigator.clipboard) {
return false;
}
await navigator.clipboard.writeText(text);
scheduleClipboardClear(text);
return true;
}
function setSettingsMessage(message, mode = "") {
if (!settingsMessage) {
return;
}
settingsMessage.textContent = message;
settingsMessage.classList.toggle("error", mode === "error");
settingsMessage.classList.toggle("success", mode === "success");
}
function settingsFieldMap() {
return {
wallet_path: settingsWalletPath,
default_node: settingsDefaultNode,
unbroadcast_transactions_path: settingsUnbroadcastPath,
address_book_path: settingsAddressBookPath,
ipfs_gateway: settingsIpfsGateway,
auto_lock_timeout: settingsAutoLockTimeout,
clipboard_clear_timeout: settingsClipboardTimeout
};
}
function applyGuiSettings(settings) {
applyRuntimeGuiSettings(settings);
const fields = settingsFieldMap();
Object.entries(fields).forEach(([key, input]) => {
if (input) {
input.value = settings?.[key] || "";
}
});
if (settingsSavedNodes) {
settingsSavedNodes.value = Array.isArray(settings?.saved_nodes)
? settings.saved_nodes.join("\n")
: "";
}
if (settingsRequireKey) {
settingsRequireKey.checked = settings?.require_key_before_signing !== false;
}
}
function collectGuiSettings() {
return {
wallet_path: settingsWalletPath?.value?.trim() || "",
unbroadcast_transactions_path: settingsUnbroadcastPath?.value?.trim() || "",
address_book_path: settingsAddressBookPath?.value?.trim() || "",
default_node: settingsDefaultNode?.value?.trim() || "",
saved_nodes: (settingsSavedNodes?.value || "")
.split(/\r?\n/)
.map((node) => node.trim())
.filter(Boolean),
ipfs_gateway: settingsIpfsGateway?.value?.trim() || "",
auto_load_trusted_nft_images: true,
auto_lock_timeout: settingsAutoLockTimeout?.value || "15 minutes",
clipboard_clear_timeout: settingsClipboardTimeout?.value || "60 seconds",
require_key_before_signing: Boolean(settingsRequireKey?.checked),
settings_path: ""
};
}
async function loadSettingsPage({ force = false } = {}) {
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke || (settingsLoaded && !force)) {
return;
}
try {
const settings = await invoke("read_gui_settings");
applyGuiSettings(settings);
settingsLoaded = true;
setSettingsMessage("");
} catch (error) {
setSettingsMessage(String(error), "error");
}
}
async function saveSettingsPage() {
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke) {
setSettingsMessage("Settings can only be saved from the desktop app.", "error");
return;
}
if (settingsSaveButton) {
settingsSaveButton.disabled = true;
}
setSettingsMessage("Saving settings...");
try {
await invoke("save_gui_settings", { settings: collectGuiSettings() });
settingsLoaded = false;
await loadSettingsPage({ force: true });
await populateBroadcastNodeList();
setSettingsMessage("Settings saved.", "success");
} catch (error) {
setSettingsMessage(String(error), "error");
} finally {
if (settingsSaveButton) {
settingsSaveButton.disabled = false;
}
}
}
async function browseSettingsPath(button) {
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke) {
return;
}
const key = button.dataset.settingsBrowse;
const input = settingsFieldMap()[key];
if (!input) {
return;
}
try {
const selected = await invoke("select_settings_path", {
kind: button.dataset.settingsBrowseKind || "folder",
currentPath: input.value || ""
});
if (selected) {
input.value = selected;
}
} catch (error) {
setSettingsMessage(String(error), "error");
}
}
function setNodeTestStatus(message, mode = "") {
if (!settingsNodeTestStatus) {
return;
}
settingsNodeTestStatus.textContent = message || "";
settingsNodeTestStatus.classList.toggle("hidden", !message);
settingsNodeTestStatus.classList.toggle("error", mode === "error");
settingsNodeTestStatus.classList.toggle("success", mode === "success");
}
async function testSelectedSettingsNode() {
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke) {
setNodeTestStatus("Node testing is only available in the desktop app.", "error");
return;
}
const broadcastNode = settingsDefaultNode?.value?.trim() || activeBroadcastNode();
if (!broadcastNode) {
setNodeTestStatus("Select a node before testing.", "error");
return;
}
settingsTestNodeButton && (settingsTestNodeButton.disabled = true);
setNodeTestStatus("Testing node...");
try {
const timestamp = await invoke("test_broadcast_node_time", { broadcastNode });
setNodeTestStatus(`Active - node time ${formatNetworkTime(timestamp)}`, "success");
} catch (error) {
setNodeTestStatus(String(error), "error");
} finally {
settingsTestNodeButton && (settingsTestNodeButton.disabled = false);
}
}
function setCacheClearMessage(message, mode = "") {
if (!cacheClearMessage) {
return;
}
cacheClearMessage.textContent = message || "";
cacheClearMessage.classList.toggle("error", mode === "error");
cacheClearMessage.classList.toggle("success", mode === "success");
}
function setCacheClearModalOpen(open) {
cacheClearModal?.classList.toggle("hidden", !open);
if (open) {
setCacheClearMessage("");
}
}
async function clearSelectedWalletCache(kind, button) {
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke || !walletLoaded) {
setCacheClearMessage("Load a wallet before clearing local cache.", "error");
return;
}
button && (button.disabled = true);
setCacheClearMessage("Clearing cache...");
try {
await invoke("clear_wallet_cache", { cacheKind: kind });
if (kind === "history") {
clearHistoryRows();
resetLatestDashboardTransactionsMemory();
if (typeof resetHistoryPage === "function") {
resetHistoryPage();
}
} else if (kind === "nft" || kind === "nft_details") {
if (typeof resetNftsPage === "function") {
resetNftsPage();
}
if (currentView === "nfts" && typeof prepareNftsPage === "function") {
prepareNftsPage({ force: true });
}
} else if (kind === "address_book") {
if (typeof resetAddressBookPage === "function") {
resetAddressBookPage();
}
if (currentView === "addressbook" && typeof prepareAddressBookPage === "function") {
prepareAddressBookPage();
}
}
setCacheClearMessage("Cache cleared.", "success");
} catch (error) {
setCacheClearMessage(String(error), "error");
} finally {
button && (button.disabled = false);
}
}
function setBackupKeyMessage(message, mode = "") {
if (!backupKeyMessage) {
return;
}
backupKeyMessage.textContent = message;
backupKeyMessage.classList.toggle("error", mode === "error");
backupKeyMessage.classList.toggle("success", mode === "success");
}
function closeBackupKeyModal(value = null) {
if (backupKeyModal) {
backupKeyModal.classList.add("hidden");
}
if (backupKeyInput) {
backupKeyInput.value = "";
}
setBackupKeyMessage("");
if (pendingBackupKeyRequest) {
pendingBackupKeyRequest.resolve(value);
pendingBackupKeyRequest = null;
}
}
function requestWalletBackupKey(copy, title = "Wallet Backup") {
if (!backupKeyModal) {
return Promise.resolve(null);
}
if (pendingBackupKeyRequest) {
closeBackupKeyModal(null);
}
backupKeyTitle && (backupKeyTitle.textContent = title);
backupKeyCopy && (backupKeyCopy.textContent = copy || "Enter the wallet encryption key.");
backupKeyInput && (backupKeyInput.value = "");
setBackupKeyMessage("");
backupKeyModal.classList.remove("hidden");
setTimeout(() => backupKeyInput?.focus(), 0);
return new Promise((resolve) => {
pendingBackupKeyRequest = { resolve };
});
}
async function requestSigningWalletKey() {
if (guiRuntimeSettings.require_key_before_signing === false) {
return null;
}
return requestWalletBackupKey(
"Enter the wallet encryption key to sign this transaction.",
"Sign Transaction"
);
}
function htmlEscape(value) {
return String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function printWalletBackup(data) {
const printFrame = document.createElement("iframe");
printFrame.setAttribute("title", "Contractless Wallet Backup Print");
printFrame.style.position = "fixed";
printFrame.style.right = "0";
printFrame.style.bottom = "0";
printFrame.style.width = "0";
printFrame.style.height = "0";
printFrame.style.border = "0";
printFrame.style.opacity = "0";
printFrame.style.pointerEvents = "none";
document.body.appendChild(printFrame);
const printDocument = printFrame.contentWindow?.document;
if (!printDocument) {
printFrame.remove();
setSettingsMessage("Unable to prepare print document.", "error");
return;
}
printDocument.open();
printDocument.write(`<!doctype html>
<html>
<head>
<title>Contractless Wallet Backup</title>
<style>
body { font-family: Arial, sans-serif; color: #111; padding: 28px; }
h1 { margin: 0 0 8px; font-size: 24px; }
p { margin: 0 0 18px; color: #444; }
section { margin-top: 18px; }
h2 { font-size: 15px; margin: 0 0 8px; }
pre { white-space: pre-wrap; overflow-wrap: anywhere; border: 1px solid #bbb; padding: 10px; border-radius: 6px; }
.warning { border: 2px solid #9b1c1c; padding: 10px; color: #7a1111; font-weight: 700; }
</style>
</head>
<body>
<h1>Contractless Wallet Backup</h1>
<p class="warning">Keep this page private. Anyone with the private key and encryption key can control this wallet.</p>
<section><h2>Wallet</h2><pre>${htmlEscape(data.wallet_name)}</pre></section>
<section><h2>Private Key</h2><pre>${htmlEscape(data.private_key)}</pre></section>
<section><h2>Encryption Key</h2><pre>${htmlEscape(data.encryption_key)}</pre></section>
</body>
</html>`);
printDocument.close();
const cleanup = () => {
setTimeout(() => printFrame.remove(), 500);
};
printFrame.contentWindow.onafterprint = cleanup;
setTimeout(() => {
try {
printFrame.contentWindow.focus();
printFrame.contentWindow.print();
setSettingsMessage("Print dialog opened.", "success");
} catch (error) {
cleanup();
setSettingsMessage(`Unable to open print dialog: ${error}`, "error");
}
}, 100);
}
async function backupWalletImage() {
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke || !walletLoaded) {
setSettingsMessage("Load a wallet before backing it up.", "error");
return;
}
try {
setSettingsMessage("Preparing wallet image backup...");
const savedPath = await invoke("backup_active_wallet_image");
setSettingsMessage(savedPath ? `Wallet image saved to ${savedPath}` : "Wallet image backup cancelled.", savedPath ? "success" : "");
} catch (error) {
setSettingsMessage(String(error), "error");
}
}
async function backupWalletPrivateKey() {
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke || !walletLoaded) {
setSettingsMessage("Load a wallet before backing it up.", "error");
return;
}
const walletKey = await requestWalletBackupKey("Enter the wallet encryption key to decrypt the private key for backup.");
if (!walletKey) {
setSettingsMessage("Private key backup cancelled.");
return;
}
try {
setSettingsMessage("Decrypting private key for backup...");
const savedPath = await invoke("backup_active_wallet_private_key", { walletKey });
setSettingsMessage(savedPath ? `Private key saved to ${savedPath}` : "Private key backup cancelled.", savedPath ? "success" : "");
} catch (error) {
setSettingsMessage(String(error), "error");
}
}
async function printWalletPrivateKeyBackup() {
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke || !walletLoaded) {
setSettingsMessage("Load a wallet before printing backup details.", "error");
return;
}
const walletKey = await requestWalletBackupKey("Enter the wallet encryption key to print the private key and encryption key.");
if (!walletKey) {
setSettingsMessage("Print backup cancelled.");
return;
}
try {
setSettingsMessage("Decrypting wallet backup for printing...");
const data = await invoke("printable_wallet_backup", { walletKey });
printWalletBackup(data);
} catch (error) {
setSettingsMessage(String(error), "error");
}
}
settingsSaveButton?.addEventListener("click", saveSettingsPage);
settingsTestNodeButton?.addEventListener("click", testSelectedSettingsNode);
settingsCacheOpenButton?.addEventListener("click", () => setCacheClearModalOpen(true));
cacheClearCloseButton?.addEventListener("click", () => setCacheClearModalOpen(false));
cacheClearModal?.addEventListener("click", (event) => {
if (event.target === cacheClearModal) {
setCacheClearModalOpen(false);
}
});
document.querySelectorAll("[data-cache-clear-kind]").forEach((button) => {
button.addEventListener("click", () => clearSelectedWalletCache(button.dataset.cacheClearKind, button));
});
settingsBrowseButtons.forEach((button) => {
button.addEventListener("click", () => browseSettingsPath(button));
});
backupKeyForm?.addEventListener("submit", (event) => {
event.preventDefault();
const walletKey = backupKeyInput?.value || "";
if (!walletKey.trim()) {
setBackupKeyMessage("Wallet encryption key cannot be empty.", "error");
backupKeyInput?.focus();
return;
}
closeBackupKeyModal(walletKey);
});
backupKeyCloseButton?.addEventListener("click", () => closeBackupKeyModal(null));
backupKeyCancelButton?.addEventListener("click", () => closeBackupKeyModal(null));
backupKeyModal?.addEventListener("click", (event) => {
if (event.target === backupKeyModal) {
closeBackupKeyModal(null);
}
});
document.querySelector("[data-backup-wallet-image]")?.addEventListener("click", backupWalletImage);
document.querySelector("[data-backup-private-key]")?.addEventListener("click", backupWalletPrivateKey);
document.querySelector("[data-print-private-key]")?.addEventListener("click", printWalletPrivateKeyBackup);
["click", "keydown", "mousemove", "pointerdown", "touchstart"].forEach((eventName) => {
document.addEventListener(eventName, resetAutoLockOnActivity, { passive: true });
});
loadRuntimeGuiSettings();