(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 = "
" + state.queue.length + "in queue
" + "
" + state.learning.length + "learning
" + "
" + state.known.length + "known
"; } async function persist() { await saveState(state); } function downloadText(filename, text) { const blob = new Blob([text], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.rel = "noopener"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } async function init() { state = await loadState(); renderReview(); el.tabs.forEach(function (tab) { tab.addEventListener("click", function () { setTab(tab.getAttribute("data-panel")); }); }); el.btnAdd.addEventListener("click", async function () { const tokens = tokenize(el.importText.value); const dedupe = el.dedupeCi.checked; const added = mergeUnique(tokens, dedupe, state); let i; for (i = 0; i < added.length; i++) { state.queue.push(added[i]); } await persist(); el.importStatus.textContent = added.length === 0 ? "No new words added (duplicates or empty)." : "Added " + added.length + " word" + (added.length === 1 ? "" : "s") + " to the queue."; }); el.btnClearImport.addEventListener("click", function () { el.importText.value = ""; el.importStatus.textContent = ""; }); el.btnKnown.addEventListener("click", function () { decideKnown(); }); el.btnLearn.addEventListener("click", function () { decideLearn(); }); el.btnUndo.addEventListener("click", function () { undoLast(); }); el.exportLearning.addEventListener("click", function () { downloadText("learning-words.txt", sortAlpha(state.learning).join("\n")); }); el.exportKnown.addEventListener("click", function () { downloadText("known-words.txt", sortAlpha(state.known).join("\n")); }); el.btnResetAll.addEventListener("click", async function () { if (!window.confirm("Delete all saved words in this browser?")) return; state = defaultState(); await persist(); renderReview(); renderDataPanel(); el.importStatus.textContent = "Local data cleared."; }); document.addEventListener("keydown", function (e) { if (e.defaultPrevented) return; const t = e.target; if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA")) return; const k = e.key && e.key.length === 1 ? e.key.toLowerCase() : ""; if (k === "k") { e.preventDefault(); decideKnown(); } else if (k === "l") { e.preventDefault(); decideLearn(); } else if (k === "u") { e.preventDefault(); undoLast(); } }); } async function decideKnown() { if (!state.queue.length) return; const w = state.queue.shift(); state.known.push(w); pushUndo(state, { word: w, bucket: "known" }); await persist(); renderReview(); } async function decideLearn() { if (!state.queue.length) return; const w = state.queue.shift(); state.learning.push(w); pushUndo(state, { word: w, bucket: "learning" }); await persist(); renderReview(); } async function undoLast() { if (!state.undo.length) return; const last = state.undo.pop(); const w = last.word; if (last.bucket === "known") { const idx = state.known.lastIndexOf(w); if (idx !== -1) state.known.splice(idx, 1); } else if (last.bucket === "learning") { const idx = state.learning.lastIndexOf(w); if (idx !== -1) state.learning.splice(idx, 1); } state.queue.unshift(w); await persist(); renderReview(); } init().catch(function (err) { el.importStatus.textContent = "Storage error: " + (err && err.message ? err.message : String(err)); }); })();