update emoji manager
This commit is contained in:
@@ -13871,8 +13871,8 @@ body.layout-modern .xd-score-wrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.emoji-card .emoji-preview {
|
.emoji-card .emoji-preview {
|
||||||
height: 48px;
|
height: 80px;
|
||||||
max-width: 80px;
|
max-width: 90px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
image-rendering: auto;
|
image-rendering: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import path from "path";
|
|||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { execFile as _execFile } from "child_process";
|
import { execFile as _execFile } from "child_process";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
const execFile = promisify(_execFile);
|
const execFile = promisify(_execFile);
|
||||||
|
|
||||||
@@ -80,11 +81,11 @@ export const handleEmojiUpload = async (req, res) => {
|
|||||||
|
|
||||||
const file = parts.file;
|
const file = parts.file;
|
||||||
if (file && file.data && file.data.length > 0) {
|
if (file && file.data && file.data.length > 0) {
|
||||||
const randSuffix = Math.random().toString(36).substring(7);
|
const randSuffix = crypto.randomBytes(24).toString('hex');
|
||||||
const extMatch = file.filename.match(/\.([a-z0-9]+)$/i);
|
const extMatch = file.filename.match(/\.([a-z0-9]+)$/i);
|
||||||
const originalExt = extMatch ? extMatch[1].toLowerCase() : 'png';
|
const originalExt = extMatch ? extMatch[1].toLowerCase() : 'png';
|
||||||
|
|
||||||
const webpFilename = `${name}_${randSuffix}.webp`;
|
const webpFilename = `${randSuffix}.webp`;
|
||||||
const webpPath = path.join(cfg.paths.emojis, webpFilename);
|
const webpPath = path.join(cfg.paths.emojis, webpFilename);
|
||||||
|
|
||||||
if (originalExt === 'webp') {
|
if (originalExt === 'webp') {
|
||||||
@@ -135,3 +136,133 @@ export const handleEmojiUpload = async (req, res) => {
|
|||||||
return sendJson(res, { success: false, message: err.message }, 500);
|
return sendJson(res, { success: false, message: err.message }, 500);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const handleEmojiEdit = async (req, res) => {
|
||||||
|
// Manual Session Lookup
|
||||||
|
let user = [];
|
||||||
|
if (req.cookies && req.cookies.session) {
|
||||||
|
user = await db`
|
||||||
|
select "user".id, "user".login, "user".user, "user".admin, "user".is_moderator, "user_sessions".id as sess_id, "user_sessions".csrf_token
|
||||||
|
from "user_sessions"
|
||||||
|
left join "user" on "user".id = "user_sessions".user_id
|
||||||
|
where "user_sessions".session = ${lib.sha256(req.cookies.session)}
|
||||||
|
limit 1
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.length === 0 || !user[0].admin) {
|
||||||
|
return sendJson(res, { success: false, message: 'Unauthorized' }, 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.session = user[0];
|
||||||
|
|
||||||
|
// CSRF validation
|
||||||
|
if (req.session.csrf_token) {
|
||||||
|
const csrfToken = req.headers['x-csrf-token'];
|
||||||
|
if (!csrfToken || csrfToken !== req.session.csrf_token) {
|
||||||
|
console.warn(`[CSRF] Blocked emoji edit for user ${req.session.user}. Invalid token.`);
|
||||||
|
return sendJson(res, { success: false, message: 'Invalid CSRF token' }, 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = req.params.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const contentType = req.headers['content-type'] || '';
|
||||||
|
const boundaryMatch = contentType.match(/boundary=([^;]+)/);
|
||||||
|
|
||||||
|
if (!boundaryMatch) {
|
||||||
|
return sendJson(res, { success: false, message: 'Invalid content type' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
let boundary = boundaryMatch[1].trim();
|
||||||
|
if (boundary.startsWith('"') && boundary.endsWith('"')) {
|
||||||
|
boundary = boundary.substring(1, boundary.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyBuffer = await collectBody(req);
|
||||||
|
const parts = parseMultipart(bodyBuffer, boundary);
|
||||||
|
|
||||||
|
const name = (parts.name || '').trim().toLowerCase();
|
||||||
|
let url = (parts.url || '').trim();
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return sendJson(res, { success: false, message: 'Emoji name is required' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^[a-z0-9_-]+$/.test(name)) {
|
||||||
|
return sendJson(res, { success: false, message: 'Invalid name. Use lowercase a-z, 0-9, _, - only.' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the current emoji record
|
||||||
|
const current = await db`SELECT id, name, url FROM custom_emojis WHERE id = ${id} LIMIT 1`;
|
||||||
|
if (current.length === 0) {
|
||||||
|
return sendJson(res, { success: false, message: 'Emoji not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check name collision (allow keeping the same name)
|
||||||
|
if (name !== current[0].name) {
|
||||||
|
const conflict = await db`SELECT id FROM custom_emojis WHERE name = ${name} AND id != ${id} LIMIT 1`;
|
||||||
|
if (conflict.length > 0) {
|
||||||
|
return sendJson(res, { success: false, message: 'Emoji name already exists' }, 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = parts.file;
|
||||||
|
if (file && file.data && file.data.length > 0) {
|
||||||
|
const randSuffix = crypto.randomBytes(24).toString('hex');
|
||||||
|
const extMatch = file.filename.match(/\.([a-z0-9]+)$/i);
|
||||||
|
const originalExt = extMatch ? extMatch[1].toLowerCase() : 'png';
|
||||||
|
|
||||||
|
const webpFilename = `${randSuffix}.webp`;
|
||||||
|
const webpPath = path.join(cfg.paths.emojis, webpFilename);
|
||||||
|
|
||||||
|
if (originalExt === 'webp') {
|
||||||
|
await fs.writeFile(webpPath, file.data);
|
||||||
|
} else {
|
||||||
|
const tmpFilename = `${name}_${randSuffix}_tmp.${originalExt}`;
|
||||||
|
const tmpPath = path.join(cfg.paths.emojis, tmpFilename);
|
||||||
|
await fs.writeFile(tmpPath, file.data);
|
||||||
|
try {
|
||||||
|
await execFile('magick', [tmpPath, '-coalesce', '-quality', '80', webpPath], { env: magickEnv });
|
||||||
|
} finally {
|
||||||
|
await fs.unlink(tmpPath).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stat = await fs.stat(webpPath);
|
||||||
|
if (!stat || stat.size === 0) throw new Error('File write/conversion verification failed');
|
||||||
|
|
||||||
|
// Delete the old local file if it was a hosted emoji
|
||||||
|
if (current[0].url && current[0].url.startsWith('/s/emojis/')) {
|
||||||
|
const oldFilename = path.basename(current[0].url);
|
||||||
|
const oldPath = path.join(cfg.paths.emojis, oldFilename);
|
||||||
|
await fs.unlink(oldPath).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
url = `/s/emojis/${webpFilename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no new file and no new URL, keep the existing URL
|
||||||
|
if (!url) {
|
||||||
|
url = current[0].url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await db`
|
||||||
|
UPDATE custom_emojis
|
||||||
|
SET name = ${name}, url = ${url}
|
||||||
|
WHERE id = ${id}
|
||||||
|
RETURNING id, name, url
|
||||||
|
`;
|
||||||
|
|
||||||
|
await db`NOTIFY emojis_updated, '{}'`;
|
||||||
|
return sendJson(res, { success: true, emoji: updated[0] });
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === '23505') {
|
||||||
|
return sendJson(res, { success: false, message: 'Emoji name already exists' }, 400);
|
||||||
|
}
|
||||||
|
console.error('[EMOJI EDIT ERROR]', err);
|
||||||
|
return sendJson(res, { success: false, message: err.message }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default (router, tpl) => {
|
|||||||
// List all emojis (Public)
|
// List all emojis (Public)
|
||||||
router.get('/api/v2/emojis', async (req, res) => {
|
router.get('/api/v2/emojis', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const emojis = await db`SELECT id, name, url FROM custom_emojis ORDER BY name ASC`;
|
const emojis = await db`SELECT id, name, url FROM custom_emojis ORDER BY id DESC`;
|
||||||
return res.reply({
|
return res.reply({
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ success: true, emojis })
|
body: JSON.stringify({ success: true, emojis })
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { handleUpload } from "./upload_handler.mjs";
|
|||||||
import { handleAvatarUpload, handleAvatarDelete } from "./avatar_handler.mjs";
|
import { handleAvatarUpload, handleAvatarDelete } from "./avatar_handler.mjs";
|
||||||
import { handleRethumbUpload } from "./rethumb_handler.mjs";
|
import { handleRethumbUpload } from "./rethumb_handler.mjs";
|
||||||
import { handleMemeUpload, handleMemeEdit } from "./meme_upload_handler.mjs";
|
import { handleMemeUpload, handleMemeEdit } from "./meme_upload_handler.mjs";
|
||||||
import { handleEmojiUpload } from "./emoji_upload_handler.mjs";
|
import { handleEmojiUpload, handleEmojiEdit } from "./emoji_upload_handler.mjs";
|
||||||
import { handleHallImageUpload, handleHallImageDelete, handleHallDelete, handleHallUpdate, handleHallCreate } from "./hall_image_handler.mjs";
|
import { handleHallImageUpload, handleHallImageDelete, handleHallDelete, handleHallUpdate, handleHallCreate } from "./hall_image_handler.mjs";
|
||||||
import { handleMetaExtract } from "./meta_extract_handler.mjs";
|
import { handleMetaExtract } from "./meta_extract_handler.mjs";
|
||||||
import { handleMetaStrip } from "./meta_strip_handler.mjs";
|
import { handleMetaStrip } from "./meta_strip_handler.mjs";
|
||||||
@@ -795,6 +795,16 @@ process.on('uncaughtException', err => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Bypass middleware for emoji edits
|
||||||
|
app.use(async (req, res) => {
|
||||||
|
const editMatch = req.url.pathname.match(/^\/api\/v2\/admin\/emojis\/(\d+)\/edit$/);
|
||||||
|
if (req.method === 'POST' && editMatch) {
|
||||||
|
req.params = { id: editMatch[1] };
|
||||||
|
await handleEmojiEdit(req, res);
|
||||||
|
req.url.pathname = '/handled_emoji_edit_bypass';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Bypass middleware for hall image uploads (multipart — needs raw body)
|
// Bypass middleware for hall image uploads (multipart — needs raw body)
|
||||||
app.use(async (req, res) => {
|
app.use(async (req, res) => {
|
||||||
if (cfg.websrv.halls_enabled === false) return;
|
if (cfg.websrv.halls_enabled === false) return;
|
||||||
|
|||||||
@@ -16,43 +16,66 @@
|
|||||||
<label style="display: block; font-size: 0.8em; margin-bottom: 5px; opacity: 0.7;">Image File</label>
|
<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>
|
</div>
|
||||||
<div style="flex-grow: 1;">
|
|
||||||
<label style="display: block; font-size: 0.8em; margin-bottom: 5px; opacity: 0.7;">OR Image URL</label>
|
|
||||||
<input type="text" id="emoji-url" placeholder="" style="background: var(--bg); border: 1px solid var(--black); padding: 5px; color: var(--white); width: 100%;">
|
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<div style="margin-bottom: 20px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap;">
|
|
||||||
<button id="reconvert-webp" class="btn-upload" style="width: auto; padding: 7px 18px; border: 1px solid var(--nav-border-color); background: var(--bg); color: var(--white); cursor: pointer;">
|
|
||||||
Reconvert All to WebP
|
|
||||||
</button>
|
|
||||||
<span id="reconvert-status" style="font-size: 0.85em; opacity: 0.8;"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="emoji-list" class="emoji-grid">
|
<div id="emoji-list" class="emoji-grid">
|
||||||
<!-- Populated by JS -->
|
<!-- Populated by JS -->
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(() => {
|
(() => {
|
||||||
var i18n = window.f0ckI18n || {};
|
var i18n = window.f0ckI18n || {};
|
||||||
const esc = (s) => (s || '').toString().replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
const esc = (s) => (s || '').toString().replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||||
|
|
||||||
const loadEmojis = async () => {
|
const loadEmojis = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v2/emojis');
|
const res = await fetch('/api/v2/emojis');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
window.emojiAdmin.emojis = data.emojis;
|
||||||
const grid = document.getElementById('emoji-list');
|
const grid = document.getElementById('emoji-list');
|
||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
grid.innerHTML = data.emojis.reverse().map(e =>
|
grid.innerHTML = data.emojis.map(e =>
|
||||||
'<div class="emoji-card">' +
|
'<div class="emoji-card">' +
|
||||||
'<button class="emoji-delete" onclick="window.emojiAdmin.deleteEmoji(' + e.id + ')" title="Delete">✕</button>' +
|
'<button class="emoji-delete" onclick="window.emojiAdmin.deleteEmoji(' + e.id + ')" title="Delete">✕</button>' +
|
||||||
'<img class="emoji-preview" src="' + e.url + '" alt=":' + esc(e.name) + ':">' +
|
'<img class="emoji-preview" src="' + e.url + '" alt=":' + esc(e.name) + ':">' +
|
||||||
'<span class="emoji-label">:' + esc(e.name) + ':</span>' +
|
'<span class="emoji-label">:' + esc(e.name) + ':</span>' +
|
||||||
'<span class="emoji-url">' + esc(e.url) + '</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>'
|
'</div>'
|
||||||
).join('');
|
).join('');
|
||||||
}
|
}
|
||||||
@@ -62,10 +85,9 @@
|
|||||||
const addEmoji = async (e) => {
|
const addEmoji = async (e) => {
|
||||||
if (e) e.preventDefault();
|
if (e) e.preventDefault();
|
||||||
const name = document.getElementById('emoji-name').value;
|
const name = document.getElementById('emoji-name').value;
|
||||||
const url = document.getElementById('emoji-url').value;
|
|
||||||
const fileInput = document.getElementById('emoji-file');
|
const fileInput = document.getElementById('emoji-file');
|
||||||
|
|
||||||
if (!name || (!url && !fileInput.files[0])) return alert('Fill Name and either URL or File');
|
if (!name || !fileInput.files[0]) return alert('Fill Name and select a File');
|
||||||
|
|
||||||
const btn = document.getElementById('add-emoji');
|
const btn = document.getElementById('add-emoji');
|
||||||
const oldText = btn.textContent;
|
const oldText = btn.textContent;
|
||||||
@@ -74,7 +96,6 @@
|
|||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('name', name);
|
formData.append('name', name);
|
||||||
formData.append('url', url);
|
|
||||||
if (fileInput.files[0]) {
|
if (fileInput.files[0]) {
|
||||||
formData.append('file', fileInput.files[0]);
|
formData.append('file', fileInput.files[0]);
|
||||||
}
|
}
|
||||||
@@ -93,7 +114,6 @@
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
document.getElementById('emoji-name').value = '';
|
document.getElementById('emoji-name').value = '';
|
||||||
document.getElementById('emoji-url').value = '';
|
|
||||||
document.getElementById('emoji-file').value = '';
|
document.getElementById('emoji-file').value = '';
|
||||||
loadEmojis();
|
loadEmojis();
|
||||||
} else {
|
} else {
|
||||||
@@ -124,44 +144,79 @@
|
|||||||
} catch (e) { console.error(e); }
|
} catch (e) { console.error(e); }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Global scope for onclick
|
const openEditModal = (id) => {
|
||||||
window.emojiAdmin = { deleteEmoji };
|
const emoji = (window.emojiAdmin.emojis || []).find(e => e.id === id);
|
||||||
|
if (!emoji) return;
|
||||||
|
|
||||||
const btnAddEmoji = document.getElementById('add-emoji');
|
document.getElementById('edit-emoji-id').value = emoji.id;
|
||||||
if (btnAddEmoji) btnAddEmoji.addEventListener('click', addEmoji);
|
document.getElementById('edit-emoji-name').value = emoji.name;
|
||||||
|
document.getElementById('edit-emoji-file').value = '';
|
||||||
|
|
||||||
// Reconvert all emojis to WebP
|
const preview = document.getElementById('edit-emoji-preview');
|
||||||
const reconvertEmojis = async () => {
|
preview.src = emoji.url;
|
||||||
const btn = document.getElementById('reconvert-webp');
|
preview.alt = ':' + emoji.name + ':';
|
||||||
const status = document.getElementById('reconvert-status');
|
|
||||||
if (!btn || !status) return;
|
|
||||||
if (!confirm('Reconvert all non-WebP emojis to WebP? This will delete the originals.')) return;
|
|
||||||
|
|
||||||
|
const modal = document.getElementById('edit-emoji-modal');
|
||||||
|
if (modal) modal.style.display = 'flex';
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeEditModal = () => {
|
||||||
|
const 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');
|
||||||
|
|
||||||
|
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;
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
status.textContent = '\u23F3 Converting\u2026';
|
btn.textContent = 'Saving...';
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('name', name);
|
||||||
|
if (fileInput.files[0]) {
|
||||||
|
formData.append('file', fileInput.files[0]);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const headers = { 'X-Requested-With': 'XMLHttpRequest' };
|
||||||
const csrf = '{{ csrf_token }}';
|
const csrf = '{{ csrf_token }}';
|
||||||
const res = await fetch('/api/v2/admin/emojis/reconvert', {
|
if (csrf) headers['X-CSRF-Token'] = csrf;
|
||||||
|
|
||||||
|
const res = await fetch('/api/v2/admin/emojis/' + id + '/edit', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'X-CSRF-Token': csrf, 'X-Requested-With': 'XMLHttpRequest' }
|
headers: headers,
|
||||||
|
body: formData
|
||||||
});
|
});
|
||||||
const result = await res.json();
|
|
||||||
if (result.success) {
|
const data = await res.json();
|
||||||
status.textContent = '\u2705 Done \u2014 converted: ' + result.converted + ', skipped: ' + result.skipped + ', errors: ' + result.errors;
|
if (data.success) {
|
||||||
if (result.converted > 0) loadEmojis();
|
closeEditModal();
|
||||||
|
loadEmojis();
|
||||||
} else {
|
} else {
|
||||||
status.textContent = '\u274C Failed: ' + (result.message || 'Unknown error');
|
alert('Save failed: ' + (data.message || data.msg || 'Unknown error'));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (e) {
|
||||||
status.textContent = '\u274C Error: ' + err.message;
|
console.error('[EMOJI_ADMIN] Edit Error:', e);
|
||||||
|
alert('Save failed: ' + e.message);
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
|
btn.textContent = oldText;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const btnReconvert = document.getElementById('reconvert-webp');
|
// Global scope for onclick handlers
|
||||||
if (btnReconvert) btnReconvert.addEventListener('click', reconvertEmojis);
|
window.emojiAdmin = { deleteEmoji, openEditModal, closeEditModal, saveEmoji, emojis: [] };
|
||||||
|
|
||||||
|
const btnAddEmoji = document.getElementById('add-emoji');
|
||||||
|
if (btnAddEmoji) btnAddEmoji.addEventListener('click', addEmoji);
|
||||||
|
|
||||||
// Live Update Listener (SSE dispatched via f0ckm.js)
|
// Live Update Listener (SSE dispatched via f0ckm.js)
|
||||||
document.addEventListener('f0ck:emojis_updated', loadEmojis);
|
document.addEventListener('f0ck:emojis_updated', loadEmojis);
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
<div class="modal-content content-warning-content">
|
<div class="modal-content content-warning-content">
|
||||||
<h3 style="color: var(--danger);">{{ t('account_warning.title') }}</h3>
|
<h3 style="color: var(--danger);">{{ t('account_warning.title') }}</h3>
|
||||||
<p>{{ t('account_warning.text') }}</p>
|
<p>{{ t('account_warning.text') }}</p>
|
||||||
<blockquote id="warning-reason" style="border-left: 4px solid var(--danger); padding-left: 10px; margin: 10px 0; font-style: italic;"></blockquote>
|
<blockquote id="warning-reason" style="border-left: 4px solid var(--danger); padding-left: 10px; margin: 10px 0;"></blockquote>
|
||||||
<input type="hidden" id="warning-id">
|
<input type="hidden" id="warning-id">
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button id="warning-acknowledge" class="btn-danger">{{ t('account_warning.acknowledge') }}</button>
|
<button id="warning-acknowledge" class="btn-danger">{{ t('account_warning.acknowledge') }}</button>
|
||||||
@@ -441,7 +441,7 @@
|
|||||||
upload: "{{ t('upload_btn.upload') }}",
|
upload: "{{ t('upload_btn.upload') }}",
|
||||||
uploading: "{{ t('upload.uploading') }}",
|
uploading: "{{ t('upload.uploading') }}",
|
||||||
processing: "{{ t('toast.processing') }}",
|
processing: "{{ t('toast.processing') }}",
|
||||||
upload_await_approval: "{{ t('upload.pending_approval_patient') }}",
|
upload_pending_approval_patient: "{{ t('upload.pending_approval_patient') }}",
|
||||||
upload_shitpost_success: "{{ t('upload.shitpost_success') || 'Successfully shitposted {n} items!' }}",
|
upload_shitpost_success: "{{ t('upload.shitpost_success') || 'Successfully shitposted {n} items!' }}",
|
||||||
upload_shitposting_status: "{{ t('upload.shitposting_status') || 'Shitposting' }}",
|
upload_shitposting_status: "{{ t('upload.shitposting_status') || 'Shitposting' }}",
|
||||||
upload_comment_placeholder: "{{ t('upload.item_comment_placeholder') || 'Comment (optional)...' }}",
|
upload_comment_placeholder: "{{ t('upload.item_comment_placeholder') || 'Comment (optional)...' }}",
|
||||||
|
|||||||
Reference in New Issue
Block a user