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 = `${emote.name}`; + 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.username)} - ${roleIcon} - ${msg.time} + ${escapeHTML(msg.username)} + ${msg.timestamp || 'Just now'} ${pinnedBadge}
-
- ${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 = '' . htmlspecialchars($ce['name']) . ''; + $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['name']) . ''; + $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: []