ux: improve skintone interaction & active state behavior
- Remove swatch modal → replaced with inline tone row UX - Add active card highlight and tone selection row spanning 4 columns - Apply tone immediately on click (copy/insert according to settings) - Add tooltip on hover for names (replacing marquee on hover) - Enable marquee animation only for overflowed text when active - Integrate forbid & non-toneable filters matching site policy (LGBT & fantasy exclusions) - Sync tone whitelist (roles like technologist, scientist, firefighter, etc.) - Sync tone blacklist (e.g. merman, mermaid, deaf man/woman, woman beard, men with bunny ears) - Add overflow-y hidden toggle when settings modal shown - Move inline styles to style.css for cleaner structure - Refactor panel.js for maintainable tone row injection logic
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Dewemoji - Emojis Made Effortless",
|
"name": "Dewemoji - Emojis Made Effortless",
|
||||||
"description": "Find and copy emojis instantly. Optional Pro license unlocks tone lock, insert mode, and more.",
|
"description": "Find and copy emojis instantly. Optional Pro license unlocks tone lock, insert mode, and more.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"offline_enabled": false,
|
"offline_enabled": false,
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage",
|
"storage",
|
||||||
|
|||||||
35
panel.html
35
panel.html
@@ -5,6 +5,16 @@
|
|||||||
<title>Emoji Widget</title>
|
<title>Emoji Widget</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link href="styles.css" rel="stylesheet">
|
<link href="styles.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
.modal { position: fixed; inset: 0; display:flex; align-items:center; justify-content:center; z-index: 1000; }
|
||||||
|
.modal[hidden] { display:none; }
|
||||||
|
.modal-card { background: var(--c-bg, #0b1220); color: var(--c-fg, #e5e7eb); border: 1px solid var(--c-border, rgba(255,255,255,.1)); border-radius: 14px; width: min(420px, 92vw); padding: 16px; box-shadow: 0 20px 50px rgba(0,0,0,.45); }
|
||||||
|
.theme-light .modal-card { background: #fff; color: #111827; border-color: rgba(0,0,0,.08); }
|
||||||
|
.modal-head { display:flex; align-items:center; gap:12px; margin-bottom: 8px; }
|
||||||
|
.modal-logo { width: 28px; height: 28px; border-radius: 6px; }
|
||||||
|
.modal-body { font-size: 14px; opacity: .9; margin: 8px 0 14px; }
|
||||||
|
.modal-actions { display:flex; justify-content:flex-end; gap:10px; }
|
||||||
|
</style>
|
||||||
<script src="vendor/twemoji.min.js"></script>
|
<script src="vendor/twemoji.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -86,7 +96,12 @@
|
|||||||
<div class="row" style="margin-top:6px;">
|
<div class="row" style="margin-top:6px;">
|
||||||
<button id="license-edit" class="link" style="width: fit-content">Change key</button>
|
<button id="license-edit" class="link" style="width: fit-content">Change key</button>
|
||||||
<button id="license-cancel-edit" class="link" style="display:none;width: fit-content">Cancel</button>
|
<button id="license-cancel-edit" class="link" style="display:none;width: fit-content">Cancel</button>
|
||||||
<span id="license-status" class="muted">Free mode — Pro features locked.</span>
|
<span id="license-status" class="muted">
|
||||||
|
Free mode — Pro features locked.
|
||||||
|
<br><a href="https://dewemoji.com/pricing" target="_blank" class="cta-link" style="margin-left:6px; text-decoration: underline; font-weight: 500;">
|
||||||
|
🔓 Upgrade to Pro
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -107,7 +122,23 @@
|
|||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
<!-- Branded confirm modal -->
|
||||||
|
<div id="confirm-backdrop" class="backdrop" hidden></div>
|
||||||
|
<div id="confirm-modal" class="modal" hidden role="dialog" aria-modal="true" aria-labelledby="confirm-title" aria-describedby="confirm-message">
|
||||||
|
<div class="modal-card">
|
||||||
|
<div class="modal-head">
|
||||||
|
<img src="assets/icon-128.png" alt="Dewemoji" class="modal-logo" />
|
||||||
|
<h3 id="confirm-title">Confirm</h3>
|
||||||
|
</div>
|
||||||
|
<div id="confirm-message" class="modal-body">Are you sure?</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button id="confirm-cancel" class="btn ghost">Cancel</button>
|
||||||
|
<button id="confirm-ok" class="btn">OK</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||||
<script src="panel.js"></script>
|
<script src="panel.js?ver=1.0.1"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
565
panel.js
565
panel.js
@@ -23,25 +23,85 @@ const licenseDeactivateBtn = document.getElementById('license-deactivate');
|
|||||||
const licenseEditBtn = document.getElementById('license-edit');
|
const licenseEditBtn = document.getElementById('license-edit');
|
||||||
const licenseCancelEditBtn = document.getElementById('license-cancel-edit');
|
const licenseCancelEditBtn = document.getElementById('license-cancel-edit');
|
||||||
|
|
||||||
|
// --- Branded confirm modal helper ---
|
||||||
|
function showConfirmModal(opts = {}) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const modal = document.getElementById('confirm-modal');
|
||||||
|
const back = document.getElementById('confirm-backdrop');
|
||||||
|
const title = document.getElementById('confirm-title');
|
||||||
|
const msg = document.getElementById('confirm-message');
|
||||||
|
const okBtn = document.getElementById('confirm-ok');
|
||||||
|
const noBtn = document.getElementById('confirm-cancel');
|
||||||
|
if (!modal || !back) return resolve(window.confirm(opts.message || 'Are you sure?'));
|
||||||
|
|
||||||
|
title.textContent = opts.title || 'Confirm';
|
||||||
|
msg.textContent = opts.message || 'Are you sure?';
|
||||||
|
okBtn.textContent = opts.okText || 'OK';
|
||||||
|
noBtn.textContent = opts.cancelText || 'Cancel';
|
||||||
|
|
||||||
|
function close(val){
|
||||||
|
modal.setAttribute('hidden','');
|
||||||
|
back.setAttribute('hidden','');
|
||||||
|
document.removeEventListener('keydown', onKey);
|
||||||
|
okBtn.removeEventListener('click', onOk);
|
||||||
|
noBtn.removeEventListener('click', onNo);
|
||||||
|
back.removeEventListener('click', onNo);
|
||||||
|
resolve(val);
|
||||||
|
}
|
||||||
|
function onOk(){ close(true); }
|
||||||
|
function onNo(){ close(false); }
|
||||||
|
function onKey(e){ if (e.key === 'Escape') close(false); if (e.key === 'Enter') close(true); }
|
||||||
|
|
||||||
|
back.removeAttribute('hidden');
|
||||||
|
modal.removeAttribute('hidden');
|
||||||
|
document.addEventListener('keydown', onKey);
|
||||||
|
okBtn.addEventListener('click', onOk);
|
||||||
|
noBtn.addEventListener('click', onNo);
|
||||||
|
back.addEventListener('click', onNo);
|
||||||
|
// focus primary
|
||||||
|
setTimeout(()=> okBtn.focus(), 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// --- License busy helpers (UI feedback for activate/deactivate) ---
|
// --- License busy helpers (UI feedback for activate/deactivate) ---
|
||||||
let __licensePrev = { text: '', disabled: false };
|
let __licensePrev = {
|
||||||
|
text: '',
|
||||||
|
activateDisabled: false,
|
||||||
|
keyDisabled: false,
|
||||||
|
deactivateDisabled: false,
|
||||||
|
editDisabled: false,
|
||||||
|
cancelDisabled: false,
|
||||||
|
};
|
||||||
function setLicenseBusy(on, label){
|
function setLicenseBusy(on, label){
|
||||||
const btn = licenseActivateBtn;
|
const btn = licenseActivateBtn;
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
if (on) {
|
if (on) {
|
||||||
|
// snapshot current states
|
||||||
__licensePrev.text = btn.textContent;
|
__licensePrev.text = btn.textContent;
|
||||||
__licensePrev.disabled = btn.disabled;
|
__licensePrev.activateDisabled = !!btn.disabled;
|
||||||
|
__licensePrev.keyDisabled = !!(licenseKeyEl && licenseKeyEl.disabled);
|
||||||
|
__licensePrev.deactivateDisabled = !!(licenseDeactivateBtn && licenseDeactivateBtn.disabled);
|
||||||
|
__licensePrev.editDisabled = !!(licenseEditBtn && licenseEditBtn.disabled);
|
||||||
|
__licensePrev.cancelDisabled = !!(licenseCancelEditBtn && licenseCancelEditBtn.disabled);
|
||||||
|
|
||||||
|
// set busy states
|
||||||
btn.textContent = label || 'Verifying…';
|
btn.textContent = label || 'Verifying…';
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
licenseKeyEl && (licenseKeyEl.disabled = true);
|
if (licenseKeyEl) licenseKeyEl.disabled = true;
|
||||||
licenseDeactivateBtn && (licenseDeactivateBtn.disabled = true);
|
if (licenseDeactivateBtn) licenseDeactivateBtn.disabled = true;
|
||||||
licenseEditBtn && (licenseEditBtn.disabled = true);
|
if (licenseEditBtn) licenseEditBtn.disabled = true;
|
||||||
licenseCancelEditBtn && (licenseCancelEditBtn.disabled = true);
|
if (licenseCancelEditBtn) licenseCancelEditBtn.disabled = true;
|
||||||
licenseStatusEl && (licenseStatusEl.textContent = 'Checking license…');
|
if (licenseStatusEl) licenseStatusEl.textContent = 'Checking license…';
|
||||||
} else {
|
} else {
|
||||||
|
// restore previous states
|
||||||
btn.textContent = __licensePrev.text || 'Activate';
|
btn.textContent = __licensePrev.text || 'Activate';
|
||||||
btn.disabled = __licensePrev.disabled || false;
|
btn.disabled = !!__licensePrev.activateDisabled;
|
||||||
// Restore enabled state based on current license view
|
if (licenseKeyEl) licenseKeyEl.disabled = !!__licensePrev.keyDisabled;
|
||||||
|
if (licenseDeactivateBtn) licenseDeactivateBtn.disabled = !!__licensePrev.deactivateDisabled;
|
||||||
|
if (licenseEditBtn) licenseEditBtn.disabled = !!__licensePrev.editDisabled;
|
||||||
|
if (licenseCancelEditBtn) licenseCancelEditBtn.disabled = !!__licensePrev.cancelDisabled;
|
||||||
|
|
||||||
|
// Re-apply UI (visibility/display) for current license state
|
||||||
applyLicenseUI();
|
applyLicenseUI();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,8 +159,28 @@ async function loadCategories() {
|
|||||||
try {
|
try {
|
||||||
const res = await fetch(`${API.base}${API.cats}`);
|
const res = await fetch(`${API.base}${API.cats}`);
|
||||||
if (res && res.ok) {
|
if (res && res.ok) {
|
||||||
CAT_MAP = await res.json();
|
const data = await res.json();
|
||||||
ok = true;
|
// Normalize payloads:
|
||||||
|
// A) New shape: { items: [ { name: 'People & Body', subcategories: ['person','person-gesture'] }, ... ] }
|
||||||
|
// B) Legacy shape: { 'People & Body': ['person','person-gesture'], ... }
|
||||||
|
if (data && Array.isArray(data.items)) {
|
||||||
|
const map = {};
|
||||||
|
for (const it of data.items) {
|
||||||
|
if (!it || typeof it.name !== 'string') continue;
|
||||||
|
const name = it.name;
|
||||||
|
const subsRaw = Array.isArray(it.subcategories) ? it.subcategories : [];
|
||||||
|
const subs = subsRaw
|
||||||
|
.map(s => (typeof s === 'string') ? s : (s && (s.slug || s.name) || ''))
|
||||||
|
.filter(Boolean);
|
||||||
|
map[name] = subs;
|
||||||
|
}
|
||||||
|
CAT_MAP = map;
|
||||||
|
} else if (data && typeof data === 'object') {
|
||||||
|
CAT_MAP = data; // assume already in object-map form
|
||||||
|
} else {
|
||||||
|
throw new Error('bad_categories_payload');
|
||||||
|
}
|
||||||
|
ok = Object.keys(CAT_MAP || {}).length > 0;
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
@@ -134,12 +214,16 @@ function populateSubcategorySelect(category) {
|
|||||||
const subSel = document.getElementById('sub');
|
const subSel = document.getElementById('sub');
|
||||||
if (!subSel) return;
|
if (!subSel) return;
|
||||||
subSel.innerHTML = `<option value="">All subcategories</option>`;
|
subSel.innerHTML = `<option value="">All subcategories</option>`;
|
||||||
const subs = (CAT_MAP?.[category] || []).slice().sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base", numeric: true }));
|
const raw = (CAT_MAP?.[category] || []).slice();
|
||||||
|
const subs = raw
|
||||||
|
.map(s => (typeof s === 'string') ? s : (s && (s.slug || s.name) || ''))
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base', numeric: true }));
|
||||||
subs.forEach(sc => {
|
subs.forEach(sc => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = sc;
|
opt.value = sc;
|
||||||
opt.textContent = sc;
|
opt.textContent = sc;
|
||||||
subSel.appendChild(opt);
|
subSel.appendChild(opt);
|
||||||
});
|
});
|
||||||
subSel.disabled = subs.length === 0;
|
subSel.disabled = subs.length === 0;
|
||||||
subSel.classList.toggle('opacity-50', subs.length === 0);
|
subSel.classList.toggle('opacity-50', subs.length === 0);
|
||||||
@@ -284,7 +368,145 @@ const SKIN_TONES = [
|
|||||||
const TONE_SLUGS = SKIN_TONES.map(t => t.slug);
|
const TONE_SLUGS = SKIN_TONES.map(t => t.slug);
|
||||||
const STRIP_TONE_RE = /[\u{1F3FB}-\u{1F3FF}]/gu;
|
const STRIP_TONE_RE = /[\u{1F3FB}-\u{1F3FF}]/gu;
|
||||||
function stripSkinTone(s){ return (s||'').replace(STRIP_TONE_RE,''); }
|
function stripSkinTone(s){ return (s||'').replace(STRIP_TONE_RE,''); }
|
||||||
function withSkinTone(base, ch){ return `${base||''}${ch||''}`; }
|
|
||||||
|
// === Policy: forbid list & non‑toneable list (mirror of site script.js) ===
|
||||||
|
const FORBID_NAMES = new Set([
|
||||||
|
'woman: beard',
|
||||||
|
'man with veil',
|
||||||
|
'pregnant man',
|
||||||
|
'man with bunny ears',
|
||||||
|
'men with bunny ears',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const NON_TONEABLE_NAMES = new Set([
|
||||||
|
// Core non-toneable bases
|
||||||
|
'mechanical arm', 'mechanical leg','anatomical heart','brain','lungs','tooth','bone','eyes','eye','tongue','mouth','biting lips',
|
||||||
|
// Fantasy subset (problematic gendered variants remain but not toneable)
|
||||||
|
'genie','man genie','woman genie','zombie','man zombie','woman zombie','troll',
|
||||||
|
// Activities & gestures we treat as non-toneable
|
||||||
|
'skier','snowboarder','speaking head','bust in silhouette','busts in silhouette','people hugging','family','footprints','fingerprint',
|
||||||
|
'people fencing',
|
||||||
|
// Directional variants we keep but not toneable
|
||||||
|
'person walking facing right','person running facing right','person kneeling facing right',
|
||||||
|
// Accessibility
|
||||||
|
'deaf man','deaf woman',
|
||||||
|
// Merfolk
|
||||||
|
'merman','mermaid'
|
||||||
|
]);
|
||||||
|
|
||||||
|
function normName(e){
|
||||||
|
return String(e?.name || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
function isForbiddenEntry(e){
|
||||||
|
const n = normName(e);
|
||||||
|
return FORBID_NAMES.has(n);
|
||||||
|
}
|
||||||
|
function isNonToneableByPolicy(e){
|
||||||
|
const n = normName(e);
|
||||||
|
if (!n) return false;
|
||||||
|
// Families never toneable
|
||||||
|
if (isFamilyEntry(e)) return true;
|
||||||
|
// Explicit list
|
||||||
|
if (NON_TONEABLE_NAMES.has(n)) return true;
|
||||||
|
// Buckets by subcategory we consider symbol-like (person-symbol) or family
|
||||||
|
const sc = String(e?.subcategory || '').toLowerCase();
|
||||||
|
if (sc === 'family' || sc === 'person-symbol') return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function isToneableByPolicy(e){
|
||||||
|
// Must advertise tone support, must not be family, and not on our non-toneable list.
|
||||||
|
if (!e?.supports_skin_tone) return false;
|
||||||
|
if (isNonToneableByPolicy(e)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tone applicability heuristic ---
|
||||||
|
function canApplyToneTo(emojiStr){
|
||||||
|
if (!emojiStr) return false;
|
||||||
|
const cps = Array.from(emojiStr);
|
||||||
|
// If any explicit Emoji_Modifier_Base exists, it's safe
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < cps.length; i++) {
|
||||||
|
if (/\p{Emoji_Modifier_Base}/u.test(cps[i])) return true;
|
||||||
|
}
|
||||||
|
} catch { /* property escapes may not be supported; fall through */ }
|
||||||
|
|
||||||
|
// Fallback heuristic:
|
||||||
|
// Count human bases (👨 U+1F468, 👩 U+1F469, 🧑 U+1F9D1)
|
||||||
|
let humanCount = 0;
|
||||||
|
for (let i = 0; i < cps.length; i++) {
|
||||||
|
const cp = cps[i].codePointAt(0);
|
||||||
|
if (cp === 0x1F468 || cp === 0x1F469 || cp === 0x1F9D1) humanCount++;
|
||||||
|
}
|
||||||
|
// Allow tones for couples (2 humans) and single-person emojis; block families (3+ humans)
|
||||||
|
if (humanCount >= 3) return false; // families
|
||||||
|
if (humanCount === 2) return true; // couples (kiss, couple-with-heart, etc.)
|
||||||
|
if (humanCount === 1) return true; // single human (ok)
|
||||||
|
|
||||||
|
// Non-human (hands etc.) — many support tone, but those have modifier base property
|
||||||
|
// Without the property present, avoid applying to prevent stray squares
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Detect multi-human ZWJ sequences
|
||||||
|
function isMultiHuman(s){
|
||||||
|
if (!s) return false;
|
||||||
|
const hasZWJ = /\u200D/.test(s);
|
||||||
|
if (!hasZWJ) return false;
|
||||||
|
let count = 0;
|
||||||
|
for (const ch of Array.from(s)) {
|
||||||
|
const cp = ch.codePointAt(0);
|
||||||
|
if (cp === 0x1F468 || cp === 0x1F469 || cp === 0x1F9D1) count++;
|
||||||
|
}
|
||||||
|
return count >= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Detect family entries by metadata
|
||||||
|
function isFamilyEntry(e){
|
||||||
|
const n = String(e?.name||'').toLowerCase();
|
||||||
|
const sc = String(e?.subcategory||'').toLowerCase();
|
||||||
|
return n.startsWith('family:') || sc === 'family';
|
||||||
|
}
|
||||||
|
|
||||||
|
function withSkinTone(base, ch){
|
||||||
|
const emojiChar = base || '';
|
||||||
|
const modifierChar = ch || '';
|
||||||
|
if (!emojiChar || !modifierChar) return emojiChar;
|
||||||
|
|
||||||
|
// Guard: if we heuristically think tone shouldn't be applied, return base
|
||||||
|
if (!canApplyToneTo(emojiChar)) return emojiChar;
|
||||||
|
|
||||||
|
// Split into codepoints (keeps surrogate pairs intact)
|
||||||
|
const cps = Array.from(emojiChar);
|
||||||
|
|
||||||
|
// Try to find the last Emoji_Modifier_Base in the sequence
|
||||||
|
let lastBaseIdx = -1;
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < cps.length; i++) {
|
||||||
|
if (/\p{Emoji_Modifier_Base}/u.test(cps[i])) lastBaseIdx = i;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Property escapes not supported → handled by fallback below
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: if no base detected, treat first human as base (man, woman, person)
|
||||||
|
if (lastBaseIdx === -1) {
|
||||||
|
const cp0 = cps[0] ? cps[0].codePointAt(0) : 0;
|
||||||
|
const HUMAN_BASES = new Set([0x1F468, 0x1F469, 0x1F9D1]); // 👨, 👩, 🧑
|
||||||
|
if (HUMAN_BASES.has(cp0)) lastBaseIdx = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastBaseIdx === -1) return emojiChar; // avoid appending stray square
|
||||||
|
|
||||||
|
// Insert after the base (and after VS16 if present)
|
||||||
|
const afterBase = (idx) => {
|
||||||
|
if (cps[idx + 1] && cps[idx + 1].codePointAt(0) === 0xFE0F) return idx + 2; // VS16
|
||||||
|
return idx + 1;
|
||||||
|
};
|
||||||
|
const insertPos = afterBase(lastBaseIdx);
|
||||||
|
const out = cps.slice(0, insertPos).concat([modifierChar], cps.slice(insertPos));
|
||||||
|
return out.join('');
|
||||||
|
}
|
||||||
|
|
||||||
// Tone helpers: keep globals in sync and expose helpers for slug
|
// Tone helpers: keep globals in sync and expose helpers for slug
|
||||||
async function getPreferredToneIndex(){
|
async function getPreferredToneIndex(){
|
||||||
@@ -795,6 +1017,87 @@ function showToast(text='Done') {
|
|||||||
showToast._t = setTimeout(() => toastEl.classList.remove('show'), 1400);
|
showToast._t = setTimeout(() => toastEl.classList.remove('show'), 1400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Tone row UX (row-spanning picker) =========================
|
||||||
|
const GRID_COLS = 4; // grid is 4 columns in the panel
|
||||||
|
let __toneRow = null; // the expanded row element (if any)
|
||||||
|
let __activeCard = null; // the card that owns the expanded row
|
||||||
|
let __prevOverflowY = '';
|
||||||
|
|
||||||
|
function closeToneRow(){
|
||||||
|
if (__activeCard) {
|
||||||
|
__activeCard.classList.remove('active');
|
||||||
|
__activeCard.style.background = '';
|
||||||
|
__activeCard.style.boxShadow = '';
|
||||||
|
if (typeof __activeCard.__setNameStatic === 'function') {
|
||||||
|
__activeCard.__setNameStatic();
|
||||||
|
}
|
||||||
|
__activeCard = null;
|
||||||
|
}
|
||||||
|
if (__toneRow && __toneRow.parentNode) __toneRow.parentNode.removeChild(__toneRow);
|
||||||
|
__toneRow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildToneRow(baseGlyph, activeIdx, onPick){
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'tone-row';
|
||||||
|
|
||||||
|
SKIN_TONES.forEach((t,i)=>{
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'tone-option';
|
||||||
|
|
||||||
|
const variant = canApplyToneTo(baseGlyph) ? withSkinTone(baseGlyph, t.ch) : baseGlyph;
|
||||||
|
const span = document.createElement('span');
|
||||||
|
ensureRenderableAndGlyph(variant, span);
|
||||||
|
btn.appendChild(span);
|
||||||
|
|
||||||
|
if (i === activeIdx) btn.classList.add('selected');
|
||||||
|
btn.addEventListener('click', ()=> onPick(i, t, variant));
|
||||||
|
row.appendChild(btn);
|
||||||
|
});
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openToneRowFor(card, item, baseGlyph){
|
||||||
|
// Toggle behavior
|
||||||
|
if (__activeCard === card) { closeToneRow(); return; }
|
||||||
|
// Close any previous
|
||||||
|
closeToneRow();
|
||||||
|
|
||||||
|
__activeCard = card;
|
||||||
|
// Visual highlight for the active owner card
|
||||||
|
card.classList.add('active');
|
||||||
|
card.style.background = 'var(--c-chip,#eef2ff)';
|
||||||
|
card.style.boxShadow = '0 0 0 2px rgba(59,130,246,.65) inset';
|
||||||
|
if (isToneableByPolicy(item) && typeof card.__setNameMarquee === 'function') {
|
||||||
|
card.__setNameMarquee();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decide where to insert: after the end of this 4-col row
|
||||||
|
const cardsOnly = Array.from(list.querySelectorAll('.card'));
|
||||||
|
const idx = cardsOnly.indexOf(card);
|
||||||
|
const rowStart = Math.floor(idx / GRID_COLS) * GRID_COLS;
|
||||||
|
const rowEnd = Math.min(rowStart + GRID_COLS - 1, cardsOnly.length - 1);
|
||||||
|
const anchor = cardsOnly[rowEnd] || card;
|
||||||
|
|
||||||
|
const prefIdxPromise = getPreferredToneIndex();
|
||||||
|
|
||||||
|
(async ()=>{
|
||||||
|
const currentIdx = await prefIdxPromise;
|
||||||
|
const base = stripSkinTone(item.emoji_base || baseGlyph);
|
||||||
|
__toneRow = buildToneRow(base, currentIdx, async (newIdx, tone, variant)=>{
|
||||||
|
if (toneLock && currentIdx < 0) {
|
||||||
|
await setPreferredToneIndex(newIdx);
|
||||||
|
showToast('Preferred tone set');
|
||||||
|
}
|
||||||
|
await performEmojiAction(variant);
|
||||||
|
closeToneRow();
|
||||||
|
});
|
||||||
|
anchor.insertAdjacentElement('afterend', __toneRow);
|
||||||
|
try { __toneRow.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } catch(_){}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
// (obsolete) placeholder; real renderCard defined later with Windows support + Pro tone UI
|
// (obsolete) placeholder; real renderCard defined later with Windows support + Pro tone UI
|
||||||
function renderCard(e) {}
|
function renderCard(e) {}
|
||||||
// --- Popover helpers for skin tone picker ---
|
// --- Popover helpers for skin tone picker ---
|
||||||
@@ -823,14 +1126,23 @@ function buildTonePopover(base, activeIdx, onPick){
|
|||||||
el.setAttribute('role','dialog');
|
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;';
|
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) => {
|
SKIN_TONES.forEach((t,i) => {
|
||||||
const b = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
b.className = 'tone-btn';
|
btn.className = 'tone-btn';
|
||||||
b.textContent = withSkinTone(base, t.ch);
|
btn.title = t.label;
|
||||||
b.title = t.label;
|
btn.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)';
|
||||||
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');
|
// Build the toned variant only when applicable; else preview base to avoid squares
|
||||||
b.addEventListener('click', () => onPick(i, t));
|
const variant = canApplyToneTo(base) ? withSkinTone(base, t.ch) : base;
|
||||||
el.appendChild(b);
|
const span = document.createElement('span');
|
||||||
|
span.style.display = 'inline-flex';
|
||||||
|
span.style.alignItems = 'center';
|
||||||
|
span.style.justifyContent = 'center';
|
||||||
|
ensureRenderableAndGlyph(variant, span);
|
||||||
|
btn.appendChild(span);
|
||||||
|
|
||||||
|
if (i === activeIdx) btn.classList.add('selected');
|
||||||
|
btn.addEventListener('click', () => onPick(i, t));
|
||||||
|
el.appendChild(btn);
|
||||||
});
|
});
|
||||||
setTimeout(() => document.addEventListener('click', handleOutsideClose, true), 0);
|
setTimeout(() => document.addEventListener('click', handleOutsideClose, true), 0);
|
||||||
return el;
|
return el;
|
||||||
@@ -869,6 +1181,15 @@ async function saveSettings() {
|
|||||||
await chrome.storage.local.set({ licenseValid, actionMode, licenseKey: licenseKeyEl?.value || '' });
|
await chrome.storage.local.set({ licenseValid, actionMode, licenseKey: licenseKeyEl?.value || '' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Helper to render Free status with CTA ---
|
||||||
|
function freeStatusHTML(){
|
||||||
|
return 'Free mode — Pro features locked.' +
|
||||||
|
'<br><a href="https://dewemoji.com/pricing" target="_blank" class="cta-link"' +
|
||||||
|
' style="margin-left:6px; text-decoration: underline; font-weight:500;">' +
|
||||||
|
'🔓 Upgrade to Pro' +
|
||||||
|
'</a>';
|
||||||
|
}
|
||||||
|
|
||||||
function setLicenseEditMode(on) {
|
function setLicenseEditMode(on) {
|
||||||
if (!licenseKeyEl) return;
|
if (!licenseKeyEl) return;
|
||||||
const isOn = !!on;
|
const isOn = !!on;
|
||||||
@@ -909,7 +1230,7 @@ function applyLicenseUI() {
|
|||||||
// Force visible selection to 'copy' for clarity
|
// Force visible selection to 'copy' for clarity
|
||||||
const copy = modeGroup.querySelector('input[type="radio"][value="copy"]');
|
const copy = modeGroup.querySelector('input[type="radio"][value="copy"]');
|
||||||
if (copy) copy.checked = true;
|
if (copy) copy.checked = true;
|
||||||
licenseStatusEl && (licenseStatusEl.textContent = 'Free mode — Pro features locked.');
|
licenseStatusEl && (licenseStatusEl.innerHTML = freeStatusHTML());
|
||||||
} else {
|
} else {
|
||||||
// Restore selected mode if Pro; if current actionMode is pro, check it
|
// Restore selected mode if Pro; if current actionMode is pro, check it
|
||||||
const target = modeGroup.querySelector(`input[type="radio"][value="${actionMode}"]`) ||
|
const target = modeGroup.querySelector(`input[type="radio"][value="${actionMode}"]`) ||
|
||||||
@@ -932,6 +1253,8 @@ function applyLicenseUI() {
|
|||||||
if (licenseCancelEditBtn) licenseCancelEditBtn.style.display = 'none';
|
if (licenseCancelEditBtn) licenseCancelEditBtn.style.display = 'none';
|
||||||
}
|
}
|
||||||
setStatusTag();
|
setStatusTag();
|
||||||
|
// --- PATCH: Refresh tone section after license state changes ---
|
||||||
|
try { renderToneSettingsSection(); } catch(_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyModeUI() {
|
function applyModeUI() {
|
||||||
@@ -1023,6 +1346,13 @@ function renderToneSettingsSection(){
|
|||||||
pal.innerHTML = '';
|
pal.innerHTML = '';
|
||||||
const lockEl = sec.querySelector('#tone-lock');
|
const lockEl = sec.querySelector('#tone-lock');
|
||||||
lockEl.checked = !!toneLock;
|
lockEl.checked = !!toneLock;
|
||||||
|
// Disable lock checkbox and visually dim if license is invalid
|
||||||
|
lockEl.disabled = !licenseValid;
|
||||||
|
if (!licenseValid) {
|
||||||
|
lockEl.parentElement.style.opacity = '0.5';
|
||||||
|
} else {
|
||||||
|
lockEl.parentElement.style.opacity = '';
|
||||||
|
}
|
||||||
lockEl.onchange = async (e)=>{ await setToneLock(!!e.target.checked); showToast(toneLock ? 'Tone lock enabled' : 'Tone lock disabled'); };
|
lockEl.onchange = async (e)=>{ await setToneLock(!!e.target.checked); showToast(toneLock ? 'Tone lock enabled' : 'Tone lock disabled'); };
|
||||||
const idx = TONE_SLUGS.indexOf(preferredToneSlug);
|
const idx = TONE_SLUGS.indexOf(preferredToneSlug);
|
||||||
const basePreview = '✋';
|
const basePreview = '✋';
|
||||||
@@ -1033,6 +1363,12 @@ function renderToneSettingsSection(){
|
|||||||
b.textContent = basePreview + t.ch; // small preview
|
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)';
|
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');
|
if (i === idx) b.classList.add('selected');
|
||||||
|
// Disable tone chips and visually dim if license is invalid
|
||||||
|
if (!licenseValid) {
|
||||||
|
b.disabled = true;
|
||||||
|
b.style.opacity = '0.5';
|
||||||
|
b.style.pointerEvents = 'none';
|
||||||
|
}
|
||||||
b.addEventListener('click', async ()=>{
|
b.addEventListener('click', async ()=>{
|
||||||
await setPreferredToneIndex(i);
|
await setPreferredToneIndex(i);
|
||||||
showToast('Preferred tone set');
|
showToast('Preferred tone set');
|
||||||
@@ -1051,10 +1387,24 @@ function renderToneSettingsSection(){
|
|||||||
function openSheet() {
|
function openSheet() {
|
||||||
sheet.removeAttribute('hidden'); backdrop.removeAttribute('hidden');
|
sheet.removeAttribute('hidden'); backdrop.removeAttribute('hidden');
|
||||||
requestAnimationFrame(() => { sheet.classList.add('show'); backdrop.classList.add('show'); });
|
requestAnimationFrame(() => { sheet.classList.add('show'); backdrop.classList.add('show'); });
|
||||||
|
// lock page scroll while sheet is open
|
||||||
|
try {
|
||||||
|
__prevOverflowY = document.documentElement.style.overflowY || '';
|
||||||
|
document.documentElement.style.overflowY = 'hidden';
|
||||||
|
// also lock body in case the site CSS scrolls body instead of root
|
||||||
|
document.body && (document.body.style.overflowY = 'hidden');
|
||||||
|
} catch(_) {}
|
||||||
}
|
}
|
||||||
function closeSheet() {
|
function closeSheet() {
|
||||||
sheet.classList.remove('show'); backdrop.classList.remove('show');
|
sheet.classList.remove('show'); backdrop.classList.remove('show');
|
||||||
setTimeout(() => { sheet.setAttribute('hidden',''); backdrop.setAttribute('hidden',''); }, 180);
|
setTimeout(() => {
|
||||||
|
sheet.setAttribute('hidden',''); backdrop.setAttribute('hidden','');
|
||||||
|
// restore page scroll after the closing animation completes
|
||||||
|
try {
|
||||||
|
document.documentElement.style.overflowY = __prevOverflowY || '';
|
||||||
|
document.body && (document.body.style.overflowY = '');
|
||||||
|
} catch(_) {}
|
||||||
|
}, 180);
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsBtn?.addEventListener('click', openSheet);
|
settingsBtn?.addEventListener('click', openSheet);
|
||||||
@@ -1113,6 +1463,8 @@ licenseActivateBtn?.addEventListener('click', async () => {
|
|||||||
applyLicenseUI();
|
applyLicenseUI();
|
||||||
setLicenseEditMode(false);
|
setLicenseEditMode(false);
|
||||||
setStatusTag();
|
setStatusTag();
|
||||||
|
// PATCH: Immediately re-render tone section to enable controls after activation
|
||||||
|
renderToneSettingsSection();
|
||||||
showToast('License activated ✓');
|
showToast('License activated ✓');
|
||||||
|
|
||||||
// Refresh results so Pro headers/tier take effect immediately
|
// Refresh results so Pro headers/tier take effect immediately
|
||||||
@@ -1140,7 +1492,12 @@ licenseCancelEditBtn?.addEventListener('click', async () => {
|
|||||||
|
|
||||||
licenseDeactivateBtn?.addEventListener('click', async () => {
|
licenseDeactivateBtn?.addEventListener('click', async () => {
|
||||||
if (!licenseValid) return;
|
if (!licenseValid) return;
|
||||||
const ok = confirm('Deactivate Pro on this device?');
|
const ok = await showConfirmModal({
|
||||||
|
title: 'Deactivate Dewemoji Pro?',
|
||||||
|
message: 'Deactivate Pro on this device?',
|
||||||
|
okText: 'Deactivate',
|
||||||
|
cancelText: 'Cancel'
|
||||||
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
setLicenseBusy(true, 'Deactivating…');
|
setLicenseBusy(true, 'Deactivating…');
|
||||||
try {
|
try {
|
||||||
@@ -1155,6 +1512,8 @@ licenseDeactivateBtn?.addEventListener('click', async () => {
|
|||||||
await chrome.storage.local.set({ licenseValid: false, licenseKey: '' });
|
await chrome.storage.local.set({ licenseValid: false, licenseKey: '' });
|
||||||
if (licenseKeyEl) licenseKeyEl.value = '';
|
if (licenseKeyEl) licenseKeyEl.value = '';
|
||||||
applyLicenseUI();
|
applyLicenseUI();
|
||||||
|
// PATCH: Immediately re-render tone section to disable controls after deactivation
|
||||||
|
renderToneSettingsSection();
|
||||||
licenseKeyCurrent = '';
|
licenseKeyCurrent = '';
|
||||||
setLicenseEditMode(false);
|
setLicenseEditMode(false);
|
||||||
showToast('License deactivated');
|
showToast('License deactivated');
|
||||||
@@ -1312,60 +1671,125 @@ function renderCard(e) {
|
|||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'card';
|
card.className = 'card';
|
||||||
card.title = e.name || '';
|
card.title = e.name || '';
|
||||||
|
// Policy: skip forbidden entries entirely
|
||||||
|
if (isForbiddenEntry(e)) return;
|
||||||
|
const FAMILY = isFamilyEntry(e);
|
||||||
|
|
||||||
const emo = document.createElement('div');
|
const emo = document.createElement('div');
|
||||||
emo.className = 'emo';
|
emo.className = 'emo';
|
||||||
|
|
||||||
let baseGlyph = e.emoji || e.symbol || e.text || '';
|
let baseGlyph = e.emoji || e.symbol || e.text || '';
|
||||||
|
// Strictly sanitize: families & any non‑toneable policy items must not carry tone modifiers
|
||||||
|
if (FAMILY || !isToneableByPolicy(e) || isMultiHuman(baseGlyph)) {
|
||||||
|
baseGlyph = stripSkinTone(baseGlyph);
|
||||||
|
}
|
||||||
|
|
||||||
const prefIdxPromise = getPreferredToneIndex();
|
const prefIdxPromise = getPreferredToneIndex();
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
let renderGlyph = baseGlyph;
|
let renderGlyph = baseGlyph;
|
||||||
if (licenseValid && toneLock && e.supports_skin_tone) {
|
if (licenseValid && toneLock && isToneableByPolicy(e) && !FAMILY) {
|
||||||
const idx = await prefIdxPromise;
|
const idx = await prefIdxPromise;
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
const base = e.emoji_base || stripSkinTone(baseGlyph);
|
const base = stripSkinTone(e.emoji_base || baseGlyph);
|
||||||
renderGlyph = withSkinTone(base, SKIN_TONES[idx].ch);
|
if (isToneableByPolicy(e) && canApplyToneTo(base)) {
|
||||||
|
renderGlyph = withSkinTone(base, SKIN_TONES[idx].ch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const res = ensureRenderableAndGlyph(renderGlyph, emo);
|
const res = ensureRenderableAndGlyph(renderGlyph, emo);
|
||||||
baseGlyph = res.glyph;
|
baseGlyph = res.glyph;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const name = document.createElement('div');
|
// --- Name (truncated by default; marquee on hover/active) ---
|
||||||
name.className = 'nm';
|
function buildNameStatic(text){
|
||||||
name.textContent = e.name || '';
|
const el = document.createElement('div');
|
||||||
|
el.className = 'nm';
|
||||||
|
el.textContent = text;
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
function buildNameMarquee(text){
|
||||||
|
const mq = document.createElement('marquee');
|
||||||
|
mq.className = 'nm';
|
||||||
|
mq.setAttribute('behavior','scroll');
|
||||||
|
mq.setAttribute('direction','left');
|
||||||
|
mq.setAttribute('scrollamount','3');
|
||||||
|
mq.textContent = text;
|
||||||
|
return mq;
|
||||||
|
}
|
||||||
|
// --- Name: truncated (tooltip on hover); marquee only when .card.active ---
|
||||||
|
function buildNameStatic(text){
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'nm';
|
||||||
|
el.textContent = text;
|
||||||
|
// tooltip for hover
|
||||||
|
el.title = text;
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
function buildNameMarquee(text){
|
||||||
|
const mq = document.createElement('marquee');
|
||||||
|
mq.className = 'nm';
|
||||||
|
mq.setAttribute('behavior','scroll');
|
||||||
|
mq.setAttribute('direction','left');
|
||||||
|
mq.setAttribute('scrollamount','3');
|
||||||
|
mq.setAttribute('truespeed','');
|
||||||
|
// keep tooltip text
|
||||||
|
mq.title = text;
|
||||||
|
mq.textContent = text;
|
||||||
|
return mq;
|
||||||
|
}
|
||||||
|
let nameEl = buildNameStatic(e.name || '');
|
||||||
|
|
||||||
|
// Toggle helpers invoked by tone-row open/close
|
||||||
|
card.__setNameStatic = () => {
|
||||||
|
if (!nameEl || nameEl.tagName !== 'MARQUEE') return;
|
||||||
|
const s = buildNameStatic(e.name || '');
|
||||||
|
nameEl.replaceWith(s);
|
||||||
|
nameEl = s;
|
||||||
|
};
|
||||||
|
card.__setNameMarquee = () => {
|
||||||
|
// If already marquee, do nothing
|
||||||
|
if (nameEl && nameEl.tagName === 'MARQUEE') return;
|
||||||
|
// Ensure we only marquee when the static text overflows its container
|
||||||
|
const needsMarquee = (() => {
|
||||||
|
try {
|
||||||
|
// Force a layout read; the element must be in the DOM
|
||||||
|
const el = nameEl;
|
||||||
|
if (!el) return false;
|
||||||
|
// Small epsilon to avoid float rounding issues
|
||||||
|
return (el.scrollWidth - el.clientWidth) > 1;
|
||||||
|
} catch(_) { return false; }
|
||||||
|
})();
|
||||||
|
if (!needsMarquee) return; // keep static if it fits
|
||||||
|
|
||||||
|
const m = buildNameMarquee(e.name || '');
|
||||||
|
nameEl.replaceWith(m);
|
||||||
|
nameEl = m;
|
||||||
|
// best-effort nudge so it begins smoothly
|
||||||
|
requestAnimationFrame(() => { try { m.stop && m.stop(); m.start && m.start(); } catch(_){} });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optional UI cue: dots for toneable entries
|
||||||
|
if (isToneableByPolicy(e)) {
|
||||||
|
const dots = document.createElement('div');
|
||||||
|
dots.className = 'tone-ind';
|
||||||
|
dots.style.cssText = 'position:absolute;top:6px;right:6px;display:flex;gap:3px;';
|
||||||
|
const sw = (c)=>{ const d=document.createElement('span'); d.style.cssText=`width:6px;height:6px;border-radius:50%;background:${c};opacity:.9;`; return d; };
|
||||||
|
dots.append(sw('#f7d7c4')); dots.append(sw('#c68642')); dots.append(sw('#8d5524'));
|
||||||
|
card.appendChild(dots);
|
||||||
|
}
|
||||||
|
|
||||||
card.addEventListener('click', async () => {
|
card.addEventListener('click', async () => {
|
||||||
if (!licenseValid || !e.supports_skin_tone) {
|
// For non‑Pro, or non‑toneable, or families → perform action immediately
|
||||||
|
if (!licenseValid || !isToneableByPolicy(e) || isFamilyEntry(e)) {
|
||||||
performEmojiAction(baseGlyph).catch(console.error);
|
performEmojiAction(baseGlyph).catch(console.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Open the row‑spanning tone chooser right below this card's row
|
||||||
const idx = await prefIdxPromise;
|
openToneRowFor(card, e, baseGlyph);
|
||||||
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);
|
card.append(emo, nameEl);
|
||||||
list.appendChild(card);
|
list.appendChild(card);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1382,10 +1806,20 @@ if (window.twemoji) {
|
|||||||
|
|
||||||
(async function verifyLicenseOnBoot(){
|
(async function verifyLicenseOnBoot(){
|
||||||
try {
|
try {
|
||||||
const { licenseKey, lastLicenseCheck } = await chrome.storage.local.get(['licenseKey','lastLicenseCheck']);
|
const { licenseKey, lastLicenseCheck, licenseValid: storedValid } = await chrome.storage.local.get(['licenseKey','lastLicenseCheck','licenseValid']);
|
||||||
if (!licenseKey) return;
|
if (!licenseKey) return;
|
||||||
|
const day = 24*60*60*1000;
|
||||||
|
// If licenseValid is true and last check is within a day, skip checking and set UI immediately
|
||||||
|
if (storedValid && (Date.now() - (lastLicenseCheck||0) < day)) {
|
||||||
|
licenseValid = true;
|
||||||
|
refreshPageLimit();
|
||||||
|
applyLicenseUI();
|
||||||
|
setStatusTag();
|
||||||
|
licenseStatusEl && (licenseStatusEl.textContent = 'Pro active');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Otherwise, show "Checking license…" while verifying
|
||||||
licenseStatusEl && (licenseStatusEl.textContent = 'Checking license…');
|
licenseStatusEl && (licenseStatusEl.textContent = 'Checking license…');
|
||||||
const day = 24*60*60*1000; if (Date.now() - (lastLicenseCheck||0) < day) return;
|
|
||||||
const acct = await accountId();
|
const acct = await accountId();
|
||||||
const res = await fetch('https://api.dewemoji.com/v1/license/verify', {
|
const res = await fetch('https://api.dewemoji.com/v1/license/verify', {
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -1394,8 +1828,17 @@ if (window.twemoji) {
|
|||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
const ok = !!data.ok;
|
const ok = !!data.ok;
|
||||||
await chrome.storage.local.set({ licenseValid: ok, lastLicenseCheck: Date.now() });
|
await chrome.storage.local.set({ licenseValid: ok, lastLicenseCheck: Date.now() });
|
||||||
licenseValid = ok; refreshPageLimit(); applyLicenseUI(); setStatusTag();
|
licenseValid = ok;
|
||||||
licenseStatusEl && (licenseStatusEl.textContent = licenseValid ? 'Pro active' : 'Free mode — Pro features locked.');
|
refreshPageLimit();
|
||||||
|
applyLicenseUI();
|
||||||
|
setStatusTag();
|
||||||
|
if (licenseStatusEl) {
|
||||||
|
if (licenseValid) {
|
||||||
|
licenseStatusEl.textContent = 'Pro active';
|
||||||
|
} else {
|
||||||
|
licenseStatusEl.innerHTML = freeStatusHTML();
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
29
styles.css
29
styles.css
@@ -1,6 +1,6 @@
|
|||||||
:root {
|
:root {
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
--bg:#fff; --fg:#111; --mut:#6b7280; --dim:#f3f4f6; --br:#e5e7eb;
|
--bg:#fff; --fg:#111; --mut:#6b7280; --dim:#f3f4f6; --br:#e5e7eb; --active: #60a5fa2b;
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark){
|
@media (prefers-color-scheme: dark){
|
||||||
:root { --bg:#0b1220; --fg:#e5e7eb; --mut:#9ca3af; --dim:#111827; --br:#374151; }
|
:root { --bg:#0b1220; --fg:#e5e7eb; --mut:#9ca3af; --dim:#111827; --br:#374151; }
|
||||||
@@ -267,4 +267,31 @@ html, body { margin:0; padding:0; background:var(--bg); color:var(--fg);
|
|||||||
select#sub {
|
select#sub {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 6;
|
z-index: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.card.active {
|
||||||
|
background-color: var(--active)!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tone-row {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
background-color: var(--active);
|
||||||
|
border: 1px solid var(--c-border, rgba(0, 0, 0, .12));
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, minmax(0px, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tone-option {
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--c-border, rgba(0, 0, 0, .12));
|
||||||
|
background: var(--c-bg, #f3f4f6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: x-large;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user