This commit is contained in:
2026-07-16 01:38:57 +02:00
parent 08d65532b1
commit aa6e76dad8
2 changed files with 161 additions and 49 deletions

View File

@@ -12,6 +12,7 @@
html[theme='f0ck'] { html[theme='f0ck'] {
--accent: #9f0; --accent: #9f0;
--accent-rgb: 153, 255, 0; --accent-rgb: 153, 255, 0;
--emoji-fav-color: #f5c518;
--bg: #111; --bg: #111;
--black: #000; --black: #000;
--white: #fff; --white: #fff;
@@ -5727,6 +5728,11 @@ body[type='login'] {
border-bottom: 2px solid var(--accent, #fff); 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 img,
.ep-tab video { .ep-tab video {
width: 28px; width: 28px;
@@ -5759,6 +5765,38 @@ body[type='login'] {
box-sizing: border-box; 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 img:hover,
.emoji-picker-grid video:hover { .emoji-picker-grid video:hover {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
@@ -8255,7 +8293,6 @@ video.autoplay-gif {
border: none; border: none;
color: var(--white); color: var(--white);
padding: 10px; padding: 10px;
border-radius: 4px;
font-family: inherit; font-family: inherit;
resize: vertical; resize: vertical;
} }

View File

@@ -4060,25 +4060,81 @@ class CommentSystem {
} }
// Helper: create img or video element for a given emoji URL // 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 = '<span style="font-size:1.1em;">' + (isFav ? '&#9733;' : '&#9734;') + '</span>' + (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 makeEmojiEl = (url, name) => {
const isVideo = url && url.endsWith('.webm'); const isVideo = url && url.endsWith('.webm');
let el; // media element (img or video)
let media;
if (isVideo) { if (isVideo) {
el = document.createElement('video'); media = document.createElement('video');
el.src = url; media.src = url;
el.autoplay = true; media.autoplay = true;
el.loop = true; media.loop = true;
el.muted = true; media.muted = true;
el.playsInline = true; media.playsInline = true;
} else { } else {
el = document.createElement('img'); media = document.createElement('img');
el.src = url; media.src = url;
el.loading = 'lazy'; media.loading = 'lazy';
} }
el.title = `:${name}:`; media.title = `:${name}:`;
el.onerror = () => { el.style.display = 'none'; }; 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._stickerSrc = url;
el._stickerIsVideo = isVideo; 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 // Hold-to-preview
let holdTimer = null; let holdTimer = null;
let holdFired = false; let holdFired = false;
@@ -4094,7 +4150,6 @@ class CommentSystem {
clearTimeout(holdTimer); clearTimeout(holdTimer);
if (holdFired) { if (holdFired) {
holdFired = false; holdFired = false;
// Suppress the click that fires on mouseup after a preview gesture
el.addEventListener('click', (e) => { el.addEventListener('click', (e) => {
e.stopImmediatePropagation(); e.stopImmediatePropagation();
e.preventDefault(); e.preventDefault();
@@ -4102,16 +4157,23 @@ class CommentSystem {
} }
window.stickerPreview?.hide(); window.stickerPreview?.hide();
}; };
el.addEventListener('mousedown', startHold); media.addEventListener('mousedown', startHold);
el.addEventListener('mouseup', endHold); media.addEventListener('mouseup', endHold);
el.addEventListener('mouseleave', cancelHold); media.addEventListener('mouseleave', cancelHold);
// Instant switch: if already previewing and mouse enters a new emoji, show it immediately media.addEventListener('mouseenter', () => {
el.addEventListener('mouseenter', () => {
if (window.stickerPreview?.isShowing) window.stickerPreview.show(url, isVideo); if (window.stickerPreview?.isShowing) window.stickerPreview.show(url, isVideo);
}); });
el.addEventListener('touchstart', startHold, { passive: true }); media.addEventListener('touchstart', startHold, { passive: true });
el.addEventListener('touchend', endHold); media.addEventListener('touchend', endHold);
el.addEventListener('touchcancel', 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; return el;
}; };
@@ -4153,6 +4215,34 @@ class CommentSystem {
tabBar.querySelectorAll('.ep-tab').forEach(t => t.classList.toggle('active', t.dataset.packId === String(packId ?? 'null'))); tabBar.querySelectorAll('.ep-tab').forEach(t => t.classList.toggle('active', t.dataset.packId === String(packId ?? 'null')));
gridArea.innerHTML = ''; 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]; const pack = packs.find(p => String(p.id ?? null) === String(packId ?? null)) || packs[0];
if (!pack) return; if (!pack) return;
@@ -4181,7 +4271,17 @@ class CommentSystem {
}, { passive: true }); }, { 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 = '<i class="fa-solid fa-star" style="color:var(--emoji-fav-color,#f5c518);font-size:1.1em;"></i>';
favTab.addEventListener('mousedown', e => e.preventDefault());
favTab.addEventListener('click', () => showTab('__favs__'));
tabBar.appendChild(favTab);
packs.forEach((pack, i) => { packs.forEach((pack, i) => {
const tab = document.createElement('button'); const tab = document.createElement('button');
tab.className = 'ep-tab'; tab.className = 'ep-tab';
@@ -4226,26 +4326,12 @@ class CommentSystem {
const isVisible = picker.style.display !== 'none'; const isVisible = picker.style.display !== 'none';
if (isVisible) { if (isVisible) {
picker.style.display = 'none'; picker.style.display = 'none';
if (closeHandler) {
document.removeEventListener('click', closeHandler);
closeHandler = null;
}
} else { } else {
// Rebuild content only if the cache has been updated since last build
buildPickerContent(); buildPickerContent();
picker.style.display = ''; // Reset to CSS default (flex) picker.style.display = '';
requestAnimationFrame(() => requestAnimationFrame(() => requestAnimationFrame(() => requestAnimationFrame(() =>
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }) 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; return;
} }
@@ -4311,17 +4397,6 @@ class CommentSystem {
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }) 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);
}); });
} }
} }