diff --git a/api/emotes.php b/api/emotes.php
new file mode 100644
index 0000000..b02f439
--- /dev/null
+++ b/api/emotes.php
@@ -0,0 +1,139 @@
+query("SELECT * FROM custom_emotes ORDER BY created_at DESC");
+ echo json_encode(['success' => true, 'emotes' => $stmt->fetchAll()]);
+ } catch (Exception $e) {
+ echo json_encode(['success' => false, 'error' => $e->getMessage()]);
+ }
+ exit;
+}
+
+if ($action === 'upload' && $_SERVER['REQUEST_METHOD'] === 'POST') {
+ if (!isset($_FILES['emote'])) {
+ echo json_encode(['success' => false, 'error' => 'Aucun fichier reçu (emote)']);
+ exit;
+ }
+
+ if ($_FILES['emote']['error'] !== UPLOAD_ERR_OK) {
+ $errorMsg = 'Erreur d\'upload PHP: ' . $_FILES['emote']['error'];
+ if ($_FILES['emote']['error'] === 1) $errorMsg = 'Fichier trop volumineux (limit PHP)';
+ if ($_FILES['emote']['error'] === 4) $errorMsg = 'Aucun fichier sélectionné';
+ echo json_encode(['success' => false, 'error' => $errorMsg]);
+ exit;
+ }
+
+ $name = preg_replace('/[^a-z0-9_]/', '', strtolower($_POST['name'] ?? 'emote'));
+ if (empty($name)) {
+ echo json_encode(['success' => false, 'error' => 'Nom invalide']);
+ exit;
+ }
+
+ $file = $_FILES['emote'];
+ $ext = pathinfo($file['name'], PATHINFO_EXTENSION);
+ if (!in_array(strtolower($ext), ['png', 'jpg', 'jpeg'])) {
+ echo json_encode(['success' => false, 'error' => 'Format non supporté (PNG uniquement recommandé)']);
+ exit;
+ }
+
+ $uploadDir = __DIR__ . '/../assets/images/custom_emotes/';
+ if (!is_dir($uploadDir)) {
+ mkdir($uploadDir, 0775, true);
+ }
+
+ $fileName = time() . '_' . $name . '.png';
+ $targetPath = $uploadDir . $fileName;
+
+ // Process image: Resize to 48x48
+ $srcImage = null;
+ if (strtolower($ext) === 'png') $srcImage = imagecreatefrompng($file['tmp_name']);
+ else if (in_array(strtolower($ext), ['jpg', 'jpeg'])) $srcImage = imagecreatefromjpeg($file['tmp_name']);
+
+ if (!$srcImage) {
+ echo json_encode(['success' => false, 'error' => 'Fichier image corrompu']);
+ exit;
+ }
+
+ $width = imagesx($srcImage);
+ $height = imagesy($srcImage);
+ $newSize = 48;
+ $dstImage = imagecreatetruecolor($newSize, $newSize);
+
+ // Transparency for PNG
+ imagealphablending($dstImage, false);
+ imagesavealpha($dstImage, true);
+ $transparent = imagecolorallocatealpha($dstImage, 255, 255, 255, 127);
+ imagefilledrectangle($dstImage, 0, 0, $newSize, $newSize, $transparent);
+
+ imagecopyresampled($dstImage, $srcImage, 0, 0, 0, 0, $newSize, $newSize, $width, $height);
+ imagepng($dstImage, $targetPath);
+
+ imagedestroy($srcImage);
+ imagedestroy($dstImage);
+
+ $relativePath = 'assets/images/custom_emotes/' . $fileName;
+ $code = ':' . $name . ':';
+
+ try {
+ $stmt = db()->prepare("INSERT INTO custom_emotes (name, path, code) VALUES (?, ?, ?)");
+ $stmt->execute([$name, $relativePath, $code]);
+ echo json_encode([
+ 'success' => true,
+ 'emote' => [
+ 'id' => db()->lastInsertId(),
+ 'name' => $name,
+ 'path' => $relativePath,
+ 'code' => $code
+ ]
+ ]);
+ } catch (Exception $e) {
+ echo json_encode(['success' => false, 'error' => $e->getMessage()]);
+ }
+ exit;
+}
+
+if ($action === 'rename' && $_SERVER['REQUEST_METHOD'] === 'POST') {
+ $id = $_POST['id'] ?? 0;
+ $newName = preg_replace('/[^a-z0-9_]/', '', strtolower($_POST['name'] ?? ''));
+ if (empty($newName)) {
+ echo json_encode(['success' => false, 'error' => 'Nom invalide']);
+ exit;
+ }
+
+ try {
+ $code = ':' . $newName . ':';
+ $stmt = db()->prepare("UPDATE custom_emotes SET name = ?, code = ? WHERE id = ?");
+ $stmt->execute([$newName, $code, $id]);
+ echo json_encode(['success' => true, 'name' => $newName, 'code' => $code]);
+ } catch (Exception $e) {
+ echo json_encode(['success' => false, 'error' => $e->getMessage()]);
+ }
+ exit;
+}
+
+if ($action === 'delete' && $_SERVER['REQUEST_METHOD'] === 'POST') {
+ $id = $_POST['id'] ?? 0;
+ try {
+ $stmt = db()->prepare("SELECT path FROM custom_emotes WHERE id = ?");
+ $stmt->execute([$id]);
+ $emote = $stmt->fetch();
+ if ($emote) {
+ $filePath = __DIR__ . '/../' . $emote['path'];
+ if (file_exists($filePath)) unlink($filePath);
+
+ $stmt = db()->prepare("DELETE FROM custom_emotes WHERE id = ?");
+ $stmt->execute([$id]);
+ echo json_encode(['success' => true]);
+ } else {
+ echo json_encode(['success' => false, 'error' => 'Emote non trouvée']);
+ }
+ } catch (Exception $e) {
+ echo json_encode(['success' => false, 'error' => $e->getMessage()]);
+ }
+ exit;
+}
diff --git a/assets/css/discord.css b/assets/css/discord.css
index 92e2995..04dfcd1 100644
--- a/assets/css/discord.css
+++ b/assets/css/discord.css
@@ -872,6 +872,48 @@ body {
border-left: 2px solid var(--blurple);
}
+/* Emotes Tab in Settings */
+#settings-emotes-sidebar button {
+ border-radius: 4px;
+ transition: background-color 0.2s, color 0.2s;
+ color: var(--text-muted) !important;
+}
+
+#settings-emotes-sidebar button:hover {
+ background-color: var(--hover) !important;
+ color: var(--text-primary) !important;
+}
+
+#settings-emotes-sidebar button.active {
+ background-color: var(--active) !important;
+ color: white !important;
+}
+
+.role-emoji-item {
+ transition: transform 0.1s, background-color 0.2s;
+ border: 1px solid transparent;
+}
+
+.role-emoji-item:hover {
+ transform: scale(1.2);
+ background-color: rgba(255, 255, 255, 0.1) !important;
+ border-color: var(--blurple);
+ z-index: 1;
+}
+
+.role-emoji-item img {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+}
+
+#custom-emote-upload-zone .btn {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ font-weight: 600;
+}
+
.pinned-badge {
font-size: 0.7em;
color: var(--blurple);
diff --git a/assets/js/main.js b/assets/js/main.js
index 2449f47..67fc062 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -32,30 +32,221 @@ document.addEventListener('DOMContentLoaded', () => {
'Smileys': ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '🥰', '😍', '🤩', '😘', '😗', '😚', '😙', '😋', '😛', '😜', '🤪', '😝', '🤑', '🤗', '🤭', '🤫', '🤔', '🤐', '🤨', '😐', '😑', '😶', '😏', '😒', '🙄', '😬', '🤥', '😌', '😔', '😪', '🤤', '😴', '😷', '🤒', '🤕', '🤢', '🤮', '🤧', '🥵', '🥶', '🥴', '😵', '🤯', '🤠', '🥳', '😎', '🤓', '🧐', '😕', '😟', '🙁', '☹️', '😮', '😯', '😲', '😳', '🥺', '😦', '😧', '😨', '😰', '😥', '😢', '😭', '😱', '😖', '😣', '😞', '😓', '😩', '😫', '🥱', '😤', '😡', '😠', '🤬', '😈', '👿', '👹', '👺', '💀', '☠️', '💩', '🤡', '👻', '👽', '👾', '🤖', '😺', '😸', '😻', '😼', '😽', '🙀', '😿', '😾', '🙈', '🙉', '🙊', '💋', '💌', '💘', '💝', '💖', '💗', '💓', '💞', '💕', '💟', '❣️', '💔', '❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💯', '💢', '💥', '💫', '💦', '💨', '🕳️', '💣', '💬', '👁️🗨️', '🗨️', '🗯️', '💭', '💤', '🪐', '🌠', '🎇', '🎆', '🌇', '🌆', '🏙️', '🌃', '🌌'],
'Gestures': ['👋', '🤚', '🖐️', '✋', '🖖', '👌', '🤏', '✌️', '🤞', '🤟', '🤘', '🤙', '👈', '👉', '👆', '🖕', '👇', '☝️', '👍', '👎', '✊', '👊', '🤛', '🤜', '👏', '🙌', '👐', '🤲', '🤝', '🙏', '✍️', '💅', '🤳', '💪', '🦾', '🦵', '🦿', '🦶', '👂', '🦻', '👃', '🧠', '🦷', '🦴', '👀', '👁️', '👅', '👄', '🖖', '🤘', '🤙', '🖐️', '🖕', '🖖', '✍️', '🤳', '💪', '🦾'],
'People': ['👶', '🧒', '👦', '👧', '🧑', '👱', '👨', '👩', '🧓', '👴', '👵', '👮', '🕵️', '💂', '👷', '🤴', '👸', '👳', '👲', '🧕', '🤵', '👰', '🤰', '🤱', '👼', '🎅', '🤶', '🦸', '🦹', '🧙', '🧚', '🧛', '🧜', '🧝', '🧞', '🧟', '💆', '💇', '🚶', '🏃', '💃', '🕺', '🕴️', '👯', '🧖', '🧗', '🤺', '🏇', '⛷️', '🏂', '🏌️', '🏄', '🚣', '🏊', '⛹️', '🏋️', '🚴', '🚵', '🤸', '🤼', '🤽', '🤾', '🤹', '🧘', '🛀', '🛌'],
- 'Animals': ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐨', '🐯', '🦁', '🐮', '🐷', '🐽', '🐸', '🐵', '🙈', '🙉', '🙊', '🐒', '🐔', '🐧', '🐦', '🐤', '🐣', '🐥', '🦆', '🦅', '🦉', '🦇', '🐺', '🐗', '🐴', '🦄', '🐝', '🐛', '🦋', '🐌', '🐞', '🐜', '🦟', '🦗', '🕷️', '🕸️', '🦂', '🐢', '🐍', '🦎', '🦖', '🦕', '🐙', '🦑', '🦐', '🦞', '🦀', '🐡', '🐠', '🐟', '🐬', '🐳', '🐋', '🦈', '🐊', '🐅', '🐆', '🦓', '🦍', '🦧', '🐘', '🦛', '🦏', '🐪', '🐫', '🦒', '🦘', '🦬', '🐃', '🐂', '🐄', '🐎', '🐖', '🐏', '🐑', '🐐', '🦌', '🐕', '🐩', '🦮', '🐕🦺', '🐈', '🐈⬛', '🐓', '🦃', '🦚', '🦜', '🦢', '🦩', '🕊️', '🐇', '🦝', '🦨', '🦡', '🦦', '🦥', '🐁', '🐀', '🐿️', '🦔', '🐾', '🐉', '🐲', '🌵', '🎄', '🌲', '🌳', '🌴', '🌱', '🌿', '☘️', '🍀', '🎍', '🎋', '🍃', '🍂', '🍁', '🍄', '🐚', '🌾'],
+ 'Animals': ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐨', '🐯', '🦁', '🐮', '🐷', '🐽', '🐸', '🐵', '🙈', '🙉', '🙊', '🐒', '🐔', '🐧', '🐦', '🐤', '🐣', '🐥', '🦆', '🦅', '🦉', '🦇', '🐺', '🐗', '🐴', '🦄', '🐝', '🐛', '🦋', '🐌', '🐞', '🐜', '🦟', '🦗', '🕷️', '🕸️', '蠍', '🐢', '🐍', '🦎', '🦖', '🦕', '🐙', '🦑', '🦐', '🦞', '🦀', '🐡', '🐠', '🐟', '🐬', '🐳', '🐋', '🦈', '🐊', '🐅', '🐆', '🦓', '🦍', '🦧', '🐘', '🦛', '🦏', '🐪', '🐫', '🦒', '🦘', '🦬', '🐃', '🐂', '🐄', '🐎', '🐖', '🐏', '🐑', '🐐', '🦌', '🐕', '🐩', '🦮', '🐕🦺', '🐈', '🐈⬛', '🐓', '🦃', '🦚', '🦜', '🦢', '🦩', '🕊️', '🐇', '🦝', '🦨', '🦡', '🦦', '🦥', '🐁', '🐀', '🐿️', '🦔', '🐾', '🐉', '🐲', '🌵', '🎄', '🌲', '🌳', '🌴', '🌱', '🌿', '☘️', '🍀', '🎍', '🎋', '🍃', '🍂', '🍁', '🍄', '🐚', '🌾'],
'Nature': ['💐', '🌷', '🌹', '🥀', '🌺', '🌸', '🌼', '🌻', '🌞', '🌝', '🌛', '🌜', '🌚', '🌕', '🌖', '🌗', '🌘', '🌑', '🌒', '🌓', '🌔', '🌙', '🌎', '🌍', '🌏', '🪐', '💫', '⭐️', '🌟', '✨', '⚡️', '☄️', '💥', '🔥', '🌪️', '🌈', '☀️', '🌤️', '⛅️', '🌥️', '☁️', '🌦️', '🌧️', '🌨️', '🌩️', '❄️', '☃️', '⛄️', '🌬️', '💨', '💧', '💦', '☔️', '☂️', '🌊', '🌫️', '⛰️', '🏔️', '🗻', '🌋', '🏜️', '🏖️', '🏝️', '🏕️', '⛺️'],
- 'Food': ['🍏', '🍎', '🍐', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🍈', '🍒', '🍑', '🥭', '🍍', '🥥', '🥝', '🍅', '🍆', '🥑', '🥦', '🥬', '🥒', '🌽', '🥕', '🧄', '🧅', '🍄', '🥜', '🌰', '🍞', '🥐', '🥖', '🥨', '🥯', '🥞', '🧇', '🧀', '🍖', '🍗', '🥩', '🥓', '🍔', '🍟', '🍕', '🌭', '🥪', '🌮', '🌯', '🥙', '🧆', '🍳', '🥘', '🍲', '🥣', '🥗', '🍿', '🧈', '🧂', '🥫', '🍱', '🍘', '🍙', '🍚', '🍛', '🍜', '🍝', '🍠', '🍢', '🍣', '🍤', '🍥', '🥮', '🍡', '🥟', '🥠', '🥡', '🍦', '🍧', '🍨', '🍩', '🍪', '🎂', '🍰', '🧁', '🥧', '🍫', '🍬', '🍭', '🍮', '🍯', '🍼', '🥛', '☕️', '🍵', '🧉', '🥤', '🧃', '🍺', '🍻', '🥂', '🍷', '🥃', '🍸', '🍹', '🍾', '🧊', '🥄', '🍴', '🍽️'],
- 'Activities': ['⚽️', '🏀', '🏈', '⚾️', '🥎', '🎾', '🏐', '🏉', '🎱', '🏓', '🏸', '🥅', '🏒', '🏑', '🏏', '⛳️', '🏹', '🎣', '🥊', '🥋', '🛹', '🛷', '⛸️', '🥌', '🎿', '⛷️', '🏂', '🏋️', '🤺', '🤼', '🤸', '⛹️', '🤽', '🤾', '🤹', '🧘', '🏇', '🚣', '🏊', '🚴', '🚵', '🧗', '🎖️', '🏆', '🏅', '🥇', '🥈', '🥉', '🎫', '🎟️', '🎭', '🎨', '🎬', '🎤', '🎧', '🎼', '🎹', '🥁', '🎷', '🎺', '🎸', '🪕', '🎻', '🎲', '♟️', '🎯', '🎳', '🎮', '🎰', '🧩'],
- 'Travel': ['🚗', '🚕', '🚙', '🚌', '🚎', '🏎️', '🚓', '🚑', '🚒', '🚐', '🚚', '🚛', '🚜', '🛵', '🚲', '🛴', '🚏', '🛣️', '🛤️', '⛽️', '🚨', '🚥', '🚦', '🚧', '⚓️', '⛵️', '🚤', '🛳️', '⛴️', '🚢', '✈️', '🛫', '🛬', '💺', '🚁', '🚟', '🚠', '🚡', '🚀', '🛸', '🛰️', '⌛️', '⏳', '⌚️', '⏰', '⏱️', '⏲️', '🕰️', '🌡️', '🌃', '🏙️', '🌄', '🌅', '🌆', '🌇', '🌉', '🎠', '🎡', '🎢', '🚂', '🚃', '🚄', '🚅', '🚆', '🚇', '🚈', '🚉', '🚊', '🚝', '🚞', '🚋'],
- 'Objects': ['⌚️', '📱', '📲', '💻', '⌨️', '🖱️', '🖲️', '🕹️', '🗜️', '💽', '💾', '💿', '📀', '📼', '📷', '📸', '📹', '🎥', '📽️', '🎞️', '📞', '📠', '📺', '📻', '🎙️', '🎚️', '🎛️', '🧭', '⏱️', '⏲️', '⏰', '🕰️', '⌛️', '⏳', '📡', '🔋', '🔌', '💡', '🔦', '🕯️', '🪔', '🧯', '🛢️', '💸', '💵', '💴', '💶', '💷', '💰', '💳', '💎', '⚖️', '🧰', '🔧', '🔨', '⚒️', '🛠️', '⛏️', '🔩', '⚙️', '🧱', '⛓️', '🧲', '🔫', '💣', '🧨', '🪓', '🔪', '🗡️', '⚔️', '🛡️', '🚬', '⚰️', '⚱️', '🏺', '🔮', '🧿', '📿', '💈', '⚗️', '🔭', '🔬', '🕳️', '💊', '💉', '🩸', '🧬', '🦠', '🧫', '🧪', '🌡️', '🧹', '🧺', '🧻', '🧼', '🧽', '🪒', '🧴', '🛎️', '🔑', '🗝️', '🚪', '🪑', '🛋️', '🛏️', '🛌', '🧸', '🖼️', '🛍️', '🛒', '🎁', '🎈', '🎏', '🎀', '🎊', '🎉', '🎎', '🏮', '🎐', '🧧', '✉️', '📩', '📨', '📧', '💌', '📥', '📤', '📦', '🏷️', '📁', '📂', '📅', '📆', '🗒️', '🗓️', '📇', '📈', '📉', '📊', '📋', '📌', '📍', '📎', '🖇️', '📏', '📐', '✂️', '🗃️', '🗄️', '🗑️', '🔒', '🔓', '🔏', '🔐', '🔑', '🗝️'],
- 'Symbols': ['💘', '💝', '💖', '💗', '💓', '💞', '💕', '💟', '❣️', '💔', '❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💯', '💢', '💥', '💫', '💦', '💨', '🕳️', '💣', '💬', '👁️🗨️', '🗨️', '🗯️', '💭', '💤', '🌐', '♠️', '♥️', '♦️', '♣️', '🃏', '🀄️', '🎴', '🔇', '🔈', '🔉', '🔊', '📢', '📣', '📯', '🔔', '🔕', '🎼', '🎵', '🎶', '💹', '🏧', '🚮', '🚰', '♿️', '🚹', '🚺', '🚻', '🚼', '🚾', '🛂', '🛃', '🛄', '🛅', '⚠️', '🚸', '⛔️', '🚫', '🚳', '🚭', '🚯', '🚱', '🚷', '📵', '🔞', '☢️', '☣️', '⬆️', '↗️', '➡️', '↘️', '⬇️', '↙️', '⬅️', '↖️', '↕️', '↔️', '↩️', '↪️', '⤴️', '⤵️', '🔃', '🔄', '🔙', '🔚', '🔛', '🔜', '🔝', '🛐', '⚛️', '🕉️', '✡️', '☸️', '☯️', '✝️', '☦️', '☪️', '☮️', '🕎', '🔯', '♈️', '♉️', '♊️', '♋️', '♌️', '♍️', '♎️', '♏️', '♐️', '♑️', '♒️', '♓️', '⛎', '🔀', '🔁', '🔂', '▶️', '⏩', '⏭️', '⏯️', '◀️', '⏪', '⏮️', '🔼', '⏫', '🔽', '⏬', '⏸️', '⏹️', '⏺️', '⏏️', '🎦', '🔅', '🔆', '📶', '📳', '📴', '➕', '➖', '➗', '✖️', '♾️', '‼️', '⁉️', '❓', '❔', '❕', '❗️', '〰️', '💱', '💲', '⚕️', '♻️', '⚜️', '🔱', '📛', '🔰', '⭕️', '✅', '☑️', '✔️', '✖️', '❌', '❎', '➰', '➿', '〽️', '✳️', '✴️', '❇️', '‼️', '🈁', '🈂️', '🈷️', '🈶', '🈯️', '🉐', '🈹', '🈚️', '🈲', '🉑', '🈸', '🈴', '🈳', '㊗️', '㊙️', '🈺', '🈵', '🔴', '🟠', '🟡', '🟢', '🔵', '🟣', '🟤', '⚫️', '⚪️', '🟥', '🟧', '🟨', '🟩', '🟦', '🟪', '🟫', '⬛️', '⬜️', '♈', '♉', '♊', '♋', '♌', '♍', '♎', '♏', '♐', '♑', '♒', '♓', '⛎', '☸', '☦', '☦', '☪', '☮', '☯', '♈', '♉', '♊', '♋', '♌', '♍', '♎', '♏', '♐', '♑', '♒', '♓', '⛎', '♀', '♂', '⚕', '♾', '⚓', '⚔', '⚖', '⚗', '⚙', '⚖', '⚓', '⚔'],
+ 'Food': ['🍏', '🍎', '🍐', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🍈', '🍒', '🍑', '🥭', '🍍', '🥥', '🥝', '🍅', '🍆', '🥑', '🥦', '🥬', '🥒', '🌽', '🥕', '🧄', '🧅', '🍄', '🥜', '🌰', '🍞', '🥐', '🥖', '🥨', '🥯', '🥞', '🧇', '🧀', '🍖', '🍗', '🥩', '🥓', '🍔', '🍟', '🍕', '🌭', '🥪', '🌮', '🌯', '🥙', '🧆', '🍳', '🥘', '🍲', '🥣', '🥗', '🍿', 'バター', '🧂', '🥫', '🍱', '🍘', '🍙', '🍚', '🍛', '🍜', '🍝', '🍠', '🍢', '🍣', '🍤', '🍥', '🥮', '🍡', '🥟', '🥠', '🥡', '🍦', '🍧', '🍨', '🍩', '🍪', '🎂', '🍰', '🧁', '🥧', '🍫', '🍬', '🍭', '🍮', '🍯', '🍼', '🥛', '☕️', '🍵', '🧉', '🥤', '🧃', '🍺', '🍻', '🥂', '🍷', '🥃', '🍸', '🍹', '🍾', '🧊', '🥄', '🍴', '🍽️'],
+ 'Activities': ['⚽️', '🏀', '🏈', '⚾️', '🥎', '🎾', '🏐', '🏉', '🎱', '🏓', '🏸', '🥅', '🏒', '🏑', '🏏', '⛳️', '🏹', '🎣', '🥊', '🥋', '🛹', '🛷', '⛸️', '🥌', '🎿', '⛷️', '🏂', '🏋️', '🤺', '🤼', '🤸', '⛹️', '🤽', '🤾', '🤹', '🧘', '🏇', '🚣', '🏊', '🚴', '🚵', '🧗', '🎖️', '🏆', '🏅', '🥇', '🥈', '🥉', '🎫', '🎟️', '🎭', '🎨', '🎬', '🎤', '🎧', '🎼', '🎹', '🥁', '🎷', '🎺', '🎸', '🪕', '🎻', '🎲', '♟️', '🎯', 'コツ', '🎮', '🎰', '🧩'],
+ 'Travel': ['🚗', '🚕', '🚙', '🚌', '🚎', '🏎️', '🚓', '🚑', '🚒', '🚐', '🚚', '🚛', '🚜', '🛵', '🚲', '🛴', '🚏', '🛣️', '🛤️', '⛽️', '🚨', '🚥', '🚦', '🚧', '⚓️', '⛵️', '🚤', '🛳️', '⛴️', '🚢', '✈️', '🛫', '🛬', '💺', '🚁', '🚟', 'ケーブル', '🚡', '🚀', '🛸', '🛰️', '⌛️', '⏳', '⌚️', '⏰', '⏱️', '⏲️', '🕰️', '🌡️', '🌃', '🏙️', '🌄', '🌅', '🌆', '🌇', '🌉', '🎠', '🎡', '🎢', '🚂', '🚃', '🚄', '🚅', '🚆', '🚇', '🚈', '🚉', '🚊', '🚝', '🚞', '🚋'],
+ 'Objects': ['⌚️', '📱', '📲', '💻', '⌨️', '🖱️', '🖲️', '🕹️', '🗜️', '💽', '💾', '💿', '📀', '📼', '📷', '📸', '📹', '🎥', '📽️', '🎞️', '📞', '📠', '📺', '📻', '🎙️', '🎚️', '🎛️', '🧭', '⏱️', '⏲️', '⏰', '🕰️', '⌛️', '⏳', '📡', '🔋', 'プラグ', '💡', '🔦', '🕯️', '🪔', '🧯', '🛢️', '💸', '💵', '💴', '💶', '💷', '💰', '💳', '💎', '⚖️', '🧰', 'レンチ', '🔨', '⚒️', '🛠️', '⛏️', 'ナット', '⚙️', '🧱', '鎖', '🧲', '🔫', '💣', '🧨', '🪓', 'ナイフ', '🗡️', '⚔️', '盾', '🚬', '⚰️', '⚱️', '🏺', '水晶', '🧿', '📿', '💈', '⚗️', '望遠鏡', '🔬', '🕳️', '💊', '💉', '🩸', 'DNA', '🦠', '🧫', '🧪', '🌡️', '🧹', 'カゴ', '🧻', '石鹸', 'スポンジ', '🪒', 'ローション', '🛎️', '鍵', '🗝️', 'ドア', '椅子', 'ソファ', 'ベッド', '🛌', 'テディベア', '額縁', '袋', 'カート', '🎁', '🎈', '🎏', 'リボン', '🎊', '🎉', '人形', '提灯', '🎐', '🧧', '✉️', '📩', '📨', '📧', '💌', '📥', '📤', '📦', '🏷️', 'フォルダ', '📂', 'カレンダー', '📆', '🗒️', '🗓️', '📇', 'チャート', '📉', '📊', 'クリップボード', '画鋲', '📍', '📎', '🖇️', '定規', '📐', 'ハサミ', '🗃️', 'キャビネット', 'ゴミ箱', '🔒', '🔓', '🔏', '🔐', '鍵', '🗝️'],
+ 'Symbols': ['💘', '💝', '💖', '💗', '💓', '💞', '💕', '💟', '❣️', '💔', '❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💯', '💢', '💥', '💫', '💦', '💨', '🕳️', '💣', '💬', '👁️🗨️', '🗨️', '🗯️', '💭', '💤', '🌐', '♠️', '♥️', '♦️', '♣️', 'ジョーカー', '🀄️', '🎴', '🔇', '🔈', '🔉', '🔊', '📢', '📣', '📯', '🔔', '🔕', '🎼', '🎵', '🎶', '💹', 'ATM', '🚮', '🚰', '♿️', '🚹', '🚺', '🚻', '🚼', '🚾', '🛂', 'カスタム', 'バゲージ', '🛅', '⚠️', '🚸', '⛔️', '🚫', '🚳', '🚯', '🚱', '🚷', '📵', '🔞', '放射能', 'バイオ', '⬆️', '↗️', '➡️', '↘️', '⬇️', '↙️', '⬅️', '↖️', '↕️', '↔️', '↩️', '↪️', '⤴️', '⤵️', '🔃', '🔄', '🔙', '🔚', '🔛', '🔜', '🔝', '🛐', '⚛️', '🕉️', '✡️', '☸️', '☯️', '✝️', '☦️', '☪️', '☮️', '🕎', '🔯', '♈️', '♉️', '♊️', '♋️', '♌️', '♍️', '♎️', '♏️', '♐️', '♑️', '♒️', '♓️', '⛎', '🔀', '🔁', '🔂', '▶️', '⏩', '⏭️', '⏯️', '◀️', '⏪', '⏮️', '🔼', '⏫', '🔽', '⏬', '⏸️', '⏹️', '⏺️', '⏏️', '🎦', '🔅', '🔆', '📶', '📳', '📴', '➕', '➖', '➗', '✖️', '♾️', '‼️', '⁉️', '❓', '❔', '❕', '❗️', '〰️', '💱', '💲', '⚕️', '♻️', '⚜️', '🔱', '📛', '🔰', '⭕️', '✅', '☑️', '✔️', '✖️', '❌', '❎', '➰', '➿', '〽️', '✳️', '✴️', '❇️', '‼️', '🈁', '🈂️', '🈷️', '🈶', '🈯️', '🉐', '🈹', '🈚️', '🈲', '🉑', '🈸', '🈴', '🈳', '㊗️', '㊙️', '🈺', '🈵', '🔴', '🟠', '🟡', '🟢', '🔵', '🟣', '🟤', '⚫️', '⚪️', '🟥', '🟧', '🟨', '🟩', '🟦', '🟪', '🟫', '⬛️', '⬜️', '♈', '♉', '♊', '♋', '♌', '♍', '♎', '♏', '♐', '♑', '♒', '♓', '⛎', '☸', '☦', '☦', '☪', '☮', '☯', '♈', '♉', '♊', '♋', '♌', '♍', '♎', '♏', '♐', '♑', '♒', '♓', '⛎', '♀', '♂', '⚕', '♾', '⚓', '⚔', '⚖', '⚗', '⚙', '⚖', '⚓', '⚔'],
'Flags': ['🏁', '🚩', '🎌', '🏴', '🏳️', '🏳️🌈', '🏳️⚧️', '🏴☠️', '🇦🇫', '🇦🇽', '🇦🇱', '🇩🇿', '🇦🇲', '🇦🇺', '🇦🇹', '🇦🇿', '🇧🇪', '🇧🇷', '🇨🇦', '🇨🇱', '🇨🇳', '🇨🇴', '🇨🇿', '🇩🇰', '🇪🇬', '🇫🇮', '🇫🇷', '🇩🇪', '🇬🇷', '🇭🇰', '🇮🇳', '🇮🇩', '🇮🇪', '🇮🇱', '🇮🇹', '🇯🇵', '🇰🇷', '🇲🇽', '🇳🇱', '🇳🇿', '🇳🇴', '🇵🇰', '🇵🇭', '🇵🇱', '🇵🇹', '🇷🇺', '🇸🇦', '🇸🇬', '🇿🇦', '🇪🇸', '🇸🇪', '🇨🇭', '🇹🇭', '🇹🇷', '🇺🇦', '🇦🇪', '🇬🇧', '🇺🇸', '🇻🇳', '🇦🇷', '🇧🇩', '🇧🇪', '🇧🇴', '🇮🇩', '🇮🇷', '🇮🇶', '🇯🇲', '🇰🇿', '🇰🇪', '🇲🇾', '🇲🇦', '🇳🇬', '🇵🇪', '🇷🇴', '🇷🇸', '🇸🇰', '🇺🇾', '🇿🇼']
};
+ const categoryIcons = {
+ 'Custom': '⭐',
+ 'Smileys': '😀',
+ 'Gestures': '👌',
+ 'People': '👶',
+ 'Animals': '🐶',
+ 'Nature': '🌵',
+ 'Food': '🍏',
+ 'Activities': '⚽️',
+ 'Travel': '🚗',
+ 'Objects': '⌚️',
+ 'Symbols': '❤️',
+ 'Flags': '🏁'
+ };
+
const ALL_EMOJIS = Object.values(EMOJI_CATEGORIES).flat();
+ // Unified custom emote loading and caching
+ window.CUSTOM_EMOTES_CACHE = [];
+ window.loadCustomEmotes = async () => {
+ try {
+ const resp = await fetch('api/emotes.php?action=list');
+ const data = await resp.json();
+ if (data.success) {
+ window.CUSTOM_EMOTES_CACHE = data.emotes || [];
+ return window.CUSTOM_EMOTES_CACHE;
+ }
+ return [];
+ } catch (e) {
+ console.error("Failed to load custom emotes", e);
+ return [];
+ }
+ };
+
+ // Settings Emotes Tab Logic
+ async function setupSettingsEmotes() {
+ console.log("Setting up Emotes Tab...");
+ const sidebar = document.getElementById('settings-emotes-sidebar');
+ const grid = document.getElementById('settings-emotes-grid');
+ const searchInput = document.getElementById('settings-emotes-search');
+ const uploadZone = document.getElementById('custom-emote-upload-zone');
+ const uploadInput = document.getElementById('emote-upload-input');
+
+ if (!sidebar || !grid) return;
+
+ const categories = ['Custom', ...Object.keys(EMOJI_CATEGORIES)];
+
+ const renderGrid = async (category, searchTerm = '') => {
+ grid.innerHTML = '
';
+
+ if (category === 'Custom' && !searchTerm) {
+ if (uploadZone) uploadZone.classList.remove('d-none');
+ const emotes = await window.loadCustomEmotes();
+ grid.innerHTML = '';
+
+ if (emotes.length === 0) {
+ grid.innerHTML = 'Aucune emote personnalisée. Ajoutez-en une !
';
+ } else {
+ emotes.forEach(emote => {
+ const div = document.createElement('div');
+ div.className = 'role-emoji-item rounded d-flex flex-column align-items-center justify-content-center p-2 text-center position-relative';
+ div.style.cursor = 'pointer';
+ div.style.backgroundColor = 'rgba(255,255,255,0.05)';
+ div.style.minHeight = '70px';
+ div.innerHTML = `
+
+ ${emote.code}
+
+
+
+
+ `;
+
+ div.onmouseenter = () => div.querySelector('.emote-actions')?.classList.remove('d-none');
+ div.onmouseleave = () => div.querySelector('.emote-actions')?.classList.add('d-none');
+
+ div.onclick = (e) => {
+ if (e.target.closest('.emote-actions')) return;
+ navigator.clipboard.writeText(emote.code);
+ const originalBg = div.style.backgroundColor;
+ div.style.backgroundColor = 'var(--blurple)';
+ setTimeout(() => div.style.backgroundColor = originalBg, 200);
+ };
+
+ div.querySelector('.delete-emote').onclick = async (e) => {
+ e.stopPropagation();
+ if (!confirm(`Supprimer l'emote ${emote.code} ?`)) return;
+ const fd = new FormData();
+ fd.append('id', emote.id);
+ const res = await (await fetch('api/emotes.php?action=delete', { method: 'POST', body: fd })).json();
+ if (res.success) renderGrid('Custom');
+ };
+
+ div.querySelector('.edit-emote').onclick = async (e) => {
+ e.stopPropagation();
+ const newName = prompt("Nouveau nom (sans les :)", emote.name);
+ if (!newName || newName === emote.name) return;
+ const fd = new FormData();
+ fd.append('id', emote.id);
+ fd.append('name', newName);
+ const res = await (await fetch('api/emotes.php?action=rename', { method: 'POST', body: fd })).json();
+ if (res.success) renderGrid('Custom');
+ };
+
+ grid.appendChild(div);
+ });
+ }
+ } else {
+ if (uploadZone) uploadZone.classList.add('d-none');
+ grid.innerHTML = '';
+ const list = searchTerm ? ALL_EMOJIS.filter(e => e.includes(searchTerm)) : EMOJI_CATEGORIES[category];
+
+ (list || []).forEach(emoji => {
+ const div = document.createElement('div');
+ div.className = 'role-emoji-item rounded d-flex align-items-center justify-content-center p-2';
+ div.style.cursor = 'pointer';
+ div.style.fontSize = '24px';
+ div.style.backgroundColor = 'rgba(255,255,255,0.05)';
+ div.textContent = emoji;
+ div.onclick = () => {
+ navigator.clipboard.writeText(emoji);
+ const originalBg = div.style.backgroundColor;
+ div.style.backgroundColor = 'var(--blurple)';
+ setTimeout(() => div.style.backgroundColor = originalBg, 200);
+ };
+ grid.appendChild(div);
+ });
+ }
+ };
+
+ sidebar.innerHTML = '';
+ categories.forEach((cat, idx) => {
+ const btn = document.createElement('button');
+ btn.className = `btn w-100 text-start text-white border-0 py-2 px-3 mb-1 d-flex align-items-center gap-2 ${idx === 0 ? 'active' : ''}`;
+ btn.style.backgroundColor = idx === 0 ? 'rgba(255,255,255,0.1)' : 'transparent';
+ btn.style.fontSize = '0.9em';
+ btn.innerHTML = `${categoryIcons[cat] || '❓'} ${cat}`;
+ btn.onclick = () => {
+ sidebar.querySelectorAll('button').forEach(b => {
+ b.classList.remove('active');
+ b.style.backgroundColor = 'transparent';
+ });
+ btn.classList.add('active');
+ btn.style.backgroundColor = 'rgba(255,255,255,0.1)';
+ if (searchInput) searchInput.value = '';
+ renderGrid(cat);
+ };
+ sidebar.appendChild(btn);
+ });
+
+ if (uploadInput) {
+ uploadInput.onchange = async () => {
+ const file = uploadInput.files[0];
+ if (!file) return;
+ const name = prompt("Nom de l'emote:", file.name.split('.')[0]);
+ if (!name) return;
+ const fd = new FormData();
+ fd.append('emote', file);
+ fd.append('name', name);
+ const res = await (await fetch('api/emotes.php?action=upload', { method: 'POST', body: fd })).json();
+ if (res.success) {
+ renderGrid('Custom');
+ window.loadCustomEmotes();
+ } else alert(res.error);
+ uploadInput.value = '';
+ };
+ }
+
+ if (searchInput) {
+ searchInput.oninput = () => {
+ const term = searchInput.value.trim();
+ if (term) {
+ sidebar.querySelectorAll('button').forEach(b => {
+ b.classList.remove('active');
+ b.style.backgroundColor = 'transparent';
+ });
+ renderGrid(null, term);
+ } else {
+ const activeBtn = sidebar.querySelector('button.active');
+ renderGrid(activeBtn ? activeBtn.querySelector('span:last-child').textContent : 'Custom');
+ }
+ };
+ }
+
+ renderGrid('Custom');
+ }
+
+ // Call when tab is shown
+ const emotesTabBtn = document.getElementById('emotes-tab-btn');
+ if (emotesTabBtn) {
+ emotesTabBtn.addEventListener('shown.bs.tab', setupSettingsEmotes);
+ }
+
/**
* Centralized Emoji Picker Component
*/
const UniversalEmojiPicker = {
currentPicker: null,
- show(anchor, callback, options = {}) {
+ async show(anchor, callback, options = {}) {
this.hide();
const picker = document.createElement('div');
picker.className = 'emoji-picker p-0 overflow-hidden d-flex flex-column';
- picker.style.width = options.width || '450px';
+ picker.style.width = options.width || '350px';
picker.style.height = options.height || '450px';
picker.style.backgroundColor = '#313338';
picker.style.border = '1px solid #1e1f22';
@@ -89,77 +280,113 @@ document.addEventListener('DOMContentLoaded', () => {
grid.style.gap = '5px';
grid.style.backgroundColor = '#313338';
- const renderGrid = (cat = null, term = '') => {
+ const renderGrid = async (cat = null, term = '') => {
grid.innerHTML = '';
- let list = term ? ALL_EMOJIS.filter(e => e.includes(term)) : EMOJI_CATEGORIES[cat];
- (list || []).forEach(emoji => {
- const span = document.createElement('span');
- span.textContent = emoji;
- span.className = 'emoji-item rounded d-flex align-items-center justify-content-center';
- span.style.cursor = 'pointer';
- span.style.fontSize = '24px';
- span.style.padding = '8px';
- span.style.aspectRatio = '1/1';
+ if (term) {
+ const customEmotes = window.CUSTOM_EMOTES_CACHE || [];
+ const filteredCustom = customEmotes.filter(e => e.name.toLowerCase().includes(term.toLowerCase()) || e.code.toLowerCase().includes(term.toLowerCase()));
- span.onclick = (e) => {
+ filteredCustom.forEach(emote => {
+ const div = document.createElement('div');
+ div.className = 'role-emoji-item rounded d-flex align-items-center justify-content-center p-2';
+ div.style.cursor = 'pointer';
+ div.innerHTML = `
`;
+ div.title = emote.code;
+ div.onclick = (e) => {
+ e.stopPropagation();
+ callback(emote.code);
+ if (!options.keepOpen) this.hide();
+ };
+ grid.appendChild(div);
+ });
+
+ const filteredStandard = ALL_EMOJIS.filter(e => e.includes(term));
+ filteredStandard.forEach(emoji => {
+ const div = document.createElement('div');
+ div.className = 'role-emoji-item rounded d-flex align-items-center justify-content-center p-2';
+ div.style.cursor = 'pointer';
+ div.style.fontSize = '24px';
+ div.textContent = emoji;
+ div.onclick = (e) => {
+ e.stopPropagation();
+ callback(emoji);
+ if (!options.keepOpen) this.hide();
+ };
+ grid.appendChild(div);
+ });
+ return;
+ }
+
+ if (cat === 'Custom') {
+ const customEmotes = (window.CUSTOM_EMOTES_CACHE && window.CUSTOM_EMOTES_CACHE.length > 0) ? window.CUSTOM_EMOTES_CACHE : await window.loadCustomEmotes();
+ if (customEmotes.length === 0) {
+ grid.innerHTML = 'Aucune emote personnalisée.
';
+ return;
+ }
+ customEmotes.forEach(emote => {
+ const div = document.createElement('div');
+ div.className = 'role-emoji-item rounded d-flex align-items-center justify-content-center p-2';
+ div.style.cursor = 'pointer';
+ div.innerHTML = `
`;
+ div.title = emote.code;
+ div.onclick = (e) => {
+ e.stopPropagation();
+ callback(emote.code);
+ if (!options.keepOpen) this.hide();
+ };
+ grid.appendChild(div);
+ });
+ return;
+ }
+
+ let list = EMOJI_CATEGORIES[cat];
+ (list || []).forEach(emoji => {
+ const div = document.createElement('div');
+ div.className = 'role-emoji-item rounded d-flex align-items-center justify-content-center p-2';
+ div.style.cursor = 'pointer';
+ div.style.fontSize = '24px';
+ div.textContent = emoji;
+ div.onclick = (e) => {
e.stopPropagation();
callback(emoji);
if (!options.keepOpen) this.hide();
};
- grid.appendChild(span);
+ grid.appendChild(div);
});
};
- // Init Tabs
- const categoryIcons = {
- 'Smileys': '😀',
- 'Gestures': '👌',
- 'People': '👶',
- 'Animals': '🐶',
- 'Nature': '🌵',
- 'Food': '🍏',
- 'Activities': '⚽️',
- 'Travel': '🚗',
- 'Objects': '⌚️',
- 'Symbols': '❤️',
- 'Flags': '🏁'
- };
-
- Object.keys(EMOJI_CATEGORIES).forEach((cat, idx) => {
+ const cats = ['Custom', ...Object.keys(EMOJI_CATEGORIES)];
+ cats.forEach((cat, idx) => {
const btn = document.createElement('button');
- btn.className = `btn btn-sm text-nowrap px-2 py-2 border-0 ${idx === 0 ? 'btn-primary' : 'btn-dark'}`;
- btn.style.fontSize = '1.2em';
+ btn.className = `btn btn-sm text-white border-0 p-2 ${idx === 0 ? 'active' : ''}`;
+ btn.style.backgroundColor = idx === 0 ? 'rgba(255,255,255,0.1)' : 'transparent';
+ btn.innerHTML = categoryIcons[cat] || '❓';
btn.title = cat;
- btn.textContent = categoryIcons[cat] || '❓';
- btn.onclick = (e) => {
- e.stopPropagation();
- searchInput.value = '';
+ btn.onclick = async () => {
tabs.querySelectorAll('button').forEach(b => {
- b.classList.remove('btn-primary');
- b.classList.add('btn-dark');
+ b.classList.remove('active');
+ b.style.backgroundColor = 'transparent';
});
- btn.classList.add('btn-primary');
- btn.classList.remove('btn-dark');
- renderGrid(cat);
- };
- tabs.appendChild(btn);
- });
- btn.classList.add('btn-primary');
- btn.classList.remove('btn-dark');
- renderGrid(cat);
+ btn.classList.add('active');
+ btn.style.backgroundColor = 'rgba(255,255,255,0.1)';
+ await renderGrid(cat);
};
tabs.appendChild(btn);
});
- searchInput.oninput = () => {
+ searchInput.oninput = async () => {
const term = searchInput.value.trim();
if (term) {
- tabs.querySelectorAll('button').forEach(b => b.classList.replace('btn-primary', 'btn-dark'));
- renderGrid(null, term);
+ tabs.querySelectorAll('button').forEach(b => {
+ b.classList.remove('active');
+ b.style.backgroundColor = 'transparent';
+ });
+ await renderGrid(null, term);
} else {
- const activeCat = tabs.querySelector('button.btn-primary')?.textContent || Object.keys(EMOJI_CATEGORIES)[0];
- renderGrid(activeCat);
+ const activeBtn = tabs.querySelector('button.active');
+ const activeCat = activeBtn ? activeBtn.title : 'Custom';
+ await renderGrid(activeCat);
}
};
@@ -180,7 +407,7 @@ document.addEventListener('DOMContentLoaded', () => {
picker.style.top = `${top}px`;
picker.style.left = `${left}px`;
- renderGrid(Object.keys(EMOJI_CATEGORIES)[0]);
+ await renderGrid('Custom');
// Handle outside click
const outsideClick = (e) => {
@@ -203,45 +430,83 @@ document.addEventListener('DOMContentLoaded', () => {
// Replace old showEmojiPicker and role grid logic
window.showEmojiPicker = (anchor, callback) => UniversalEmojiPicker.show(anchor, callback, { width: '350px', height: '400px' });
- // Update role editor emoji triggers
- function setupRoleEmojiTriggers() {
- const triggers = [
- { btn: 'role-emoji-select-btn', target: 'edit-role-icon', preview: 'selected-role-emoji-preview' },
- { btn: 'add-autorole-emoji-btn', target: 'add-autorole-icon', preview: 'add-autorole-emoji-preview' },
- { btn: 'edit-autorole-emoji-btn', target: 'edit-autorole-icon', preview: 'edit-autorole-emoji-preview' }
- ];
-
- triggers.forEach(t => {
- const btn = document.getElementById(t.btn);
- if (btn) {
- btn.onclick = (e) => {
- e.preventDefault();
- UniversalEmojiPicker.show(btn, (emoji) => {
- const input = document.getElementById(t.target);
- const preview = document.getElementById(t.preview);
- if (input) input.value = emoji;
- if (preview) preview.textContent = emoji;
- }, { width: '350px', height: '400px' });
- };
+ window.renderEmojiToElement = (code, el) => {
+ if (!el) return;
+ if (!code) {
+ el.innerHTML = "";
+ return;
+ }
+ if (typeof code === "string" && code.startsWith(':') && code.endsWith(':')) {
+ const ce = window.CUSTOM_EMOTES_CACHE.find(e => e.code === code);
+ if (ce) {
+ el.innerHTML = `
`;
+ return;
}
- });
+ }
+ el.textContent = code;
+ };
+
+ // Unified Emoji Picker & Modal Logic
+ document.addEventListener("click", (e) => {
+ // Emoji Picker Triggers
+ const triggers = {
+ "role-emoji-select-btn": { target: "edit-role-icon", preview: "selected-role-emoji-preview" },
+ "add-autorole-emoji-btn": { target: "add-autorole-icon", preview: "add-autorole-emoji-preview" },
+ "edit-autorole-emoji-btn": { target: "edit-autorole-icon", preview: "edit-autorole-emoji-preview" }
+ };
+
+ const btn = e.target.closest("button[id]");
+ if (btn && triggers[btn.id]) {
+ e.preventDefault();
+ const config = triggers[btn.id];
+ UniversalEmojiPicker.show(btn, (emoji) => {
+ const input = document.getElementById(config.target);
+ const preview = document.getElementById(config.preview);
+ if (input) input.value = emoji;
+ window.renderEmojiToElement(emoji, preview);
+ }, { width: "350px", height: "400px" });
+ return;
+ }
// Chat Emoji Picker
- const chatEmojiBtn = document.getElementById('chat-emoji-btn');
- const chatInput = document.getElementById('chat-input');
- if (chatEmojiBtn && chatInput) {
- chatEmojiBtn.onclick = (e) => {
- e.preventDefault();
- UniversalEmojiPicker.show(chatEmojiBtn, (emoji) => {
+ const chatEmojiBtn = e.target.closest("#chat-emoji-btn");
+ if (chatEmojiBtn) {
+ e.preventDefault();
+ UniversalEmojiPicker.show(chatEmojiBtn, (emoji) => {
+ const chatInput = document.getElementById("chat-input");
+ if (chatInput) {
chatInput.value += emoji;
chatInput.focus();
- }, { keepOpen: true, width: '350px', height: '400px' });
- };
+ }
+ }, { keepOpen: true, width: "350px", height: "400px" });
+ return;
}
- }
- // Call setup
- setupRoleEmojiTriggers();
+ // Autorole Edit modal filling
+ const editAutoroleBtn = e.target.closest(".edit-autorole-btn");
+ if (editAutoroleBtn) {
+ const id = editAutoroleBtn.dataset.id;
+ const icon = editAutoroleBtn.dataset.icon;
+ const title = editAutoroleBtn.dataset.title;
+ const roleId = editAutoroleBtn.dataset.roleId;
+
+ const idInput = document.getElementById("edit-autorole-id");
+ const iconInput = document.getElementById("edit-autorole-icon");
+ const titleInput = document.getElementById("edit-autorole-title");
+ const roleIdInput = document.getElementById("edit-autorole-role-id");
+ const preview = document.getElementById("edit-autorole-emoji-preview");
+
+ if (idInput) idInput.value = id;
+ if (iconInput) iconInput.value = icon;
+ if (titleInput) titleInput.value = title;
+ if (roleIdInput) roleIdInput.value = roleId;
+
+ if (preview) window.renderEmojiToElement(icon, preview);
+ return;
+ }
+ });
+
+ window.loadCustomEmotes();
// Scroll to bottom
scrollToBottom(true);
@@ -2158,6 +2423,15 @@ document.addEventListener('DOMContentLoaded', () => {
return div.innerHTML;
}
+ function parseCustomEmotes(text) {
+ let parsed = escapeHTML(text);
+ (window.CUSTOM_EMOTES_CACHE || []).forEach(emote => {
+ const imgHtml = `
`;
+ parsed = parsed.split(emote.code).join(imgHtml);
+ });
+ return parsed;
+ }
+
function appendMessage(msg) {
if (!msg || !msg.id) return;
if (document.querySelector(`.message-item[data-id="${msg.id}"]`)) return;
@@ -2238,53 +2512,24 @@ document.addEventListener('DOMContentLoaded', () => {
` : '';
const mentionRegex = new RegExp(`@${window.currentUsername}\\b`, 'g');
- if (msg.content.match(mentionRegex)) {
- div.classList.add('mentioned');
- }
- if (msg.is_pinned) div.classList.add('pinned');
-
- const ytRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
- const dmRegex = /(?:https?:\/\/)?(?:www\.)?(?:dailymotion\.com\/video\/|dai\.ly\/)([a-zA-Z0-9]+)/;
- const vimeoRegex = /(?:https?:\/\/)?(?:www\.)?vimeo\.com\/(\d+)/;
-
- const ytMatch = msg.content.match(ytRegex);
- const dmMatch = msg.content.match(dmRegex);
- const vimeoMatch = msg.content.match(vimeoRegex);
-
- let videoHtml = '';
- if (ytMatch && ytMatch[1]) {
- videoHtml = ``;
- } else if (dmMatch && dmMatch[1]) {
- videoHtml = ``;
- } else if (vimeoMatch && vimeoMatch[1]) {
- videoHtml = ``;
- }
-
- const authorStyle = msg.role_color ? `color: ${msg.role_color};` : '';
-
- const isRoleIconUrl = msg.role_icon && (msg.role_icon.startsWith('http') || msg.role_icon.startsWith('/'));
- const roleIcon = msg.role_icon ? (isRoleIconUrl ? `
` : `${msg.role_icon}`) : '';
+ const mentionHtml = `@${window.currentUsername}`;
+ const contentWithMentions = parseCustomEmotes(msg.content).replace(mentionRegex, mentionHtml);
div.innerHTML = `
-
- ${escapeHTML(msg.content).replace(/\n/g, '
').replace(mentionRegex, `@${window.currentUsername}`)}
- ${attachmentHtml}
- ${videoHtml}
- ${embedHtml}
-
-
- +
-
+
${contentWithMentions.replace(/\n/g, '
')}
+ ${attachmentHtml}
+ ${embedHtml}
+
${actionsHtml}
+
`;
messagesList.appendChild(div);
scrollToBottom(isMe);
@@ -2352,45 +2597,5 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
- // Edit Autorole Modal Population
- document.addEventListener('click', (e) => {
- const btn = e.target.closest('.edit-autorole-btn');
- if (!btn) return;
- const id = btn.dataset.id;
- const icon = btn.dataset.icon;
- const title = btn.dataset.title;
- const roleId = btn.dataset.roleId;
-
- document.getElementById('edit-autorole-id').value = id;
- document.getElementById('edit-autorole-icon').value = icon;
- document.getElementById('edit-autorole-title').value = title;
- document.getElementById('edit-autorole-role-id').value = roleId;
-
- // Update preview
- const preview = document.getElementById('edit-autorole-emoji-preview');
- if (preview) preview.textContent = icon;
- });
-
- // Universal Emoji Picker Trigger
- document.addEventListener('click', (e) => {
- const btn = e.target.closest('.open-emoji-picker');
- if (!btn) return;
-
- const targetId = btn.dataset.target;
- const targetInput = document.querySelector(targetId);
-
- if (typeof showEmojiPicker === 'function') {
- showEmojiPicker(btn, (emoji) => {
- if (targetInput) {
- targetInput.value = emoji;
- // Special case for role settings preview
- if (targetId === '#edit-role-icon') {
- const preview = document.getElementById('selected-role-emoji-preview');
- if (preview) preview.textContent = emoji;
- }
- }
- });
- }
- });
});
diff --git a/assets/pasted-20260216-132213-80b79cbe.png b/assets/pasted-20260216-132213-80b79cbe.png
new file mode 100644
index 0000000..b8b920e
Binary files /dev/null and b/assets/pasted-20260216-132213-80b79cbe.png differ
diff --git a/assets/pasted-20260216-162915-c3590120.png b/assets/pasted-20260216-162915-c3590120.png
new file mode 100644
index 0000000..1c8e4f1
Binary files /dev/null and b/assets/pasted-20260216-162915-c3590120.png differ
diff --git a/db/migrations/001_create_custom_emotes.sql b/db/migrations/001_create_custom_emotes.sql
new file mode 100644
index 0000000..42240f4
--- /dev/null
+++ b/db/migrations/001_create_custom_emotes.sql
@@ -0,0 +1,7 @@
+CREATE TABLE IF NOT EXISTS custom_emotes (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(50) NOT NULL,
+ path VARCHAR(255) NOT NULL,
+ code VARCHAR(60) NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
diff --git a/index.php b/index.php
index e19e8a1..3f378cd 100644
--- a/index.php
+++ b/index.php
@@ -7,15 +7,45 @@ function renderRoleIcon($icon, $size = '12px') {
if (empty($icon)) return '';
$isUrl = (strpos($icon, 'http') === 0 || strpos($icon, '/') === 0);
$isFa = (strpos($icon, 'fa-') === 0);
+ $isCustomEmote = (strpos($icon, ':') === 0 && substr($icon, -1) === ':');
if ($isUrl) {
return '
';
} elseif ($isFa) {
return '';
+ } elseif ($isCustomEmote) {
+ // Fetch emote path
+ static $ce_icons_cache;
+ if ($ce_icons_cache === null) {
+ try { $ce_icons_cache = db()->query("SELECT code, path FROM custom_emotes")->fetchAll(PDO::FETCH_KEY_PAIR); } catch (Exception $e) { $ce_icons_cache = []; }
+ }
+ if (isset($ce_icons_cache[$icon])) {
+ return '
';
+ }
+ return '' . htmlspecialchars($icon) . '';
} else {
return '' . htmlspecialchars($icon) . '';
}
}
+
+// Helper to parse emotes in content
+function parse_emotes($content) {
+ static $custom_emotes_cache;
+ if ($custom_emotes_cache === null) {
+ try {
+ $custom_emotes_cache = db()->query("SELECT name, path, code FROM custom_emotes")->fetchAll();
+ } catch (Exception $e) {
+ $custom_emotes_cache = [];
+ }
+ }
+
+ $result = htmlspecialchars($content);
+ foreach ($custom_emotes_cache as $ce) {
+ $emote_html = '
';
+ $result = str_replace($ce['code'], $emote_html, $result);
+ }
+ return $result;
+}
requireLogin();
$user = getCurrentUser();
@@ -672,7 +702,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
data-role-id=""
data-id=""
style="">
-
+
@@ -801,6 +831,17 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
@' . htmlspecialchars($user['username']) . '', $msg_content);
+
+ // Custom Emotes parsing
+ static $custom_emotes_cache;
+ if ($custom_emotes_cache === null) {
+ $custom_emotes_cache = db()->query("SELECT name, path, code FROM custom_emotes")->fetchAll();
+ }
+ foreach ($custom_emotes_cache as $ce) {
+ $emote_html = '
![' . htmlspecialchars($ce['code']) . ' ' . htmlspecialchars($ce['name']) . '](' . htmlspecialchars($ce['path']) . ')
';
+ $msg_content = str_replace($ce['code'], $emote_html, $msg_content);
+ }
+
echo nl2br($msg_content);
?>
@@ -851,7 +892,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
$reacted = in_array($current_user_id, explode(',', $r['users']));
?>
-
+
+
@@ -1024,6 +1065,9 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
+
+
+
@@ -1138,6 +1182,27 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
+
diff --git a/requests.log b/requests.log
index 00bc576..1cd2039 100644
--- a/requests.log
+++ b/requests.log
@@ -209,3 +209,28 @@
2026-02-16 13:06:41 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-16 13:06:43 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-16 13:06:45 - GET /index.php?server_id=1&channel_id=17 - POST: []
+2026-02-16 13:11:31 - GET /?fl_project=38443 - POST: []
+2026-02-16 13:13:37 - GET /index.php?server_id=1&channel_id=17 - POST: []
+2026-02-16 13:18:51 - GET /?fl_project=38443 - POST: []
+2026-02-16 13:19:22 - GET /index.php?server_id=1&channel_id=17 - POST: []
+2026-02-16 13:25:42 - GET /?fl_project=38443 - POST: []
+2026-02-16 13:38:52 - GET /index.php?server_id=1&channel_id=17 - POST: []
+2026-02-16 13:44:04 - GET /?fl_project=38443 - POST: []
+2026-02-16 14:10:45 - GET /index.php?server_id=1&channel_id=17 - POST: []
+2026-02-16 14:10:52 - GET /index.php - POST: []
+2026-02-16 14:10:57 - GET /index.php?server_id=1&channel_id=17 - POST: []
+2026-02-16 14:11:06 - GET /index.php?server_id=1&channel_id=17 - POST: []
+2026-02-16 14:13:29 - GET /?fl_project=38443 - POST: []
+2026-02-16 16:20:04 - GET /index.php?server_id=1&channel_id=17 - POST: []
+2026-02-16 16:20:11 - GET /index.php - POST: []
+2026-02-16 16:20:31 - GET /index.php?server_id=1&channel_id=17 - POST: []
+2026-02-16 16:20:47 - GET /index.php?server_id=1&channel_id=17 - POST: []
+2026-02-16 16:23:59 - GET / - POST: []
+2026-02-16 16:24:07 - GET /?fl_project=38443 - POST: []
+2026-02-16 16:24:48 - GET /index.php?server_id=1&channel_id=17 - POST: []
+2026-02-16 16:24:51 - GET /index.php?server_id=1&channel_id=17 - POST: []
+2026-02-16 16:25:36 - GET /index.php?server_id=1&channel_id=17 - POST: []
+2026-02-16 16:32:41 - GET / - POST: []
+2026-02-16 16:32:49 - GET /?fl_project=38443 - POST: []
+2026-02-16 16:32:51 - GET /index.php?server_id=1&channel_id=17 - POST: []
+2026-02-16 16:33:28 - GET /index.php?server_id=1&channel_id=17 - POST: []