From aa6e76dad89296ebe029798ebf7a0f855b13fdad Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Thu, 16 Jul 2026 01:38:57 +0200 Subject: [PATCH] fav tab --- public/s/css/f0ckm.css | 39 ++++++++- public/s/js/comments.js | 171 +++++++++++++++++++++++++++++----------- 2 files changed, 161 insertions(+), 49 deletions(-) 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); }); } }