import { DEFAULT_SETTINGS } from "../lib/defaults.js"; import { load, save } from "../lib/settings.js"; import { initializeMenuSettings, MENU_SETTING_KEYS } from "./menu-settings.js"; import { initializeShortcut, renderStaticShortcuts } from "./shortcut.js"; const SAVE_DEBOUNCE_MS = 400; const STATUS_CLEAR_MS = 1500; const field = (id) => document.getElementById(id); // The settings inside the collapsed "More options" section, read from the page // so moving a setting there needs no change here. const MORE_OPTION_KEYS = [...field("more-options").querySelectorAll("[id]")] .map(({ id }) => id) .filter((id) => Object.hasOwn(DEFAULT_SETTINGS, id)); // A customised setting should never look lost inside a closed section. This // only ever opens it, so it never undoes the user collapsing it. function revealCustomised(settings, keys = MORE_OPTION_KEYS) { if (keys.some((key) => settings[key] !== DEFAULT_SETTINGS[key])) { field("more-options").open = true; } } let saveTimer = null; let statusTimer = null; const menuSettings = initializeMenuSettings({ document, onChange: scheduleSave }); function render(settings) { menuSettings.render(settings); for (const [id, value] of Object.entries(settings)) { if (MENU_SETTING_KEYS.includes(id)) continue; const element = field(id); // Never overwrite the control the user is actively editing — this page's // own saves echo back through storage.onChanged (see below). if (element === document.activeElement) { continue; } if (typeof value === "boolean") { element.checked = value; } else { element.value = value; } } } function collect() { return { ...menuSettings.collect(), temporaryChat: field("temporaryChat").checked, webSearch: field("webSearch").checked, openIn: field("openIn").value, promptTemplate: field("promptTemplate").value, }; } function showStatus(message) { field("status").textContent = message; clearTimeout(statusTimer); statusTimer = setTimeout(() => { field("status").textContent = ""; }, STATUS_CLEAR_MS); } async function persist(settings) { try { await save(settings); showStatus("Saved"); } catch { // storage.sync sanitises its errors, so there is nothing specific to report. showStatus("Could not save settings"); } } // Debounced so typing in a text field cannot approach any write-rate quota. function scheduleSave() { clearTimeout(saveTimer); saveTimer = setTimeout(() => persist(collect()), SAVE_DEBOUNCE_MS); } // Module scripts are deferred, so the DOM is already parsed here. const [{ os }, initial] = await Promise.all([ browser.runtime.getPlatformInfo().catch(() => ({})), load(), ]); renderStaticShortcuts(document, os); void initializeShortcut({ commands: browser.commands, document, window, os }); render(initial); revealCustomised(initial); for (const id of Object.keys(DEFAULT_SETTINGS)) { if (MENU_SETTING_KEYS.includes(id)) continue; field(id).addEventListener("input", scheduleSave); field(id).addEventListener("change", scheduleSave); } field("restore").addEventListener("click", () => { clearTimeout(saveTimer); // Restoring defaults is an explicit user action, so it should reset every // field even if one currently has focus. Blur first so render()'s // activeElement guard (see below) has nothing to skip. document.activeElement?.blur(); render(DEFAULT_SETTINGS); persist({ ...DEFAULT_SETTINGS }); }); // browser.storage.onChanged fires for every write to the "sync" area, not just // a remote sync landing on another device — this page's own debounced // autosaves and its Restore-defaults save re-enter this listener too, which // render()'s activeElement guard makes safe to react to unconditionally. browser.storage.onChanged.addListener(async (changes, area) => { if (area !== "sync") { return; } const settings = await load(); render(settings); // Only keys whose value really changed, such as a sync from another device: // this page saves every key at once, so checking them all would re-open the // section whenever any other setting is edited. revealCustomised(settings, MORE_OPTION_KEYS.filter( (key) => key in changes && changes[key].newValue !== changes[key].oldValue, )); });