dms fav picker

This commit is contained in:
2026-07-16 04:09:51 +02:00
parent 6a3b9675b8
commit d53794f9bf

View File

@@ -1789,39 +1789,154 @@ if (window.__dmLoaded) {
// Hold-to-preview (uses shared window.stickerPreview from comments.js)
let holdTimer = null;
let holdFired = false;
const startHold = () => {
let isTouchHold = false;
const startHold = (fromTouch) => {
holdFired = false;
isTouchHold = !!fromTouch;
holdTimer = setTimeout(() => {
holdFired = true;
window.stickerPreview?.show(url, isVideo);
}, 400);
window.stickerPreview?.show(url, isVideo, name, url);
}, 800);
};
const cancelHold = () => { clearTimeout(holdTimer); };
const cancelHold = () => { clearTimeout(holdTimer); isTouchHold = false; };
const endHold = () => {
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();
}, { once: true, capture: true });
if (!isTouchHold) window.stickerPreview?.hide();
// Mobile: preview stays open until user taps overlay
} else {
window.stickerPreview?.hide();
}
window.stickerPreview?.hide();
isTouchHold = false;
};
el.addEventListener('mousedown', startHold);
el.addEventListener('mousedown', () => startHold(false));
el.addEventListener('mouseup', endHold);
el.addEventListener('mouseleave', cancelHold);
// Instant switch when already previewing
el.addEventListener('mouseenter', () => {
if (window.stickerPreview?.isShowing) window.stickerPreview.show(url, isVideo);
if (window.stickerPreview?.isShowing) window.stickerPreview.show(url, isVideo, name, url);
});
el.addEventListener('touchstart', startHold, { passive: true });
el.addEventListener('touchstart', () => startHold(true), { passive: true });
el.addEventListener('touchmove', cancelHold, { passive: true });
el.addEventListener('touchend', endHold);
el.addEventListener('touchcancel', endHold);
el.addEventListener('touchcancel', cancelHold);
// Desktop-only right-click → fav context menu
el.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
if (navigator.maxTouchPoints > 0) return;
if (holdFired) return;
window._favContextMenu?.show(e.clientX, e.clientY, name, url, () => {
if (activeTabId === '__favs__') showDmTab('__favs__');
});
});
return el;
}
// Ensure fav utils are available even when no comment section exists on this page
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);
const action = idx >= 0 ? 'remove' : 'add';
if (idx >= 0) favs.splice(idx, 1); else favs.push({ name, url });
localStorage.setItem(this.KEY, JSON.stringify(favs));
const csrf = window.f0ckSession?.csrf_token;
if (csrf) {
fetch('/api/v2/user/emoji-favorites/toggle', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf, 'X-Requested-With': 'XMLHttpRequest' },
body: JSON.stringify({ name, url, action })
}).catch(() => {});
}
},
syncFromDB() {
fetch('/api/v2/user/emoji-favorites', { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(r => r.ok ? r.json() : null)
.then(data => {
if (data?.success && Array.isArray(data.favorites)) {
localStorage.setItem(this.KEY, JSON.stringify(data.favorites));
window._emojiPickerRefresh?.();
}
}).catch(() => {});
}
};
if (window.f0ckSession?.logged_in) window._emojiPickerFavUtils.syncFromDB();
}
let activeTabId = null;
let dmTabBar = null;
let dmGridArea = null;
function showDmTab(packId, packs) {
activeTabId = packId;
if (dmTabBar) dmTabBar.querySelectorAll('.ep-tab').forEach(t =>
t.classList.toggle('active', t.dataset.packId === String(packId ?? 'null'))
);
if (!dmGridArea) return;
dmGridArea.innerHTML = '';
if (packId === '__favs__') {
const favs = window._emojiPickerFavUtils?.get() || [];
if (!favs.length) {
dmGridArea.innerHTML = '<div style="padding:10px;color:rgba(255,255,255,0.4);font-size:0.8em;text-align:center;">No favorites yet.<br>Hold a sticker to preview, then tap ★</div>';
return;
}
favs.forEach(({ name, url }) => {
const el = makeDmEmojiEl(url, name);
el.addEventListener('click', (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);
});
dmGridArea.appendChild(el);
});
return;
}
const pack = packs.find(p => String(p.id ?? null) === String(packId ?? null)) || packs[0];
if (!pack) return;
pack.emojis.forEach(({ name, url }) => {
const el = makeDmEmojiEl(url, name);
el.addEventListener('click', (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);
});
dmGridArea.appendChild(el);
});
// Autoplay webm stickers
dmGridArea.querySelectorAll('video').forEach(v =>
v.play().catch(() => v.addEventListener('canplay', () => v.play().catch(() => {}), { once: true }))
);
// Touch slide: switch preview as finger moves
dmGridArea.addEventListener('touchmove', (e) => {
if (!window.stickerPreview?.isShowing) return;
const touch = e.touches[0];
const target = document.elementFromPoint(touch.clientX, touch.clientY);
if (target && target !== dmGridArea && (target.tagName === 'IMG' || target.tagName === 'VIDEO') && target._stickerSrc) {
window.stickerPreview.show(target._stickerSrc, target._stickerIsVideo);
}
}, { passive: true });
}
function buildDmPickerContent(emojis, packs) {
picker.innerHTML = '';
picker.classList.remove('has-tabs');
@@ -1848,58 +1963,30 @@ if (window.__dmLoaded) {
// ── Tabbed picker ──
picker.classList.add('has-tabs');
const tabBar = document.createElement('div');
tabBar.className = 'emoji-picker-tabs';
dmTabBar = document.createElement('div');
dmTabBar.className = 'emoji-picker-tabs';
const gridArea = document.createElement('div');
gridArea.className = 'emoji-picker-grid';
dmGridArea = document.createElement('div');
dmGridArea.className = 'emoji-picker-grid';
let activeTabId = null;
const showTab = (packId) => {
activeTabId = packId;
tabBar.querySelectorAll('.ep-tab').forEach(t =>
t.classList.toggle('active', t.dataset.packId === String(packId ?? 'null'))
);
gridArea.innerHTML = '';
const pack = packs.find(p => String(p.id ?? null) === String(packId ?? null)) || packs[0];
if (!pack) return;
pack.emojis.forEach(({ name, url }) => {
const el = makeDmEmojiEl(url, name);
el.addEventListener('click', (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);
});
// Autoplay any webm stickers in the grid
gridArea.querySelectorAll('video').forEach(v =>
v.play().catch(() => v.addEventListener('canplay', () => v.play().catch(() => {}), { once: true }))
);
// Touch slide: switch preview immediately as finger moves over a new sticker
gridArea.onTouchMoveBound = null; // reset before re-adding
gridArea.addEventListener('touchmove', (e) => {
if (!window.stickerPreview?.isShowing) return;
const touch = e.touches[0];
const target = document.elementFromPoint(touch.clientX, touch.clientY);
if (target && target !== gridArea && (target.tagName === 'IMG' || target.tagName === 'VIDEO') && target._stickerSrc) {
window.stickerPreview.show(target._stickerSrc, target._stickerIsVideo);
}
}, { passive: true });
};
// ── Favorites tab ──
const favsTab = document.createElement('button');
favsTab.className = 'ep-tab ep-tab-favs';
favsTab.dataset.packId = '__favs__';
favsTab.title = 'Favorites';
favsTab.type = 'button';
favsTab.innerHTML = '<i class="fa-solid fa-star" style="font-size:14px;color:var(--emoji-fav-color,#f5c518);"></i>';
favsTab.addEventListener('mousedown', e => e.preventDefault());
favsTab.addEventListener('click', () => showDmTab('__favs__', packs));
dmTabBar.appendChild(favsTab);
// ── Pack tabs ──
packs.forEach((pack) => {
const tab = document.createElement('button');
tab.className = 'ep-tab';
tab.dataset.packId = String(pack.id ?? null);
tab.title = pack.name || 'Emojis';
tab.type = 'button';
// Always use static img for tab icon (thumb_url or first non-video emoji)
if (pack.emojis && pack.emojis.length > 0) {
const iconUrl = pack.thumb_url
|| (pack.emojis.find(e => !e.url.endsWith('.webm')) || pack.emojis[0]).url;
@@ -1912,13 +1999,18 @@ if (window.__dmLoaded) {
tab.textContent = '☺';
}
tab.addEventListener('mousedown', e => e.preventDefault());
tab.addEventListener('click', () => showTab(pack.id ?? null));
tabBar.appendChild(tab);
tab.addEventListener('click', () => showDmTab(pack.id ?? null, packs));
dmTabBar.appendChild(tab);
});
picker.appendChild(tabBar);
picker.appendChild(gridArea);
showTab(packs[0]?.id ?? null);
picker.appendChild(dmTabBar);
picker.appendChild(dmGridArea);
showDmTab(packs[0]?.id ?? null, packs);
// Expose refresh hook so syncFromDB can update the open favs tab
window._emojiPickerRefresh = () => {
if (activeTabId === '__favs__') showDmTab('__favs__', packs);
};
}
function populatePicker() {