dms fav picker
This commit is contained in:
@@ -1789,39 +1789,154 @@ if (window.__dmLoaded) {
|
|||||||
// Hold-to-preview (uses shared window.stickerPreview from comments.js)
|
// Hold-to-preview (uses shared window.stickerPreview from comments.js)
|
||||||
let holdTimer = null;
|
let holdTimer = null;
|
||||||
let holdFired = false;
|
let holdFired = false;
|
||||||
const startHold = () => {
|
let isTouchHold = false;
|
||||||
|
const startHold = (fromTouch) => {
|
||||||
holdFired = false;
|
holdFired = false;
|
||||||
|
isTouchHold = !!fromTouch;
|
||||||
holdTimer = setTimeout(() => {
|
holdTimer = setTimeout(() => {
|
||||||
holdFired = true;
|
holdFired = true;
|
||||||
window.stickerPreview?.show(url, isVideo);
|
window.stickerPreview?.show(url, isVideo, name, url);
|
||||||
}, 400);
|
}, 800);
|
||||||
};
|
};
|
||||||
const cancelHold = () => { clearTimeout(holdTimer); };
|
const cancelHold = () => { clearTimeout(holdTimer); isTouchHold = false; };
|
||||||
const endHold = () => {
|
const endHold = () => {
|
||||||
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();
|
||||||
}, { once: true, capture: true });
|
}, { 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('mouseup', endHold);
|
||||||
el.addEventListener('mouseleave', cancelHold);
|
el.addEventListener('mouseleave', cancelHold);
|
||||||
// Instant switch when already previewing
|
// Instant switch when already previewing
|
||||||
el.addEventListener('mouseenter', () => {
|
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('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;
|
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) {
|
function buildDmPickerContent(emojis, packs) {
|
||||||
picker.innerHTML = '';
|
picker.innerHTML = '';
|
||||||
picker.classList.remove('has-tabs');
|
picker.classList.remove('has-tabs');
|
||||||
@@ -1848,58 +1963,30 @@ if (window.__dmLoaded) {
|
|||||||
// ── Tabbed picker ──
|
// ── Tabbed picker ──
|
||||||
picker.classList.add('has-tabs');
|
picker.classList.add('has-tabs');
|
||||||
|
|
||||||
const tabBar = document.createElement('div');
|
dmTabBar = document.createElement('div');
|
||||||
tabBar.className = 'emoji-picker-tabs';
|
dmTabBar.className = 'emoji-picker-tabs';
|
||||||
|
|
||||||
const gridArea = document.createElement('div');
|
dmGridArea = document.createElement('div');
|
||||||
gridArea.className = 'emoji-picker-grid';
|
dmGridArea.className = 'emoji-picker-grid';
|
||||||
|
|
||||||
let activeTabId = null;
|
// ── Favorites tab ──
|
||||||
|
const favsTab = document.createElement('button');
|
||||||
const showTab = (packId) => {
|
favsTab.className = 'ep-tab ep-tab-favs';
|
||||||
activeTabId = packId;
|
favsTab.dataset.packId = '__favs__';
|
||||||
tabBar.querySelectorAll('.ep-tab').forEach(t =>
|
favsTab.title = 'Favorites';
|
||||||
t.classList.toggle('active', t.dataset.packId === String(packId ?? 'null'))
|
favsTab.type = 'button';
|
||||||
);
|
favsTab.innerHTML = '<i class="fa-solid fa-star" style="font-size:14px;color:var(--emoji-fav-color,#f5c518);"></i>';
|
||||||
gridArea.innerHTML = '';
|
favsTab.addEventListener('mousedown', e => e.preventDefault());
|
||||||
const pack = packs.find(p => String(p.id ?? null) === String(packId ?? null)) || packs[0];
|
favsTab.addEventListener('click', () => showDmTab('__favs__', packs));
|
||||||
if (!pack) return;
|
dmTabBar.appendChild(favsTab);
|
||||||
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 });
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// ── Pack tabs ──
|
||||||
packs.forEach((pack) => {
|
packs.forEach((pack) => {
|
||||||
const tab = document.createElement('button');
|
const tab = document.createElement('button');
|
||||||
tab.className = 'ep-tab';
|
tab.className = 'ep-tab';
|
||||||
tab.dataset.packId = String(pack.id ?? null);
|
tab.dataset.packId = String(pack.id ?? null);
|
||||||
tab.title = pack.name || 'Emojis';
|
tab.title = pack.name || 'Emojis';
|
||||||
tab.type = 'button';
|
tab.type = 'button';
|
||||||
// Always use static img for tab icon (thumb_url or first non-video emoji)
|
|
||||||
if (pack.emojis && pack.emojis.length > 0) {
|
if (pack.emojis && pack.emojis.length > 0) {
|
||||||
const iconUrl = pack.thumb_url
|
const iconUrl = pack.thumb_url
|
||||||
|| (pack.emojis.find(e => !e.url.endsWith('.webm')) || pack.emojis[0]).url;
|
|| (pack.emojis.find(e => !e.url.endsWith('.webm')) || pack.emojis[0]).url;
|
||||||
@@ -1912,13 +1999,18 @@ if (window.__dmLoaded) {
|
|||||||
tab.textContent = '☺';
|
tab.textContent = '☺';
|
||||||
}
|
}
|
||||||
tab.addEventListener('mousedown', e => e.preventDefault());
|
tab.addEventListener('mousedown', e => e.preventDefault());
|
||||||
tab.addEventListener('click', () => showTab(pack.id ?? null));
|
tab.addEventListener('click', () => showDmTab(pack.id ?? null, packs));
|
||||||
tabBar.appendChild(tab);
|
dmTabBar.appendChild(tab);
|
||||||
});
|
});
|
||||||
|
|
||||||
picker.appendChild(tabBar);
|
picker.appendChild(dmTabBar);
|
||||||
picker.appendChild(gridArea);
|
picker.appendChild(dmGridArea);
|
||||||
showTab(packs[0]?.id ?? null);
|
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() {
|
function populatePicker() {
|
||||||
|
|||||||
Reference in New Issue
Block a user