575 lines
17 KiB
JavaScript
575 lines
17 KiB
JavaScript
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, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
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();
|