(function () { const DB_NAME = "wordFilterDB"; const DB_VER = 1; const STORE = "kv"; const KEY_STATE = "state"; const defaultState = () => ({ queue: [], known: [], learning: [], undo: [], }); let dbPromise = null; function openDb() { if (dbPromise) return dbPromise; dbPromise = new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VER); req.onerror = () => reject(req.error); req.onupgradeneeded = () => { const db = req.result; if (!db.objectStoreNames.contains(STORE)) { db.createObjectStore(STORE); } }; req.onsuccess = () => resolve(req.result); }); return dbPromise; } async function loadState() { const db = await openDb(); return new Promise((resolve, reject) => { const tx = db.transaction(STORE, "readonly"); const st = tx.objectStore(STORE); const g = st.get(KEY_STATE); g.onerror = () => reject(g.error); g.onsuccess = () => { const v = g.result; if (!v) { resolve(defaultState()); return; } resolve({ queue: Array.isArray(v.queue) ? v.queue.slice() : [], known: Array.isArray(v.known) ? v.known.slice() : [], learning: Array.isArray(v.learning) ? v.learning.slice() : [], undo: Array.isArray(v.undo) ? v.undo.slice() : [], }); }; }); } async function saveState(state) { const db = await openDb(); const payload = { queue: state.queue.slice(), known: state.known.slice(), learning: state.learning.slice(), undo: state.undo.slice(), }; return new Promise((resolve, reject) => { const tx = db.transaction(STORE, "readwrite"); const st = tx.objectStore(STORE); const p = st.put(payload, KEY_STATE); p.onerror = () => reject(p.error); p.onsuccess = () => resolve(); }); } function normalizeKey(s) { return String(s).trim().toLowerCase(); } function tokenize(text) { const raw = String(text); const parts = raw.split(/[\r\n,;\t]+|\s{2,}|\s+/g); const out = []; for (let i = 0; i < parts.length; i++) { const t = parts[i].trim(); if (t.length) out.push(t); } return out; } function mergeUnique(tokens, dedupeCi, state) { const seen = new Set(); const existing = new Set(); let i; for (i = 0; i < state.queue.length; i++) { existing.add(dedupeCi ? normalizeKey(state.queue[i]) : state.queue[i]); } for (i = 0; i < state.known.length; i++) { existing.add(dedupeCi ? normalizeKey(state.known[i]) : state.known[i]); } for (i = 0; i < state.learning.length; i++) { existing.add(dedupeCi ? normalizeKey(state.learning[i]) : state.learning[i]); } const added = []; for (i = 0; i < tokens.length; i++) { const w = tokens[i]; const key = dedupeCi ? normalizeKey(w) : w; if (seen.has(key)) continue; seen.add(key); if (existing.has(key)) continue; existing.add(key); added.push(w); } return added; } function pushUndo(state, entry) { const u = state.undo; u.push(entry); if (u.length > 80) u.splice(0, u.length - 80); } function sortAlpha(arr) { return arr.slice().sort(function (a, b) { return a.localeCompare(b, undefined, { sensitivity: "base" }); }); } const el = { tabs: document.querySelectorAll(".tab"), panels: { import: document.getElementById("panel-import"), review: document.getElementById("panel-review"), data: document.getElementById("panel-data"), }, importText: document.getElementById("import-text"), dedupeCi: document.getElementById("dedupe-ci"), btnAdd: document.getElementById("btn-add"), btnClearImport: document.getElementById("btn-clear-import"), importStatus: document.getElementById("import-status"), reviewCount: document.getElementById("review-count"), reviewEmpty: document.getElementById("review-empty"), reviewActive: document.getElementById("review-active"), reviewWord: document.getElementById("review-word"), btnKnown: document.getElementById("btn-known"), btnLearn: document.getElementById("btn-learn"), btnUndo: document.getElementById("btn-undo"), stats: document.getElementById("stats"), listLearning: document.getElementById("list-learning"), listKnown: document.getElementById("list-known"), exportLearning: document.getElementById("export-learning"), exportKnown: document.getElementById("export-known"), btnResetAll: document.getElementById("btn-reset-all"), }; let state = defaultState(); function setTab(name) { const keys = Object.keys(el.panels); let i; for (i = 0; i < el.tabs.length; i++) { const t = el.tabs[i]; const on = t.getAttribute("data-panel") === name; t.classList.toggle("is-active", on); t.setAttribute("aria-selected", on ? "true" : "false"); } for (i = 0; i < keys.length; i++) { const k = keys[i]; const p = el.panels[k]; const vis = k === name; p.hidden = !vis; p.classList.toggle("is-visible", vis); } if (name === "review") { renderReview(); } if (name === "data") { renderDataPanel(); } } function renderReview() { const n = state.queue.length; el.reviewCount.textContent = n ? "Queue: " + n + " word" + (n === 1 ? "" : "s") : "Queue: empty"; if (!n) { el.reviewEmpty.hidden = false; el.reviewActive.hidden = true; el.reviewWord.textContent = ""; return; } el.reviewEmpty.hidden = true; el.reviewActive.hidden = false; el.reviewWord.textContent = state.queue[0]; } function renderDataPanel() { const learnSorted = sortAlpha(state.learning); const knownSorted = sortAlpha(state.known); el.listLearning.value = learnSorted.join("\n"); el.listKnown.value = knownSorted.join("\n"); el.stats.innerHTML = "