// === License & mode (stub) === // Flip this to true for local testing of Pro features if you want: let licenseValid = false; // <- default: Free // Persistent settings let actionMode = 'copy'; // 'copy' | 'insert' | 'auto' // Tone settings (persisted in sync storage) let toneLock = false; // if true, always use preferred tone let preferredToneSlug = null; // 'light' | 'medium-light' | 'medium' | 'medium-dark' | 'dark' // Elements const settingsBtn = document.getElementById('settings'); const sheet = document.getElementById('settings-sheet'); const sheetClose = document.getElementById('sheet-close'); const backdrop = document.getElementById('sheet-backdrop'); const modeGroup = document.getElementById('mode-group'); const licenseKeyEl = document.getElementById('license-key'); const licenseActivateBtn = document.getElementById('license-activate'); const licenseStatusEl = document.getElementById('license-status'); const licenseDeactivateBtn = document.getElementById('license-deactivate'); const licenseEditBtn = document.getElementById('license-edit'); const licenseCancelEditBtn = document.getElementById('license-cancel-edit'); const diagRunBtn = document.getElementById('diag-run'); const diagSpin = document.getElementById('diag-spin'); const diagOut = document.getElementById('diag-out'); const API = { base: "https://emoji.dewe.pw/api", list: "/emojis", limit: 20 // lighter for extensions; your API caps at 50, docs try-it at 10 }; API.cats = "/categories"; // endpoint for categories map let CAT_MAP = null; // { "Category": ["sub1","sub2", ...], ... } // Preferred, human-first category order (fallback: alphabetical) const PREFERRED_CATEGORY_ORDER = [ "Smileys & Emotion", "People & Body", "Animals & Nature", "Food & Drink", "Travel & Places", "Activities", "Objects", "Symbols", "Flags" ]; const CAT_INDEX = Object.fromEntries(PREFERRED_CATEGORY_ORDER.map((c, i) => [c, i])); function categoryComparator(a, b) { const ia = CAT_INDEX[a]; const ib = CAT_INDEX[b]; if (ia !== undefined && ib !== undefined) return ia - ib; // both in preferred list if (ia !== undefined) return -1; // a preferred, b not if (ib !== undefined) return 1; // b preferred, a not return a.localeCompare(b, undefined, { sensitivity: "base" }); // both not preferred → A–Z } async function loadCategories() { try { const res = await fetch(`${API.base}${API.cats}`); if (!res.ok) throw new Error(`cats ${res.status}`); CAT_MAP = await res.json(); populateCategorySelect(); } catch (e) { console.warn("Failed to load categories", e); } } function populateCategorySelect() { const catSel = document.getElementById('cat'); const subSel = document.getElementById('sub'); if (!catSel || !CAT_MAP) return; // reset catSel.innerHTML = ``; Object.keys(CAT_MAP).sort(categoryComparator).forEach(cat => { const opt = document.createElement('option'); opt.value = cat; opt.textContent = cat; catSel.appendChild(opt); }); // clear subs initially subSel.innerHTML = ``; subSel.disabled = true; } function populateSubcategorySelect(category) { const subSel = document.getElementById('sub'); if (!subSel) return; subSel.innerHTML = ``; const subs = (CAT_MAP?.[category] || []).slice().sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base", numeric: true })); subs.forEach(sc => { const opt = document.createElement('option'); opt.value = sc; opt.textContent = sc; subSel.appendChild(opt); }); subSel.disabled = subs.length === 0; } let page = 1, total = 0, items = []; const q = document.getElementById('q'); const cat = document.getElementById('cat'); const sub = document.getElementById('sub'); const clearBtn = document.getElementById('clear'); const list = document.getElementById('list'); const more = document.getElementById('more'); const count = document.getElementById('count'); const verEl = document.getElementById('ver'); function setVersionBadge() { try { const { name, version } = chrome.runtime.getManifest(); if (verEl) { verEl.textContent = `v${version}`; verEl.title = `${name} v${version}`; } } catch (e) { // ignore if unavailable } } setVersionBadge(); // --- Loading spinner (flip-flop with Load More) --- let spinnerEl = null; function showSpinner() { if (spinnerEl) return; // Hide Load More while loading if (more) more.classList.add('hidden'); spinnerEl = document.createElement('div'); spinnerEl.id = 'grid-spinner'; spinnerEl.className = 'w-full flex justify-center py-6'; spinnerEl.innerHTML = '
'; // place under the grid (list?.parentElement || document.body).appendChild(spinnerEl); } function hideSpinner() { if (spinnerEl) { spinnerEl.remove(); spinnerEl = null; } // Recompute Load More visibility after loading completes if (typeof updateFooter === 'function') { try { updateFooter(); } catch(_) {} } else if (more) { more.classList.remove('hidden'); } } // THEME — two states only: 'light' | 'dark' const themeBtn = document.getElementById('theme'); const toastEl = document.getElementById('toast'); async function initTheme() { const stored = (await chrome.storage.local.get('theme')).theme; const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; const initial = stored || (prefersDark ? 'dark' : 'light'); await chrome.storage.local.set({ theme: initial }); applyTheme(initial); } function applyTheme(theme) { const clsLight = 'theme-light'; const clsDark = 'theme-dark'; document.body.classList.remove(clsLight, clsDark); document.documentElement.classList.remove(clsLight, clsDark); // <-- add const cls = (theme === 'dark') ? clsDark : clsLight; document.body.classList.add(cls); document.documentElement.classList.add(cls); // <-- add // Button shows the opposite icon so it's clear what tapping will do themeBtn.textContent = theme === 'dark' ? '🌞' : '🌙'; themeBtn.dataset.theme = theme; } async function toggleTheme() { const curr = themeBtn.dataset.theme === 'dark' ? 'dark' : 'light'; const next = curr === 'dark' ? 'light' : 'dark'; await chrome.storage.local.set({ theme: next }); applyTheme(next); } themeBtn?.addEventListener('click', toggleTheme); initTheme(); updateClearButtonIcon(); // --- Skin tone (Pro) ------------------------------------------------------- const SKIN_TONES = [ { key: '1f3fb', ch: '\u{1F3FB}', slug: 'light', label: 'Light' }, { key: '1f3fc', ch: '\u{1F3FC}', slug: 'medium-light', label: 'Medium-Light' }, { key: '1f3fd', ch: '\u{1F3FD}', slug: 'medium', label: 'Medium' }, { key: '1f3fe', ch: '\u{1F3FE}', slug: 'medium-dark', label: 'Medium-Dark' }, { key: '1f3ff', ch: '\u{1F3FF}', slug: 'dark', label: 'Dark' }, ]; const TONE_SLUGS = SKIN_TONES.map(t => t.slug); const STRIP_TONE_RE = /[\u{1F3FB}-\u{1F3FF}]/gu; function stripSkinTone(s){ return (s||'').replace(STRIP_TONE_RE,''); } function withSkinTone(base, ch){ return `${base||''}${ch||''}`; } // Tone helpers: keep globals in sync and expose helpers for slug async function getPreferredToneIndex(){ return new Promise(res => { chrome.storage.sync.get(['preferredSkinTone'], v => { preferredToneSlug = v?.preferredSkinTone || null; const idx = TONE_SLUGS.indexOf(preferredToneSlug); res(idx >= 0 ? idx : -1); }); }); } async function setPreferredToneIndex(i){ preferredToneSlug = TONE_SLUGS[i] || null; return new Promise(res => chrome.storage.sync.set({ preferredSkinTone: preferredToneSlug }, () => res())); } async function getToneLock(){ return new Promise(res => chrome.storage.sync.get(['toneLock'], v => { toneLock = !!v.toneLock; res(toneLock); })); } async function setToneLock(val){ toneLock = !!val; return new Promise(res => chrome.storage.sync.set({ toneLock }, () => res())); } // debounce let timer; function debounced(fn, delay=250){ clearTimeout(timer); timer = setTimeout(fn, delay); } async function fetchPage(reset=false) { more.disabled = true; if (reset) { page = 1; items = []; list.innerHTML = ""; } try { showSpinner(); const params = new URLSearchParams({ page: String(page), limit: String(API.limit) }); if (q.value.trim()) params.set('q', q.value.trim()); if (cat.value.trim()) params.set('category', cat.value.trim()); if (sub.value.trim()) params.set('subcategory', sub.value.trim()); const url = `${API.base}${API.list}?${params.toString()}`; const res = await fetch(url, { cache: 'no-store' }); if (!res.ok) throw new Error(`API ${res.status}`); const data = await res.json(); total = data.total || 0; // append for (const e of (data.items || [])) { items.push(e); renderCard(e); } updateFooter(); } catch (err) { console.error('Fetch failed', err); // Show a small notice const msg = document.createElement('div'); msg.className = 'w-full text-center text-red-500 py-2'; msg.textContent = 'Failed to load. Please try again.'; list?.appendChild(msg); } finally { hideSpinner(); } } function updateFooter() { count.textContent = `${items.length} / ${total}`; const loading = !!spinnerEl; if (items.length < total) { more.textContent = 'Load more'; more.disabled = loading; // Only show button when not loading more.classList.toggle('hidden', loading); } else { more.textContent = 'All loaded'; more.disabled = true; // If fully loaded, keep it visible but disabled, or hide if you prefer: // more.classList.add('hidden'); } } async function ensureContentScript(tabId) { // try ping try { const pong = await chrome.tabs.sendMessage(tabId, { type: 'dewemoji_ping' }); if (pong?.ok) return true; } catch {} // inject then ping again try { await chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'] }); const pong2 = await chrome.tabs.sendMessage(tabId, { type: 'dewemoji_ping' }); return !!pong2?.ok; } catch { return false; } } async function insert(text, opts = {}) { const strict = !!opts.strict; // if true, do NOT fallback to copy if (!text) return false; try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (!tab?.id) { if (!strict) { await navigator.clipboard.writeText(text); } return false; } // A) Try inline injection (fast path) const [{ result }] = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: (txt) => { const el = document.activeElement; const isEditable = el && ( el.tagName === 'TEXTAREA' || (el.tagName === 'INPUT' && /^(text|search|email|url|tel|number|password)?$/i.test(el.type)) || el.isContentEditable ); if (!isEditable) return false; if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') { const start = el.selectionStart ?? el.value.length; const end = el.selectionEnd ?? start; const val = el.value ?? ''; el.value = val.slice(0, start) + txt + val.slice(end); const pos = start + txt.length; try { el.setSelectionRange(pos, pos); } catch {} el.dispatchEvent(new Event('input', { bubbles: true })); return true; } try { document.execCommand('insertText', false, txt); return true; } catch { return false; } }, args: [text] }); if (result) return true; // inserted via inline // B) Ensure content.js present, then ask it (robust path) if (await ensureContentScript(tab.id)) { const res = await chrome.tabs.sendMessage(tab.id, { type: 'dewemoji_insert', text }); if (res?.ok) return true; // inserted via content script } // C) Fallback if (!strict) { await navigator.clipboard.writeText(text); } return false; } catch { if (!strict) { await navigator.clipboard.writeText(text); } return false; } } function updateClearButtonIcon() { if (!clearBtn) return; const hasText = q.value.trim().length > 0; if (hasText) { clearBtn.innerHTML = ``; clearBtn.title = 'Clear'; clearBtn.setAttribute('aria-label', 'Clear'); clearBtn.classList.add('nm'); clearBtn.dataset.mode = 'clear'; } else { clearBtn.innerHTML = ``; // refresh icon clearBtn.title = 'Refresh'; clearBtn.setAttribute('aria-label', 'Refresh'); clearBtn.classList.add('nm'); clearBtn.dataset.mode = 'refresh'; } } // events q.addEventListener('input', () => { updateClearButtonIcon(); debounced(() => fetchPage(true)); }); cat.addEventListener('change', () => { populateSubcategorySelect(cat.value); sub.value = ''; fetchPage(true).catch(console.error); }); sub.addEventListener('change', () => { fetchPage(true).catch(console.error); }); clearBtn.addEventListener('click', () => { const mode = clearBtn.dataset.mode; if (mode === 'clear') { q.value = ''; updateClearButtonIcon(); fetchPage(true); } else { // refresh when empty fetchPage(true); } }); more.addEventListener('click', () => { more.disabled = true; page += 1; fetchPage(false); }); function showToast(text='Done') { if (!toastEl) return; toastEl.textContent = text; toastEl.classList.add('show'); clearTimeout(showToast._t); showToast._t = setTimeout(() => toastEl.classList.remove('show'), 1400); } // (obsolete) placeholder; real renderCard defined later with Windows support + Pro tone UI function renderCard(e) {} // --- Popover helpers for skin tone picker --- function showPopoverNear(anchorEl, popEl){ const r = anchorEl.getBoundingClientRect(); Object.assign(popEl.style, { position: 'fixed', left: `${Math.min(window.innerWidth - 8, Math.max(8, r.left))}px`, top: `${Math.min(window.innerHeight - 8, r.bottom + 6)}px`, zIndex: 9999, }); document.body.appendChild(popEl); } function removePopover(){ const p = document.getElementById('dewemoji-tone-popover'); if (p && p.parentNode) p.parentNode.removeChild(p); document.removeEventListener('click', handleOutsideClose, true); } function handleOutsideClose(e){ const p = document.getElementById('dewemoji-tone-popover'); if (p && !p.contains(e.target)) removePopover(); } function buildTonePopover(base, activeIdx, onPick){ const el = document.createElement('div'); el.id = 'dewemoji-tone-popover'; el.setAttribute('role','dialog'); el.style.cssText = 'background: var(--c-bg, #fff); border: 1px solid var(--c-border, rgba(0,0,0,.12)); padding:6px; border-radius:8px; box-shadow:0 6px 18px rgba(0,0,0,.15); display:flex; gap:6px;'; SKIN_TONES.forEach((t,i) => { const b = document.createElement('button'); b.className = 'tone-btn'; b.textContent = withSkinTone(base, t.ch); b.title = t.label; b.style.cssText = 'min-width:32px;height:32px;border-radius:6px;border:1px solid transparent;display:flex;align-items:center;justify-content:center;background:var(--c-chip,#f3f4f6)'; if (i === activeIdx) b.classList.add('selected'); b.addEventListener('click', () => onPick(i, t)); el.appendChild(b); }); setTimeout(() => document.addEventListener('click', handleOutsideClose, true), 0); return el; } // initial loadCategories().catch(console.error); fetchPage(true).catch(console.error); async function loadSettings() { const data = await chrome.storage.local.get(['licenseValid', 'licenseKey', 'actionMode']); licenseValid = !!data.licenseValid; actionMode = data.actionMode || 'copy'; if (data.licenseKey) licenseKeyEl && (licenseKeyEl.value = data.licenseKey); applyLicenseUI(); applyModeUI(); setStatusTag(); // tone settings from sync storage await getPreferredToneIndex(); await getToneLock(); renderToneSettingsSection(); // ensure license field starts in non-edit mode setLicenseEditMode(false); } async function saveSettings() { await chrome.storage.local.set({ licenseValid, actionMode, licenseKey: licenseKeyEl?.value || '' }); } function setLicenseEditMode(on) { if (!licenseKeyEl) return; const isOn = !!on; // Input enabled only when editing OR not licensed licenseKeyEl.disabled = licenseValid ? !isOn : false; if (licenseValid) { // Licensed: default view hides Activate; editing shows Save/Cancel if (licenseActivateBtn) { licenseActivateBtn.style.display = isOn ? '' : 'none'; licenseActivateBtn.textContent = isOn ? 'Save' : 'Activate'; } if (licenseDeactivateBtn) licenseDeactivateBtn.style.display = licenseActivateBtn.textContent == 'Save' ? 'none' : ''; if (licenseEditBtn) licenseEditBtn.style.display = isOn ? 'none' : ''; if (licenseCancelEditBtn) licenseCancelEditBtn.style.display = isOn ? '' : 'none'; } else { // Free: show Activate only if (licenseActivateBtn) { licenseActivateBtn.style.display = ''; licenseActivateBtn.textContent = 'Activate'; } if (licenseDeactivateBtn) licenseDeactivateBtn.style.display = 'none'; if (licenseEditBtn) licenseEditBtn.style.display = 'none'; if (licenseCancelEditBtn) licenseCancelEditBtn.style.display = 'none'; } } function applyLicenseUI() { // enable/disable Pro radios const radios = modeGroup.querySelectorAll('input[type="radio"][name="actionMode"]'); radios.forEach(r => { const isPro = (r.value === 'insert' || r.value === 'auto'); if (isPro) { r.disabled = !licenseValid; r.closest('.radio').setAttribute('aria-disabled', !licenseValid ? 'true' : 'false'); } }); if (!licenseValid) { // Force visible selection to 'copy' for clarity const copy = modeGroup.querySelector('input[type="radio"][value="copy"]'); if (copy) copy.checked = true; licenseStatusEl && (licenseStatusEl.textContent = 'Free mode — Pro features locked.'); } else { // Restore selected mode if Pro; if current actionMode is pro, check it const target = modeGroup.querySelector(`input[type="radio"][value="${actionMode}"]`) || modeGroup.querySelector('input[type="radio"][value="auto"]'); if (target) target.checked = true; licenseStatusEl && (licenseStatusEl.textContent = 'Pro active'); } // Reset to non-edit view per license state if (licenseValid) { if (licenseKeyEl) licenseKeyEl.disabled = true; if (licenseActivateBtn) { licenseActivateBtn.style.display = 'none'; licenseActivateBtn.textContent = 'Activate'; } if (licenseDeactivateBtn) licenseDeactivateBtn.style.display = ''; if (licenseEditBtn) licenseEditBtn.style.display = ''; if (licenseCancelEditBtn) licenseCancelEditBtn.style.display = 'none'; } else { if (licenseKeyEl) licenseKeyEl.disabled = false; if (licenseActivateBtn) { licenseActivateBtn.style.display = ''; licenseActivateBtn.textContent = 'Activate'; } if (licenseDeactivateBtn) licenseDeactivateBtn.style.display = 'none'; if (licenseEditBtn) licenseEditBtn.style.display = 'none'; if (licenseCancelEditBtn) licenseCancelEditBtn.style.display = 'none'; } setStatusTag(); } function applyModeUI() { // Ensure the selected radio matches actionMode (if not locked) const input = modeGroup.querySelector(`input[type="radio"][value="${actionMode}"]`); if (input && !input.disabled) input.checked = true; } // --- License account id helper (best practice: hash + label) --- async function accountId() { // Return a stable, privacy-preserving identifier and a masked label for UI // { id: , label: } // Try cache first const cached = await chrome.storage.local.get(['accountId','accountLabel']); if (cached.accountId && cached.accountLabel) { return { id: cached.accountId, label: cached.accountLabel }; } let raw = ''; let label = ''; try { if (chrome.identity && chrome.identity.getProfileUserInfo) { const info = await new Promise(res => chrome.identity.getProfileUserInfo(res)); // info: { email, id } if (info?.email) { raw = (info.email || '').trim().toLowerCase(); label = maskEmail(info.email); } else if (info?.id) { raw = String(info.id); label = shortId(info.id); } } } catch {} if (!raw) { // Fallback to a per-profile UUID const got = await chrome.storage.local.get(['profileUUID']); if (got?.profileUUID) { raw = got.profileUUID; label = shortId(raw); } else { raw = crypto.randomUUID(); label = shortId(raw); await chrome.storage.local.set({ profileUUID: raw }); } } const hashed = await sha256Base64Url(raw); await chrome.storage.local.set({ accountId: hashed, accountLabel: label }); return { id: hashed, label }; } function maskEmail(email){ const [u, d] = String(email).split('@'); if (!d) return shortId(email); const head = u.slice(0,1); const tail = u.slice(-1); return `${head}${u.length>2?'***':''}${tail}@${d}`; } function shortId(s){ const x = String(s); return x.length <= 8 ? x : `${x.slice(0,4)}…${x.slice(-2)}`; } async function sha256Base64Url(input){ const enc = new TextEncoder(); const buf = await crypto.subtle.digest('SHA-256', enc.encode(input)); const bytes = new Uint8Array(buf); let bin = ''; for (let i=0;iPreferred skin tone
`; sheet.querySelector('#tab-pro')?.appendChild(sec) || sheet.appendChild(sec); } const pal = sec.querySelector('#tone-palette'); pal.innerHTML = ''; const lockEl = sec.querySelector('#tone-lock'); lockEl.checked = !!toneLock; lockEl.onchange = async (e)=>{ await setToneLock(!!e.target.checked); showToast(toneLock ? 'Tone lock enabled' : 'Tone lock disabled'); }; const idx = TONE_SLUGS.indexOf(preferredToneSlug); const basePreview = '✋'; SKIN_TONES.forEach((t,i)=>{ const b = document.createElement('button'); b.className = 'tone-chip'; b.title = t.label; b.textContent = basePreview + t.ch; // small preview b.style.cssText = 'height:32px;border-radius:8px;padding:0 8px;border:1px solid var(--c-border, rgba(0,0,0,.12));background:var(--c-chip,#f3f4f6)'; if (i === idx) b.classList.add('selected'); b.addEventListener('click', async ()=>{ await setPreferredToneIndex(i); showToast('Preferred tone set'); renderToneSettingsSection(); }); pal.appendChild(b); }); const note = sec.querySelector('#tone-note'); if (!preferredToneSlug) { note.textContent = 'Tip: choosing a tone will lock it for next picks if the toggle is on.'; } else { note.textContent = toneLock ? `Using ${preferredToneSlug.replace('-', ' ')} tone by default.` : 'Lock is off — we will ask via the palette.'; } } function openSheet() { sheet.removeAttribute('hidden'); backdrop.removeAttribute('hidden'); requestAnimationFrame(() => { sheet.classList.add('show'); backdrop.classList.add('show'); }); } function closeSheet() { sheet.classList.remove('show'); backdrop.classList.remove('show'); setTimeout(() => { sheet.setAttribute('hidden',''); backdrop.setAttribute('hidden',''); }, 180); } settingsBtn?.addEventListener('click', openSheet); sheetClose?.addEventListener('click', closeSheet); backdrop?.addEventListener('click', closeSheet); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeSheet(); }); // Tabs logic for settings sheet const tabsWrap = document.querySelector('#settings-sheet .tabs'); if (tabsWrap) { tabsWrap.addEventListener('click', (e) => { const btn = e.target.closest('.tab'); if (!btn) return; const target = btn.dataset.tab; document.querySelectorAll('#settings-sheet .tab').forEach(b => b.classList.toggle('active', b === btn)); document.querySelectorAll('#settings-sheet .tabpane').forEach(p => p.classList.toggle('active', p.id === `tab-${target}`)); }); } modeGroup?.addEventListener('change', async (e) => { if (e.target && e.target.name === 'actionMode') { const val = e.target.value; // If not licensed and user clicked a Pro option, bounce back to copy if (!licenseValid && (val === 'insert' || val === 'auto')) { showToast('Pro required for Insert/Automatic'); const copy = modeGroup.querySelector('input[value="copy"]'); if (copy) copy.checked = true; actionMode = 'copy'; } else { actionMode = val; } await saveSettings(); } }); licenseActivateBtn?.addEventListener('click', async () => { const key = (licenseKeyEl?.value || '').trim(); if (!key) { showToast('Enter a license key'); return; } // TODO: replace with server activation; for now treat as success in dev // Example: // const acct = await accountId(); // const res = await fetch('https://YOUR-API/license/activate', { // method: 'POST', headers: { 'Content-Type': 'application/json' }, // body: JSON.stringify({ key, account_id: acct.id, version: chrome.runtime.getManifest().version }) // }); // const data = await res.json(); if (!res.ok || !data.ok) throw new Error(data.message || 'Invalid license'); try { licenseValid = true; await chrome.storage.local.set({ licenseValid: true, licenseKey: key, lastLicenseCheck: Date.now() }); applyLicenseUI(); setLicenseEditMode(false); showToast('License activated ✓'); } catch (e) { licenseValid = false; await chrome.storage.local.set({ licenseValid: false }); applyLicenseUI(); showToast('Activation failed'); } }); licenseEditBtn?.addEventListener('click', () => { setLicenseEditMode(true); }); licenseCancelEditBtn?.addEventListener('click', async () => { const data = await chrome.storage.local.get(['licenseKey']); if (licenseKeyEl) licenseKeyEl.value = data.licenseKey || ''; setLicenseEditMode(false); }); licenseDeactivateBtn?.addEventListener('click', async () => { if (!licenseValid) return; const ok = confirm('Deactivate Pro on this device?'); if (!ok) return; try { // (Optional) TODO: call your API /license/deactivate here, e.g.: // const acct = await accountId(); // await fetch('https://YOUR-API/license/deactivate', { // method: 'POST', headers: { 'Content-Type': 'application/json' }, // body: JSON.stringify({ key: (licenseKeyEl?.value || '').trim(), account_id: acct.id }) // }); licenseValid = false; await chrome.storage.local.set({ licenseValid: false, licenseKey: '' }); if (licenseKeyEl) licenseKeyEl.value = ''; applyLicenseUI(); setLicenseEditMode(false); showToast('License deactivated'); } catch (e) { showToast('Could not deactivate'); } }); function renderDiag(obj) { const lines = []; lines.push(`Content script loaded: ${obj ? 'yes' : 'no'}`); if (!obj) return lines.join('\n'); lines.push(`Active editable type: ${obj.activeType ?? 'none'}`); lines.push(`Has caret/selection: ${obj.hasRange ? 'yes' : 'no'}`); lines.push(`Last insert result: ${obj.lastInsertOK === null ? 'n/a' : obj.lastInsertOK ? 'success' : 'failed'}`); if (obj.lastInsertMessage) lines.push(`Note: ${obj.lastInsertMessage}`); return lines.join('\n'); } diagRunBtn?.addEventListener('click', async () => { diagOut.textContent = ''; diagSpin.style.display = 'inline'; try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (!tab?.id) { diagOut.textContent = 'No active tab.'; return; } const ready = await ensureContentScript(tab.id); if (!ready) { diagOut.textContent = renderDiag(null); return; } const info = await chrome.tabs.sendMessage(tab.id, { type: 'dewemoji_diag' }); diagOut.textContent = renderDiag(info || null); } catch (e) { diagOut.textContent = `Error: ${e?.message || e}`; } finally { diagSpin.style.display = 'none'; } }); async function performEmojiAction(glyph) { // Free users always copy const mode = licenseValid ? actionMode : 'copy'; if (mode === 'copy') { await navigator.clipboard.writeText(glyph); showToast('Copied ✅'); return; } if (mode === 'insert') { const ok = await insert(glyph, { strict: true }); // no copy fallback showToast(ok ? 'Inserted ✅' : '❗ No editable field'); return; } // mode === 'auto' (insert else copy) { const ok = await insert(glyph, { strict: false }); showToast(ok ? 'Inserted ✅' : 'Copied ✅'); return; } } async function setStatusTag(){ const dewemojiStatusTag = document.getElementById('dewemoji-status'); if(licenseValid){ dewemojiStatusTag.innerText = 'Pro'; dewemojiStatusTag.classList.add('pro'); dewemojiStatusTag.classList.remove('free'); }else{ dewemojiStatusTag.innerText = 'Free'; dewemojiStatusTag.classList.add('free'); dewemojiStatusTag.classList.remove('pro'); } } setStatusTag().catch(() => {}); const IS_WINDOWS = navigator.userAgent.includes('Windows'); // Build Twemoji SVG url from codepoints function twemojiSrcFromCodepoints(cp) { return `https://twemoji.maxcdn.com/v/latest/svg/${cp}.svg`; } // OPTIONAL: as a secondary image fallback if Twemoji doesn't have it function notoSrcFromCodepoints(cp) { // Twemoji uses "1f469-200d-1f4bb.svg" // Noto repo names use underscores: "emoji_u1f469_200d_1f4bb.svg" const underscored = cp.split('-').join('_'); return `https://cdn.jsdelivr.net/gh/googlefonts/noto-emoji@main/svg/emoji_u${underscored}.svg`; } /** * Ensure an emoji is renderable inside `container` on Windows, * and return a canonical glyph string for copy/insert. * - Tries twemoji.parse first * - If nothing replaced, forces an using toCodePoint() * - Returns { glyph, usedImg } */ function ensureRenderableAndGlyph(emojiStr, container) { let glyph = emojiStr; if (!(IS_WINDOWS && window.twemoji)) { container.textContent = glyph; return { glyph, usedImg: false }; } // Try normal twemoji.parse on a temp node const temp = document.createElement('span'); temp.textContent = emojiStr; window.twemoji.parse(temp, { folder: 'svg', ext: '.svg', base: 'https://twemoji.maxcdn.com/v/latest/', attributes: () => ({ draggable: 'false', alt: '' }) }); const img = temp.querySelector('img'); if (img) { // Parsed OK — adopt result container.replaceChildren(img); return { glyph, usedImg: true }; } // Force build from codepoints (handles cases parse() didn't catch) try { const cp = window.twemoji.convert.toCodePoint(emojiStr); const img2 = new Image(); img2.alt = emojiStr; img2.draggable = false; img2.src = notoSrcFromCodepoints(cp); img2.onerror = () => { img2.onerror = null; // prevent loops img2.src = twemojiSrcFromCodepoints(cp); }; container.replaceChildren(img2); // Normalize the glyph for copying from the same codepoints // (twemoji provides the inverse) glyph = window.twemoji.convert.fromCodePoint(cp); return { glyph, usedImg: true }; } catch { // Last resort: show text (may be tofu) but keep original glyph container.textContent = emojiStr; return { glyph: emojiStr, usedImg: false }; } } function renderCard(e) { const card = document.createElement('div'); card.className = 'card'; card.title = e.name || ''; const emo = document.createElement('div'); emo.className = 'emo'; let baseGlyph = e.emoji || e.symbol || e.text || ''; const prefIdxPromise = getPreferredToneIndex(); (async () => { let renderGlyph = baseGlyph; if (licenseValid && toneLock && e.supports_skin_tone) { const idx = await prefIdxPromise; if (idx >= 0) { const base = e.emoji_base || stripSkinTone(baseGlyph); renderGlyph = withSkinTone(base, SKIN_TONES[idx].ch); } } const res = ensureRenderableAndGlyph(renderGlyph, emo); baseGlyph = res.glyph; })(); const name = document.createElement('div'); name.className = 'nm'; name.textContent = e.name || ''; card.addEventListener('click', async () => { if (!licenseValid || !e.supports_skin_tone) { performEmojiAction(baseGlyph).catch(console.error); return; } const idx = await prefIdxPromise; const base = e.emoji_base || stripSkinTone(baseGlyph); if (toneLock && idx >= 0) { const variant = withSkinTone(base, SKIN_TONES[idx].ch); performEmojiAction(variant).catch(console.error); return; } removePopover(); const pop = buildTonePopover(base, idx, async (newIdx, tone) => { const variant = withSkinTone(base, tone.ch); if (toneLock && idx < 0) { await setPreferredToneIndex(newIdx); showToast('Preferred tone set'); } performEmojiAction(variant).catch(console.error); removePopover(); }); showPopoverNear(card, pop); }); card.append(emo, name); list.appendChild(card); } // pick a tofu cell const el = document.querySelector('.card .emo'); const raw = el?.textContent || ''; console.log('RAW points:', [...raw].map(c=>c.codePointAt(0).toString(16))); if (window.twemoji) { const cp = window.twemoji.convert.toCodePoint(raw); console.log('Twemoji codepoints:', cp); console.log('Canonical from codepoints:', window.twemoji.convert.fromCodePoint(cp)); } loadSettings().catch(() => {}); // Example verify-on-boot (disabled in dev): // (async function verifyLicenseOnBoot(){ // const { licenseKey, lastLicenseCheck } = await chrome.storage.local.get(['licenseKey','lastLicenseCheck']); // if (!licenseKey) return; // const day = 24*60*60*1000; if (Date.now() - (lastLicenseCheck||0) < day) return; // try { // const acct = await accountId(); // const res = await fetch('https://YOUR-API/license/verify', { // method: 'POST', headers: { 'Content-Type': 'application/json' }, // body: JSON.stringify({ key: licenseKey, account_id: acct.id }) // }); // const data = await res.json(); // const ok = !!data.ok; await chrome.storage.local.set({ licenseValid: ok, lastLicenseCheck: Date.now() }); // licenseValid = ok; applyLicenseUI(); // } catch {} // })(); // Re-render tone section when opening settings (in case lock/pref changed during session) const __openSheet = openSheet; openSheet = function(){ __openSheet(); const genBtn = document.querySelector('#settings-sheet .tab[data-tab="general"]'); const proBtn = document.querySelector('#settings-sheet .tab[data-tab="pro"]'); genBtn?.classList.add('active'); proBtn?.classList.remove('active'); document.getElementById('tab-general')?.classList.add('active'); document.getElementById('tab-pro')?.classList.remove('active'); renderToneSettingsSection(); };