telegram stickers v1

This commit is contained in:
2026-07-15 22:56:26 +02:00
parent afc776d948
commit a7c811025e
7 changed files with 1065 additions and 168 deletions

View File

@@ -2,54 +2,85 @@
<div class="pagewrapper">
<div id="main" class="admin-container">
<div class="container">
<h2>Custom Emojis</h2>
<h2>Custom Emojis &amp; Sticker Packs</h2>
<!-- Telegram Pack Import -->
<div class="admin-form-container sp-import-card"
style="margin-bottom: 28px; text-align: left; background: var(--dropdown-bg); padding: 20px; border: 1px solid var(--nav-border-color); border-radius: 10px;">
<h4 style="margin: 0 0 6px; font-size: 1rem; display: flex; align-items: center; gap: 8px;">
<i class="fa-brands fa-telegram" style="color: #26a5e4;"></i>
Import Telegram Sticker Pack
</h4>
<p style="font-size: 0.82em; color: rgba(255,255,255,0.5); margin: 0 0 14px;">
Enter a Telegram sticker pack URL (e.g. <code>https://t.me/addstickers/PackName</code>) or just the short name.
All stickers will be downloaded and added as a new sticker pack.
</p>
<div style="display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end;">
<div style="flex: 1; min-width: 200px;">
<label style="display: block; font-size: 0.8em; margin-bottom: 5px; opacity: 0.7;">Pack URL or Name</label>
<input type="text" id="sp-pack-url" placeholder="https://t.me/addstickers/PackName"
style="width: 100%; background: var(--bg); border: 1px solid var(--black); padding: 7px 10px; color: var(--white); border-radius: 5px; box-sizing: border-box;">
</div>
<div>
<label style="display: block; font-size: 0.8em; margin-bottom: 5px; opacity: 0.7;">Display Name (optional)</label>
<input type="text" id="sp-display-name" placeholder="My Pack Name"
style="background: var(--bg); border: 1px solid var(--black); padding: 7px 10px; color: var(--white); border-radius: 5px;">
</div>
<button id="sp-import-btn" class="btn-upload"
style="width: auto; padding: 8px 20px; border: 1px solid var(--nav-border-color); background: #26a5e4; color: #fff; cursor: pointer; border-radius: 5px; font-weight: 700; white-space: nowrap;">
<i class="fa-solid fa-download"></i> Import Pack
</button>
</div>
<div id="sp-import-status" style="margin-top: 12px; font-size: 0.85em; display: none; padding: 10px 14px; border-radius: 7px;"></div>
</div>
<!-- Existing Sticker Packs -->
<div id="sp-packs-section" style="margin-bottom: 28px;">
<h3 style="margin: 0 0 12px; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.6;">Imported Sticker Packs</h3>
<div id="sp-packs-list" style="display: flex; flex-direction: column; gap: 10px;"></div>
</div>
<!-- Standalone Emojis -->
<div class="admin-form-container"
style="margin-bottom: 20px; text-align: left; background: var(--dropdown-bg); padding: 15px; border: 1px solid var(--nav-border-color);">
<h4>Add New Emoji</h4>
style="margin-bottom: 20px; text-align: left; background: var(--dropdown-bg); padding: 15px; border: 1px solid var(--nav-border-color); border-radius: 8px;">
<h4 style="margin: 0 0 12px;">Add Standalone Emoji</h4>
<div style="display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end;">
<div>
<label style="display: block; font-size: 0.8em; margin-bottom: 5px; opacity: 0.7;">Name</label>
<input type="text" id="emoji-name" placeholder="" style="background: var(--bg); border: 1px solid var(--black); padding: 5px; color: var(--white);">
<input type="text" id="emoji-name" placeholder=""
style="background: var(--bg); border: 1px solid var(--black); padding: 5px; color: var(--white);">
</div>
<div>
<label style="display: block; font-size: 0.8em; margin-bottom: 5px; opacity: 0.7;">Image File</label>
<input type="file" id="emoji-file" style="background: var(--bg); border: 1px solid var(--black); padding: 4px; color: var(--white);">
<input type="file" id="emoji-file"
style="background: var(--bg); border: 1px solid var(--black); padding: 4px; color: var(--white);">
</div>
<button id="add-emoji" class="btn-upload" style="width: auto; padding: 7px 20px; border: 1px solid var(--nav-border-color); background: var(--bg); color: var(--white); cursor: pointer;">Add</button>
<button id="add-emoji" class="btn-upload"
style="width: auto; padding: 7px 20px; border: 1px solid var(--nav-border-color); background: var(--bg); color: var(--white); cursor: pointer;">Add</button>
</div>
</div>
<div id="emoji-list" class="emoji-grid">
<!-- Populated by JS -->
</div>
<div id="emoji-list" class="emoji-grid"></div>
</div>
<!-- Edit Emoji Modal -->
<div id="edit-emoji-modal" class="modal-overlay" style="display: none;">
<div class="modal-content" style="max-width: 460px; background: var(--dropdown-bg, #222); border: 1px solid var(--nav-border-color, #444); border-radius: 8px; padding: 20px;">
<h3 style="margin-top: 0; margin-bottom: 15px; border-bottom: 1px solid var(--nav-border-color, rgba(255,255,255,0.1)); padding-bottom: 10px; color: var(--white);">Edit Emoji</h3>
<input type="hidden" id="edit-emoji-id">
<div style="display: flex; flex-direction: column; gap: 12px; text-align: left;">
<div style="text-align: center;">
<img id="edit-emoji-preview" src="" alt="" style="height: 64px; width: 64px; object-fit: contain; border-radius: 4px; background: rgba(0,0,0,0.3);">
</div>
<div>
<label style="display: block; font-size: 0.85em; margin-bottom: 4px; color: rgba(255,255,255,0.7);">Name (lowercase a-z, 0-9, _, - only)</label>
<input type="text" id="edit-emoji-name" style="width: 100%; background: var(--bg, #111); border: 1px solid var(--black, #000); padding: 8px; color: var(--white); border-radius: 4px; box-sizing: border-box;">
</div>
<div>
<label style="display: block; font-size: 0.85em; margin-bottom: 4px; color: rgba(255,255,255,0.7);">Replace Image Upload New File</label>
<label style="display: block; font-size: 0.85em; margin-bottom: 4px; color: rgba(255,255,255,0.7);">Replace Image &mdash; Upload New File</label>
<input type="file" id="edit-emoji-file" accept="image/*" style="width: 100%; background: var(--bg, #111); border: 1px solid var(--black, #000); padding: 8px; color: var(--white); border-radius: 4px; box-sizing: border-box;">
</div>
</div>
<div class="modal-actions" style="margin-top: 20px; display: flex; justify-content: flex-end; gap: 10px;">
<button onclick="window.emojiAdmin.closeEditModal()" class="btn-cancel" style="padding: 8px 16px; background: rgba(255,255,255,0.1); color: var(--white); border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; cursor: pointer;">Cancel</button>
<button onclick="window.emojiAdmin.saveEmoji()" class="btn-save" style="padding: 8px 16px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer;">Save Changes</button>
@@ -60,168 +91,313 @@
<script>
(() => {
var i18n = window.f0ckI18n || {};
const esc = (s) => (s || '').toString().replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
var csrf = '{{ csrf_token }}';
const loadEmojis = async () => {
try {
const res = await fetch('/api/v2/emojis');
const data = await res.json();
if (data.success) {
window.emojiAdmin.emojis = data.emojis;
const grid = document.getElementById('emoji-list');
if (!grid) return;
grid.innerHTML = data.emojis.map(e =>
'<div class="emoji-card">' +
'<button class="emoji-delete" onclick="window.emojiAdmin.deleteEmoji(' + e.id + ')" title="Delete">✕</button>' +
'<img class="emoji-preview" src="' + e.url + '" alt=":' + esc(e.name) + ':">' +
'<span class="emoji-label">:' + esc(e.name) + ':</span>' +
'<button onclick="window.emojiAdmin.openEditModal(' + e.id + ')" style="margin-top: 6px; width: 100%; padding: 4px 0; font-size: 0.75em; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">Edit</button>' +
'</div>'
).join('');
}
} catch (err) { console.error('[EMOJI_ADMIN] Load Error:', err); }
};
function esc(s) {
return (s || '').toString()
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
const addEmoji = async (e) => {
/* Returns an HTML string for a sticker preview — <video> for .webm, <img> otherwise */
function stickerEl(url, name, extraAttrs) {
var a = extraAttrs || '';
if (url && url.slice(-5) === '.webm') {
return '<video class="emoji-preview" src="' + url + '" autoplay loop muted playsinline title=":' + esc(name) + ':"' + a + '></video>';
}
return '<img class="emoji-preview" src="' + url + '" alt=":' + esc(name) + ':"' + a + '>';
}
/* ── DATA LOAD ── */
function loadAll() {
Promise.all([
fetch('/api/v2/emojis'),
fetch('/api/v2/admin/sticker-packs', { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
]).then(function(responses) {
return Promise.all([responses[0].json(), responses[1].json()]);
}).then(function(results) {
var emojisData = results[0];
var packsData = results[1];
window.emojiAdmin.emojis = emojisData.emojis || [];
window.emojiAdmin.packs = packsData.packs || [];
renderStandaloneEmojis(window.emojiAdmin.emojis);
renderPacksList(window.emojiAdmin.packs);
}).catch(function(err) {
console.error('[EMOJI_ADMIN] Load Error:', err);
});
}
/* ── STANDALONE EMOJIS ── */
function renderStandaloneEmojis(emojis) {
var grid = document.getElementById('emoji-list');
if (!grid) return;
var standalone = emojis.filter(function(e) { return !e.pack_id; });
if (standalone.length === 0) {
grid.innerHTML = '<p style="opacity:0.5;font-size:0.85em;">No standalone emojis yet.</p>';
return;
}
grid.innerHTML = standalone.map(function(e) {
return '<div class="emoji-card">' +
'<button class="emoji-delete" onclick="window.emojiAdmin.deleteEmoji(' + e.id + ')" title="Delete">x</button>' +
stickerEl(e.url, e.name) +
'<span class="emoji-label">:' + esc(e.name) + ':</span>' +
'<button onclick="window.emojiAdmin.openEditModal(' + e.id + ')" style="margin-top:6px;width:100%;padding:4px 0;font-size:0.75em;background:#28a745;color:white;border:none;border-radius:3px;cursor:pointer;">Edit</button>' +
'</div>';
}).join('');
}
/* ── STICKER PACKS LIST ── */
function renderPacksList(packs) {
var container = document.getElementById('sp-packs-list');
if (!container) return;
if (packs.length === 0) {
container.innerHTML = '<p style="opacity:0.5;font-size:0.85em;">No sticker packs imported yet.</p>';
return;
}
container.innerHTML = packs.map(function(p) {
var tgLink = p.tg_name
? '<a href="https://t.me/addstickers/' + esc(p.tg_name) + '" target="_blank" rel="noopener" style="font-size:0.72em;color:#26a5e4;text-decoration:none;opacity:0.8;">@' + esc(p.tg_name) + ' &nearr;</a>'
: '';
var count = (p.actual_count !== undefined && p.actual_count !== null) ? p.actual_count : (p.sticker_count || 0);
return '<div class="sp-pack-row" data-pack-id="' + p.id + '" style="background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.1);border-radius:10px;padding:14px 16px;">' +
'<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;">' +
'<div style="flex:1;min-width:0;">' +
'<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">' +
'<span class="sp-pack-name" style="font-size:0.95rem;font-weight:700;">' + esc(p.name) + '</span>' +
tgLink +
'</div>' +
'<div style="font-size:0.76em;opacity:0.5;margin-top:2px;">' + count + ' sticker(s)</div>' +
'</div>' +
'<div style="display:flex;gap:8px;flex-shrink:0;">' +
'<button onclick="window.emojiAdmin.renamePack(' + p.id + ')" style="padding:5px 12px;border-radius:5px;border:1px solid rgba(255,255,255,0.18);background:rgba(255,255,255,0.07);color:#fff;cursor:pointer;font-size:0.8em;">Rename</button>' +
'<button onclick="window.emojiAdmin.togglePackEmojis(' + p.id + ')" style="padding:5px 12px;border-radius:5px;border:1px solid rgba(255,255,255,0.18);background:rgba(255,255,255,0.07);color:#fff;cursor:pointer;font-size:0.8em;">View</button>' +
'<button onclick="window.emojiAdmin.deletePack(' + p.id + ')" style="padding:5px 12px;border-radius:5px;border:1px solid rgba(180,40,40,0.5);background:rgba(180,40,40,0.15);color:#e55;cursor:pointer;font-size:0.8em;">Delete</button>' +
'</div>' +
'</div>' +
'<div id="sp-emojis-' + p.id + '" style="display:none;margin-top:12px;"></div>' +
'</div>';
}).join('');
}
function togglePackEmojis(packId) {
var container = document.getElementById('sp-emojis-' + packId);
if (!container) return;
if (container.style.display !== 'none') { container.style.display = 'none'; return; }
container.style.display = 'block';
if (container.dataset.loaded) return;
var packEmojis = (window.emojiAdmin.emojis || []).filter(function(e) { return e.pack_id === packId; });
if (packEmojis.length === 0) {
container.innerHTML = '<p style="opacity:0.5;font-size:0.82em;padding:8px 0;">No stickers.</p>';
} else {
container.innerHTML = '<div class="emoji-grid" style="max-height:320px;overflow-y:auto;">' +
packEmojis.map(function(e) {
return '<div class="emoji-card" style="position:relative;">' +
'<button class="emoji-delete" onclick="window.emojiAdmin.deleteEmoji(' + e.id + ')" title="Delete">x</button>' +
stickerEl(e.url, e.name, ' loading="lazy"') +
'<span class="emoji-label">:' + esc(e.name) + ':</span>' +
'</div>';
}).join('') +
'</div>';
}
container.dataset.loaded = '1';
}
/* ── STANDALONE EMOJI ACTIONS ── */
function addEmoji(e) {
if (e) e.preventDefault();
const name = document.getElementById('emoji-name').value;
const fileInput = document.getElementById('emoji-file');
var name = document.getElementById('emoji-name').value;
var fileInput = document.getElementById('emoji-file');
if (!name || !fileInput.files[0]) return alert('Fill Name and select a File');
const btn = document.getElementById('add-emoji');
const oldText = btn.textContent;
var btn = document.getElementById('add-emoji');
var oldText = btn.textContent;
btn.disabled = true;
btn.textContent = i18n.uploading || 'Uploading...';
const formData = new FormData();
var formData = new FormData();
formData.append('name', name);
if (fileInput.files[0]) {
formData.append('file', fileInput.files[0]);
}
formData.append('file', fileInput.files[0]);
try {
const headers = { 'X-Requested-With': 'XMLHttpRequest' };
const csrf = '{{ csrf_token }}';
if (csrf) headers['X-CSRF-Token'] = csrf;
var headers = { 'X-Requested-With': 'XMLHttpRequest' };
if (csrf) headers['X-CSRF-Token'] = csrf;
const res = await fetch('/api/v2/admin/emojis', {
method: 'POST',
headers: headers,
body: formData
});
const data = await res.json();
if (data.success) {
document.getElementById('emoji-name').value = '';
document.getElementById('emoji-file').value = '';
loadEmojis();
} else {
alert('Failed: ' + (data.message || data.msg || 'Unknown error'));
}
} catch (e) {
console.error('[EMOJI_ADMIN] Add Error:', e);
alert('Error: ' + e.message);
} finally {
btn.disabled = false;
btn.textContent = oldText;
}
};
fetch('/api/v2/admin/emojis', { method: 'POST', headers: headers, body: formData })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
document.getElementById('emoji-name').value = '';
document.getElementById('emoji-file').value = '';
loadAll();
} else {
alert('Failed: ' + (data.message || data.msg || 'Unknown error'));
}
})
.catch(function(err) { alert('Error: ' + err.message); })
.finally(function() { btn.disabled = false; btn.textContent = oldText; });
}
const deleteEmoji = async (id) => {
function deleteEmoji(id) {
if (!confirm('Delete this emoji?')) return;
try {
const res = await fetch('/api/v2/admin/emojis/' + id + '/delete', {
method: 'POST',
headers: { 'X-CSRF-Token': '{{ csrf_token }}' }
});
const data = await res.json();
if (data.success) {
loadEmojis();
} else {
alert('Delete failed');
}
} catch (e) { console.error(e); }
};
fetch('/api/v2/admin/emojis/' + id + '/delete', { method: 'POST', headers: { 'X-CSRF-Token': csrf } })
.then(function(r) { return r.json(); })
.then(function(data) { if (data.success) loadAll(); else alert('Delete failed'); })
.catch(function(err) { console.error(err); });
}
const openEditModal = (id) => {
const emoji = (window.emojiAdmin.emojis || []).find(e => e.id === id);
function openEditModal(id) {
var emoji = (window.emojiAdmin.emojis || []).find(function(e) { return e.id === id; });
if (!emoji) return;
document.getElementById('edit-emoji-id').value = emoji.id;
document.getElementById('edit-emoji-name').value = emoji.name;
document.getElementById('edit-emoji-file').value = '';
const preview = document.getElementById('edit-emoji-preview');
var preview = document.getElementById('edit-emoji-preview');
preview.src = emoji.url;
preview.alt = ':' + emoji.name + ':';
const modal = document.getElementById('edit-emoji-modal');
var modal = document.getElementById('edit-emoji-modal');
if (modal) modal.style.display = 'flex';
};
}
const closeEditModal = () => {
const modal = document.getElementById('edit-emoji-modal');
function closeEditModal() {
var modal = document.getElementById('edit-emoji-modal');
if (modal) modal.style.display = 'none';
document.getElementById('edit-emoji-file').value = '';
};
}
const saveEmoji = async () => {
const id = document.getElementById('edit-emoji-id').value;
const name = document.getElementById('edit-emoji-name').value.trim().toLowerCase();
const fileInput = document.getElementById('edit-emoji-file');
function saveEmoji() {
var id = document.getElementById('edit-emoji-id').value;
var name = document.getElementById('edit-emoji-name').value.trim().toLowerCase();
var fileInput = document.getElementById('edit-emoji-file');
if (!name) return alert('Emoji name is required');
if (!/^[a-z0-9_-]+$/.test(name)) return alert('Invalid name. Use lowercase a-z, 0-9, _, - only.');
const btn = document.querySelector('#edit-emoji-modal .btn-save');
const oldText = btn.textContent;
var btn = document.querySelector('#edit-emoji-modal .btn-save');
var oldText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Saving...';
const formData = new FormData();
var formData = new FormData();
formData.append('name', name);
if (fileInput.files[0]) {
formData.append('file', fileInput.files[0]);
}
if (fileInput.files[0]) formData.append('file', fileInput.files[0]);
try {
const headers = { 'X-Requested-With': 'XMLHttpRequest' };
const csrf = '{{ csrf_token }}';
if (csrf) headers['X-CSRF-Token'] = csrf;
var headers = { 'X-Requested-With': 'XMLHttpRequest' };
if (csrf) headers['X-CSRF-Token'] = csrf;
const res = await fetch('/api/v2/admin/emojis/' + id + '/edit', {
method: 'POST',
headers: headers,
body: formData
fetch('/api/v2/admin/emojis/' + id + '/edit', { method: 'POST', headers: headers, body: formData })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) { closeEditModal(); loadAll(); }
else alert('Save failed: ' + (data.message || data.msg || 'Unknown error'));
})
.catch(function(err) { alert('Save failed: ' + err.message); })
.finally(function() { btn.disabled = false; btn.textContent = oldText; });
}
/* ── STICKER PACK ACTIONS ── */
function importPack() {
var packUrl = (document.getElementById('sp-pack-url').value || '').trim();
var displayName = (document.getElementById('sp-display-name').value || '').trim();
var statusEl = document.getElementById('sp-import-status');
var btn = document.getElementById('sp-import-btn');
if (!packUrl) return alert('Please enter a sticker pack URL or name.');
btn.disabled = true;
btn.textContent = 'Importing...';
statusEl.style.display = 'none';
fetch('/api/v2/admin/sticker-packs/import', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrf,
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ pack_name: packUrl, display_name: displayName })
})
.then(function(r) { return r.json(); })
.then(function(data) {
statusEl.style.display = 'block';
if (data.success) {
statusEl.style.background = 'rgba(40,167,69,0.15)';
statusEl.style.border = '1px solid rgba(40,167,69,0.4)';
statusEl.style.color = '#4caf50';
var packName = (data.pack && data.pack.name) ? data.pack.name : packUrl;
statusEl.innerHTML = 'Imported "' + esc(packName) + '" &mdash; ' + (data.imported || 0) + ' sticker(s) added' + (data.failed ? ', ' + data.failed + ' failed' : '') + '.';
document.getElementById('sp-pack-url').value = '';
document.getElementById('sp-display-name').value = '';
loadAll();
} else {
statusEl.style.background = 'rgba(180,40,40,0.15)';
statusEl.style.border = '1px solid rgba(180,40,40,0.4)';
statusEl.style.color = '#e55';
statusEl.textContent = data.message || 'Import failed';
}
})
.catch(function(err) {
statusEl.style.display = 'block';
statusEl.style.background = 'rgba(180,40,40,0.15)';
statusEl.style.border = '1px solid rgba(180,40,40,0.4)';
statusEl.style.color = '#e55';
statusEl.textContent = 'Error: ' + err.message;
})
.finally(function() {
btn.disabled = false;
btn.innerHTML = '<i class="fa-solid fa-download"></i> Import Pack';
});
}
const data = await res.json();
if (data.success) {
closeEditModal();
loadEmojis();
} else {
alert('Save failed: ' + (data.message || data.msg || 'Unknown error'));
}
} catch (e) {
console.error('[EMOJI_ADMIN] Edit Error:', e);
alert('Save failed: ' + e.message);
} finally {
btn.disabled = false;
btn.textContent = oldText;
}
};
function deletePack(id) {
if (!confirm('Delete this sticker pack AND all its stickers? This cannot be undone.')) return;
fetch('/api/v2/admin/sticker-packs/' + id, {
method: 'DELETE',
headers: { 'X-CSRF-Token': csrf, 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function(r) { return r.json(); })
.then(function(data) { if (data.success) loadAll(); else alert('Delete failed: ' + (data.message || 'Unknown error')); })
.catch(function(err) { alert('Error: ' + err.message); });
}
function renamePack(id) {
var pack = (window.emojiAdmin.packs || []).find(function(p) { return p.id === id; });
var currentName = pack ? pack.name : '';
var newName = prompt('New pack name:', currentName);
if (!newName || newName.trim() === currentName) return;
fetch('/api/v2/admin/sticker-packs/' + id + '/rename', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf, 'X-Requested-With': 'XMLHttpRequest' },
body: JSON.stringify({ name: newName.trim() })
})
.then(function(r) { return r.json(); })
.then(function(data) { if (data.success) loadAll(); else alert('Rename failed: ' + (data.message || 'Unknown error')); })
.catch(function(err) { alert('Error: ' + err.message); });
}
// Global scope for onclick handlers
window.emojiAdmin = { deleteEmoji, openEditModal, closeEditModal, saveEmoji, emojis: [] };
window.emojiAdmin = {
deleteEmoji: deleteEmoji,
openEditModal: openEditModal,
closeEditModal: closeEditModal,
saveEmoji: saveEmoji,
importPack: importPack,
deletePack: deletePack,
renamePack: renamePack,
togglePackEmojis: togglePackEmojis,
emojis: [],
packs: []
};
const btnAddEmoji = document.getElementById('add-emoji');
if (btnAddEmoji) btnAddEmoji.addEventListener('click', addEmoji);
document.getElementById('add-emoji').addEventListener('click', addEmoji);
document.getElementById('sp-import-btn').addEventListener('click', importPack);
// Live Update Listener (SSE dispatched via f0ckm.js)
document.addEventListener('f0ck:emojis_updated', loadEmojis);
loadEmojis();
// Reload when SSE emits emojis_updated
document.addEventListener('f0ck:emojis_updated', loadAll);
loadAll();
})();
</script>
</div>