diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css
index 5e8c461..63d7e61 100644
--- a/public/s/css/f0ckm.css
+++ b/public/s/css/f0ckm.css
@@ -12,6 +12,7 @@
html[theme='f0ck'] {
--accent: #9f0;
--accent-rgb: 153, 255, 0;
+ --emoji-fav-color: #f5c518;
--bg: #111;
--black: #000;
--white: #fff;
@@ -5727,6 +5728,11 @@ body[type='login'] {
border-bottom: 2px solid var(--accent, #fff);
}
+.ep-tab-favs.active {
+ border-bottom-color: var(--emoji-fav-color, #f5c518);
+ background: rgba(245,197,24,0.1);
+}
+
.ep-tab img,
.ep-tab video {
width: 28px;
@@ -5759,6 +5765,38 @@ body[type='login'] {
box-sizing: border-box;
}
+/* Sticker wrapper — holds img/video + optional fav badge */
+.ep-sticker-wrap {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ border-radius: 4px;
+ box-sizing: border-box;
+}
+
+.ep-sticker-wrap:hover {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+/* Fav star badge — top-right corner of sticker */
+.ep-fav-badge {
+ position: absolute;
+ top: 2px;
+ right: 2px;
+ font-size: 9px;
+ color: var(--emoji-fav-color, #f5c518);
+ pointer-events: none;
+ line-height: 1;
+ filter: drop-shadow(0 0 2px rgba(0,0,0,0.7));
+}
+
+/* Hide badge in the favorites tab itself — everything there is already a fav */
+.emoji-picker-grid.favs-active .ep-fav-badge {
+ display: none;
+}
+
.emoji-picker-grid img:hover,
.emoji-picker-grid video:hover {
background: rgba(255, 255, 255, 0.1);
@@ -8255,7 +8293,6 @@ video.autoplay-gif {
border: none;
color: var(--white);
padding: 10px;
- border-radius: 4px;
font-family: inherit;
resize: vertical;
}
diff --git a/public/s/js/comments.js b/public/s/js/comments.js
index 7802fb9..0a225d7 100644
--- a/public/s/js/comments.js
+++ b/public/s/js/comments.js
@@ -4060,25 +4060,81 @@ class CommentSystem {
}
// Helper: create img or video element for a given emoji URL
+ // ── Favorites (singleton utils + context menu) ──────────────
+ if (!window._emojiPickerFavUtils) {
+ window._emojiPickerFavUtils = {
+ KEY: 'f0ck_emoji_favs',
+ get() { try { return JSON.parse(localStorage.getItem(this.KEY) || '[]'); } catch(e) { return []; } },
+ has(name) { return this.get().some(f => f.name === name); },
+ toggle(name, url) {
+ const favs = this.get();
+ const idx = favs.findIndex(f => f.name === name);
+ if (idx >= 0) favs.splice(idx, 1); else favs.push({ name, url });
+ localStorage.setItem(this.KEY, JSON.stringify(favs));
+ }
+ };
+ }
+ if (!window._favContextMenu) {
+ const menu = document.createElement('div');
+ menu.style.cssText = 'position:fixed;z-index:99999;background:var(--dropdown-bg,#222);border:1px solid var(--nav-border-color,#444);border-radius:7px;padding:4px 0;box-shadow:0 4px 20px rgba(0,0,0,0.5);display:none;min-width:180px;';
+ document.body.appendChild(menu);
+ window._favContextMenu = {
+ el: menu,
+ show(x, y, name, url, onToggle) {
+ const isFav = window._emojiPickerFavUtils.has(name);
+ menu.innerHTML = '';
+ const item = document.createElement('div');
+ item.style.cssText = 'padding:9px 16px;cursor:pointer;font-size:0.875em;color:var(--white,#fff);display:flex;align-items:center;gap:8px;border-radius:4px;margin:2px 4px;';
+ item.innerHTML = '' + (isFav ? '★' : '☆') + '' + (isFav ? 'Remove from Favorites' : 'Add to Favorites');
+ item.addEventListener('mouseenter', () => { item.style.background = 'rgba(255,255,255,0.09)'; });
+ item.addEventListener('mouseleave', () => { item.style.background = ''; });
+ item.addEventListener('click', (e) => { e.stopPropagation(); window._emojiPickerFavUtils.toggle(name, url); menu.style.display = 'none'; if (onToggle) onToggle(); });
+ menu.appendChild(item);
+ menu.style.left = x + 'px'; menu.style.top = y + 'px'; menu.style.display = 'block';
+ requestAnimationFrame(() => {
+ const r = menu.getBoundingClientRect();
+ if (r.right > window.innerWidth) menu.style.left = (x - r.width) + 'px';
+ if (r.bottom > window.innerHeight) menu.style.top = (y - r.height) + 'px';
+ });
+ },
+ hide() { menu.style.display = 'none'; }
+ };
+ document.addEventListener('click', () => window._favContextMenu.hide());
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape') window._favContextMenu.hide(); });
+ }
+
const makeEmojiEl = (url, name) => {
const isVideo = url && url.endsWith('.webm');
- let el;
+ // media element (img or video)
+ let media;
if (isVideo) {
- el = document.createElement('video');
- el.src = url;
- el.autoplay = true;
- el.loop = true;
- el.muted = true;
- el.playsInline = true;
+ media = document.createElement('video');
+ media.src = url;
+ media.autoplay = true;
+ media.loop = true;
+ media.muted = true;
+ media.playsInline = true;
} else {
- el = document.createElement('img');
- el.src = url;
- el.loading = 'lazy';
+ media = document.createElement('img');
+ media.src = url;
+ media.loading = 'lazy';
}
- el.title = `:${name}:`;
- el.onerror = () => { el.style.display = 'none'; };
+ media.title = `:${name}:`;
+ media.onerror = () => { media.style.display = 'none'; };
+ media._stickerSrc = url;
+ media._stickerIsVideo = isVideo;
+ // Wrapper div — needed to position the fav badge overlay
+ const el = document.createElement('div');
+ el.className = 'ep-sticker-wrap';
el._stickerSrc = url;
el._stickerIsVideo = isVideo;
+ el.appendChild(media);
+ // Fav star badge
+ if (window._emojiPickerFavUtils?.has(name)) {
+ const badge = document.createElement('i');
+ badge.className = 'fa-solid fa-star ep-fav-badge';
+ el.appendChild(badge);
+ }
// Hold-to-preview
let holdTimer = null;
let holdFired = false;
@@ -4094,7 +4150,6 @@ class CommentSystem {
clearTimeout(holdTimer);
if (holdFired) {
holdFired = false;
- // Suppress the click that fires on mouseup after a preview gesture
el.addEventListener('click', (e) => {
e.stopImmediatePropagation();
e.preventDefault();
@@ -4102,16 +4157,23 @@ class CommentSystem {
}
window.stickerPreview?.hide();
};
- el.addEventListener('mousedown', startHold);
- el.addEventListener('mouseup', endHold);
- el.addEventListener('mouseleave', cancelHold);
- // Instant switch: if already previewing and mouse enters a new emoji, show it immediately
- el.addEventListener('mouseenter', () => {
+ media.addEventListener('mousedown', startHold);
+ media.addEventListener('mouseup', endHold);
+ media.addEventListener('mouseleave', cancelHold);
+ media.addEventListener('mouseenter', () => {
if (window.stickerPreview?.isShowing) window.stickerPreview.show(url, isVideo);
});
- el.addEventListener('touchstart', startHold, { passive: true });
- el.addEventListener('touchend', endHold);
- el.addEventListener('touchcancel', endHold);
+ media.addEventListener('touchstart', startHold, { passive: true });
+ media.addEventListener('touchend', endHold);
+ media.addEventListener('touchcancel', endHold);
+ // Right-click → favorites context menu
+ media.addEventListener('contextmenu', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ window._favContextMenu?.show(e.clientX, e.clientY, name, url, () => {
+ showTab(activeTabId); // refresh to update star badges
+ });
+ });
return el;
};
@@ -4153,6 +4215,34 @@ class CommentSystem {
tabBar.querySelectorAll('.ep-tab').forEach(t => t.classList.toggle('active', t.dataset.packId === String(packId ?? 'null')));
gridArea.innerHTML = '';
+ gridArea.classList.toggle('favs-active', packId === '__favs__');
+
+ // ── Favorites tab ──
+ if (packId === '__favs__') {
+ const favs = window._emojiPickerFavUtils?.get() || [];
+ if (!favs.length) {
+ const empty = document.createElement('div');
+ empty.style.cssText = 'grid-column:1/-1;text-align:center;padding:28px 12px;opacity:0.45;font-size:0.82em;line-height:1.5;';
+ empty.textContent = 'No favorites yet. Right-click any sticker to add it here.';
+ gridArea.appendChild(empty);
+ } else {
+ favs.forEach(({ name, url }) => {
+ const el = makeEmojiEl(url, name);
+ el.onclick = (ev) => {
+ ev.stopPropagation();
+ const pos = textarea.selectionStart ?? textarea.value.length;
+ const val = textarea.value;
+ textarea.value = val.slice(0, pos) + `:${name}:` + val.slice(pos);
+ textarea.focus();
+ const newPos = pos + name.length + 2;
+ textarea.setSelectionRange(newPos, newPos);
+ };
+ gridArea.appendChild(el);
+ });
+ }
+ return;
+ }
+
const pack = packs.find(p => String(p.id ?? null) === String(packId ?? null)) || packs[0];
if (!pack) return;
@@ -4181,7 +4271,17 @@ class CommentSystem {
}, { passive: true });
};
- // Build tab buttons
+ // Build tab buttons — Favorites first
+ const favTab = document.createElement('button');
+ favTab.className = 'ep-tab ep-tab-favs';
+ favTab.dataset.packId = '__favs__';
+ favTab.title = 'Favorites';
+ favTab.type = 'button';
+ favTab.innerHTML = '';
+ favTab.addEventListener('mousedown', e => e.preventDefault());
+ favTab.addEventListener('click', () => showTab('__favs__'));
+ tabBar.appendChild(favTab);
+
packs.forEach((pack, i) => {
const tab = document.createElement('button');
tab.className = 'ep-tab';
@@ -4226,26 +4326,12 @@ class CommentSystem {
const isVisible = picker.style.display !== 'none';
if (isVisible) {
picker.style.display = 'none';
- if (closeHandler) {
- document.removeEventListener('click', closeHandler);
- closeHandler = null;
- }
} else {
- // Rebuild content only if the cache has been updated since last build
buildPickerContent();
- picker.style.display = ''; // Reset to CSS default (flex)
+ picker.style.display = '';
requestAnimationFrame(() => requestAnimationFrame(() =>
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' })
));
- closeHandler = (ev) => {
- const isSubmit = ev.target.closest?.('.submit-comment');
- if (!picker.contains(ev.target) && ev.target !== trigger && !isSubmit) {
- picker.style.display = 'none';
- document.removeEventListener('click', closeHandler);
- closeHandler = null;
- }
- };
- setTimeout(() => document.addEventListener('click', closeHandler), 0);
}
return;
}
@@ -4311,17 +4397,6 @@ class CommentSystem {
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' })
));
- // Set up close handler
- // Set up close handler
- closeHandler = (ev) => {
- const isSubmit = ev.target.closest?.('.submit-comment');
- if (!picker.contains(ev.target) && ev.target !== trigger && !isSubmit) {
- picker.style.display = 'none';
- document.removeEventListener('click', closeHandler);
- closeHandler = null;
- }
- };
- setTimeout(() => document.addEventListener('click', closeHandler), 0);
});
}
}