NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Ultimate Emojis
// @version 1.1.1
// @description Discord-style emoji/sticker/gif picker with favorites, pagination, search, and a customizable Home screen.
// @author ZukoXZoku
// @icon https://i.ibb.co/xt5LYnZJ/91xfz9.gif
// @updateURL https://openuserjs.org/meta/ZukoXZoku/Ultimate_Emojis.meta.js
// @downloadURL https://openuserjs.org/install/ZukoXZoku/Ultimate_Emojis.user.js
// @connect openuserjs.org
// @license MIT
// @copyright 2025, ZukoXZoku (https://openuserjs.org/users/ZukoXZoku)
// @match https://aither.cc/*
// @match https://blutopia.cc/*
// @match https://fearnopeer.com/*
// @match https://reelflix.cc/*
// @match https://upload.cx/*
// @match https://oldtoons.world/*
// @match https://darkpeers.org/*
// @match https://luminarr.me/*
// @match https://lst.gg/*
// @match https://retro-movies.club/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @connect raw.githubusercontent.com
// @connect cdn.jsdelivr.net
// @connect tenor.googleapis.com
// @connect api.giphy.com
// @connect api.imgur.com
// @connect api.tumblr.com
// @connect reddit.com
// @connect www.reddit.com
// @grant GM_setClipboard
// ==/UserScript==
// ═══════════════════════════════════════════════
// Updates:
// 1. Added IRC Support
// 2. Fixed lst.gg chat
// 3. Updated the Appearance
// 4. Fixed many issues, now it's faster and improved the speed at anything, even lazy load.
// NOTE: Now you can use it at your self-host IRC Channel.
// ═══════════════════════════════════════════════
(function () {
'use strict';
// ═══════════════════════════════════════════════
// POLYFILLS
// ═══════════════════════════════════════════════
if (typeof window.GM_addStyle !== 'function') {
window.GM_addStyle = css => {
const s = document.createElement('style');
s.textContent = css;
document.head.appendChild(s);
return s;
};
}
const GM_Get = (k, d) => { try { return GM_getValue(k, d); } catch { return d; } };
const GM_Set = (k, v) => { try { GM_setValue(k, v); } catch {} };
// Font Awesome
if (!document.querySelector('link[href*="font-awesome"],link[href*="fontawesome"]')) {
const fa = document.createElement('link');
fa.rel = 'stylesheet';
fa.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css';
fa.crossOrigin = 'anonymous';
document.head.appendChild(fa);
}
// ═══════════════════════════════════════════════
// CONSTANTS
// ═══════════════════════════════════════════════
const VERSION = '1.1.1';
const LINKS = {
github: 'https://github.com/ZukoXZoku/Ultimate-Emojis',
openuserjs: 'https://openuserjs.org/scripts/ZukoXZoku/Ultimate_Emojis',
download: 'https://openuserjs.org/install/ZukoXZoku/Ultimate_Emojis.user.js',
meta: 'https://openuserjs.org/meta/ZukoXZoku/Ultimate_Emojis.meta.js'
};
const EMOJI_COUNT = 29;
const EMOJI_ORIGINS = [
'https://raw.githubusercontent.com/ZukoXZoku/Ultimate-Emojis/main/emojis/emj',
'https://cdn.jsdelivr.net/gh/ZukoXZoku/Ultimate-Emojis@main/emojis/emj'
];
const EMOJI_URL_GROUPS = Array.from({ length: EMOJI_COUNT }, (_, i) => {
const n = i + 1;
return EMOJI_ORIGINS.map(base => `${base}${n}.json`);
});
const STICKERS_JSON_URLS = [
'https://raw.githubusercontent.com/ZukoXZoku/Ultimate-Emojis/refs/heads/main/stickers/stkrs.json'
];
// ═══════════════════════════════════════════════
// EMOJI SUBCATEGORIES DATA
// ═══════════════════════════════════════════════
const EMOJI_CATS = {
custom: { label: 'Custom', icon: 'fa-star', desc: 'Your custom uploaded emojis' },
emojipedia:{ label: 'Emojipedia',icon: 'fa-face-smile', desc: 'Standard Unicode emojis' },
symbols: { label: 'Symbols', icon: 'fa-icons', desc: 'Symbols, math, arrows' },
flags: { label: 'Flags', icon: 'fa-flag', desc: 'Country & region flags' },
kaomoji: { label: 'Kaomoji', icon: 'fa-comment-dots', desc: 'Japanese text emoticons' },
ascii: { label: 'ASCII', icon: 'fa-terminal', desc: 'ASCII emoticons' }
};
const EMOJIPEDIA = {
'Smileys': '😀😃😄😁😆😅🤣😂🙂🙃😉😊😇🥰😍🤩😘😗😚😙🥲😋😛😜🤪😝🤑🤗🤭🫢🫣🤫🤔🫡🤐🤨😐😑😶🫥😏😒🙄😬🤥😌😔😪🤤😴😷🤒🤕🤢🤮🥵🥶🥴😵😵💫🤯🤠🥳🥸😎🤓🧐😕🫤😟🙁☹️😮😯😲😳🥺🥹😦😧😨😰😥😢😭😱😖😣😞😓😩😫🥱😤😡😠🤬😈👿💀☠️💩🤡👹👺👻👽👾🤖'.match(/./gu),
'Gestures': '👋🤚🖐️✋🖖🫱🫲🫳🫴👌🤌🤏✌️🤞🫰🤟🤘🤙👈👉👆🖕👇☝️🫵👍👎✊👊🤛🤜👏🙌🫶👐🤲🤝🙏✍️💅🤳💪🦾🦿🦵🦶👂🦻👃🧠🫀🫁🦷🦴👀👁️👅👄🫦'.match(/./gu),
'People': '👶🧒👦👧🧑👱👨🧔👩🧓👴👵🙍🙎🙅🙆💁🙋🧏🙇🤦🤷👮🕵️💂🥷👷🫅🤴👸👳👲🧕🤵👰🤰🫃🫄🤱👼🎅🤶🦸🦹🧙🧚🧛🧜🧝🧞🧟💆💇🚶🧍🧎🏃💃🕺'.match(/./gu),
'Animals': '🐶🐱🐭🐹🐰🦊🐻🐼🐻❄️🐨🐯🦁🐮🐷🐸🐵🙈🙉🙊🐒🐔🐧🐦🐤🐣🐥🦆🦅🦉🦇🐺🐗🐴🦄🐝🪱🐛🦋🐌🐞🐜🪰🪲🪳🦟🦗🕷️🕸️🦂🐢🐍🦎🦖🦕🐙🦑🦐🦞🦀🐡🐠🐟🐬🐳🐋🦈🦭🐊🐅🐆🦓🦍🦧🐘🦛🦏🐪🐫🦒🦘🦬🐃🐂🐄🐎🐖🐏🐑🦙🐐🦌🐕🐩🦮🐈🐈⬛🪶🐓🦃🦤🦚🦜🦢🦩🕊️🐇🦝🦨🦡🦫🦦🦥🐁🐀🐿️🦔'.match(/./gu),
'Nature': '🌵🎄🌲🌳🌴🪵🌱🌿☘️🍀🎍🪴🎋🍃🍂🍁🪺🪹🍄🌾💐🌷🌹🥀🌺🌸🌼🌻🌞🌝🌛🌜🌚🌕🌖🌗🌘🌑🌒🌓🌔🌙🌎🌍🌏🪐💫⭐🌟✨⚡☄️💥🔥🌪️🌈☀️🌤️⛅🌥️☁️🌦️🌧️⛈️🌩️🌨️❄️☃️⛄🌬️💨💧💦🫧☔☂️🌊🌫️'.match(/./gu),
'Food': '🍏🍎🍐🍊🍋🍌🍉🍇🍓🫐🍈🍒🍑🥭🍍🥥🥝🍅🍆🥑🥦🥬🥒🌶️🫑🌽🥕🫒🧄🧅🥔🍠🫘🥐🍞🥖🥨🧀🥚🍳🧈🥞🧇🥓🥩🍗🍖🌭🍔🍟🍕🫓🥪🥙🧆🌮🌯🫔🥗🥘🫕🥫🍝🍜🍲🍛🍣🍱🥟🦪🍤🍙🍚🍘🍥🥠🥮🍢🍡🍧🍨🍦🥧🧁🍰🎂🍮🍭🍬🍫🍿🍩🍪🌰🥜🍯🥛🍼🫖☕🍵🧃🥤🧋🍶🍺🍻🥂🍷🥃🍸🍹🧉🍾🧊'.match(/./gu),
'Activities': '⚽🏀🏈⚾🥎🎾🏐🏉🥏🎱🪀🏓🏸🏒🏑🥍🏏🪃🥅⛳🪁🏹🎣🤿🥊🥋🎽🛹🛼🛷⛸️🥌🎿🏆🥇🥈🥉🏅🎖️🏵️🎗️🎪🤹🎭🩰🎨🎬🎤🎧🎼🎹🥁🪘🎷🎺🪗🎸🪕🎻🪈🎲♟️🎯🎳🎮🕹️🧩🪅🪩'.match(/./gu),
'Objects': '⌚📱💻⌨️🖥️🖨️🖱️💽💾💿📀📼📷📹🎥📽️📞☎️📟📠📺📻🎙️🎚️🎛️🧭⏱️⏲️⏰🕰️⌛⏳📡🔋🔌💡🔦🕯️🧯💸💵💴💶💷🪙💰💳💎⚖️🧰🔧🔨⛏️🔩⚙️🧱⛓️🧲🔫💣🧨🔪🗡️⚔️🛡️⚰️⚱️🏺🔮📿🧿💈⚗️🔭🔬🩹🩺💊💉🧬🦠🧫🧪🌡️🧹🧺🧻🚽🧼🧽🧴🛎️🔑🗝️🚪🪑🛋️🛏️🧸🖼️🛍️🛒🎁🎈🎀🎊🎉🏮✉️📩📨📧💌📥📤📦🏷️📪📫📬📭📮📯📜📃📄📑🧾📊📈📉📇📋📁📂📰📓📔📒📕📗📘📙📚📖🔖🔗📎📐📏📌📍✂️🖊️🖋️✒️🖌️🖍️📝✏️🔍🔎🔏🔐🔒🔓'.match(/./gu),
'Travel': '🚗🚕🚙🚌🚎🏎️🚓🚑🚒🚐🛻🚚🚛🚜🛴🚲🛵🏍️🛺🚨🚔🚍🚘🚖🛞🚡🚠🚟🚃🚋🚞🚝🚄🚅🚈🚂🚆🚇🚊🚉✈️🛫🛬🛩️💺🛰️🚀🛸🚁🛶⛵🚤🛥️🛳️⛴️🚢⚓⛽🚧🚦🚥🚏🗺️🗿🗽🗼🏰🏯🏟️🎡🎢🎠⛲⛱️🏖️🏝️🏜️🌋⛰️🏔️🗻🏕️⛺🛖🏠🏡🏘️🏚️🏗️🏭🏢🏬🏣🏤🏥🏦🏨🏪🏫🏩💒🏛️⛪🕌🕍🛕🕋⛩️🛤️🛣️🌅🌄🌠🎇🎆🌇🌆🏙️🌃🌌🌉🌁'.match(/./gu)
};
const SYMBOL_DATA = {
'Hearts': '❤️🧡💛💚💙💜🖤🤍🤎❤️🔥❤️🩹💔❣️💕💞💓💗💖💘💝💟🩷🩵🩶'.match(/./gu),
'Arrows': ['⬆️','↗️','➡️','↘️','⬇️','↙️','⬅️','↖️','↕️','↔️','↩️','↪️','⤴️','⤵️','🔃','🔄','🔙','🔚','🔛','🔜','🔝'],
'Geometric': '🔴🟠🟡🟢🔵🟣🟤⚫⚪🟥🟧🟨🟩🟦🟪🟫⬛⬜◼️◻️◾◽▪️▫️🔶🔷🔸🔹🔺🔻💠🔘🔳🔲'.match(/./gu),
'Math': ['➕','➖','✖️','➗','♾️','‼️','⁉️','❓','❔','❕','❗'],
'Misc': ['✅','☑️','✔️','❌','❎','✳️','✴️','❇️','©️','®️','™️','#️⃣','*️⃣','0️⃣','1️⃣','2️⃣','3️⃣','4️⃣','5️⃣','6️⃣','7️⃣','8️⃣','9️⃣','🔟','ℹ️']
};
const FLAG_DATA = '🏁🚩🎌🏴🏳️🏳️🌈🏳️⚧️🏴☠️🇦🇫🇦🇱🇩🇿🇦🇸🇦🇩🇦🇴🇦🇮🇦🇬🇦🇷🇦🇲🇦🇼🇦🇺🇦🇹🇦🇿🇧🇸🇧🇭🇧🇩🇧🇧🇧🇾🇧🇪🇧🇿🇧🇯🇧🇲🇧🇹🇧🇴🇧🇦🇧🇼🇧🇷🇧🇳🇧🇬🇧🇫🇧🇮🇰🇭🇨🇲🇨🇦🇨🇻🇨🇫🇹🇩🇨🇱🇨🇳🇨🇴🇰🇲🇨🇬🇨🇩🇨🇷🇭🇷🇨🇺🇨🇾🇨🇿🇩🇰🇩🇯🇩🇲🇩🇴🇪🇨🇪🇬🇸🇻🇬🇶🇪🇷🇪🇪🇪🇸🇿🇪🇹🇫🇯🇫🇮🇫🇷🇬🇦🇬🇲🇬🇪🇩🇪🇬🇭🇬🇷🇬🇩🇬🇹🇬🇳🇬🇼🇬🇾🇭🇹🇭🇳🇭🇺🇮🇸🇮🇳🇮🇩🇮🇷🇮🇶🇮🇪🇮🇱🇮🇹🇯🇲🇯🇵🇯🇴🇰🇿🇰🇪🇰🇮🇽🇰🇰🇼🇰🇬🇱🇦🇱🇻🇱🇧🇱🇸🇱🇷🇱🇾🇱🇮🇱🇹🇱🇺🇲🇬🇲🇼🇲🇾🇲🇻🇲🇱🇲🇹🇲🇭🇲🇷🇲🇺🇲🇽🇫🇲🇲🇩🇲🇨🇲🇳🇲🇪🇲🇦🇲🇿🇲🇲🇳🇦🇳🇷🇳🇵🇳🇱🇳🇿🇳🇮🇳🇪🇳🇬🇰🇵🇲🇰🇳🇴🇴🇲🇵🇰🇵🇼🇵🇸🇵🇦🇵🇬🇵🇾🇵🇪🇵🇭🇵🇱🇵🇹🇶🇦🇷🇴🇷🇺🇷🇼🇸🇦🇸🇳🇷🇸🇸🇨🇸🇱🇸🇬🇸🇰🇸🇮🇸🇧🇸🇴🇿🇦🇰🇷🇸🇸🇪🇸🇱🇰🇸🇩🇸🇷🇸🇪🇨🇭🇸🇾🇹🇼🇹🇯🇹🇿🇹🇭🇹🇱🇹🇬🇹🇴🇹🇹🇹🇳🇹🇷🇹🇲🇹🇻🇺🇬🇺🇦🇦🇪🇬🇧🇺🇸🇺🇾🇺🇿🇻🇺🇻🇪🇻🇳🇾🇪🇿🇲🇿🇼'.match(/./gu);
const KAOMOJI = {
'Happy': ['(◕‿◕)', '(。◕‿◕。)', '(◠‿◠)', '(✿◠‿◠)', '(◡‿◡)', 'ʕ•ᴥ•ʔ', '(ノ◕ヮ◕)ノ*:・゚✧', '(≧◡≦)', '(◕ᴗ◕✿)', '☆*:.。.o(≧▽≦)o.。.:*☆', '(◍•ᴗ•◍)', '(灬º‿º灬)♡', '( ´ ▽ ` )ノ', '(o˘◡˘o)', '(≧▽≦)', '(⌒‿⌒)', '(。・ω・。)', '(´ ▽ ` )', '( ´ ω ` )', '。゚(TヮT)゚。', '(ノ>ω<)ノ', '(。♥‿♥。)', 'ヾ(≧▽≦*)o', '(≧◡≦) ', '(˘︶˘).。.:*♡', '(ू•ᴗ•ू❁)', '(◠‿◠)', '(。・ω・。)ノ♡', '( ´ ∀ `)ノ', '(*≧ω≦*)', 'ヾ(≧▽≦*)o', '٩(◕‿◕。)۶', '(●\'◡\'●)', '(´• ω •`)', '(◠˘◠)', '(˵ ͡° ͜ʖ ͡°˵)', '( ´ ▽ ` )ノ', '\(≧▽≦)/', '(⁀ᗢ⁀)', '(≖ᴗ≖✿)', '(◠‿◠✿)', '(人´∀`)☆', '(ノ^_^)ノ', '(*^‿^*)', '( ´ ꒳ ` )', '( ˘ ³˘)♥', '(⌬̈⃝)', '( ˘ᴥ˘ )', '(˶‾᷄ ⁻̫ ‾᷅˵)', '(ᵔᴥᵔ)', '(◕‿◕)', '(≧◠‿◠≦)', '(✿ ◕‿◕)', '(◕‿◕✿)', '⊂(◉‿◉)つ', '(◉‿◉)', '(。◕‿◕。)', '(◠‿◠)', '(◕ᴗ◕✿)', '(。◕‿◕。)', '(★^O^★)', '(^_^)b', '(^▽^)', 'ヾ(^-^)ノ', '(´ ▽ ` )', '(●\'◡\'●)', '╰(´︶`)╯', '( ´ ▽ ` )b', '٩(◕‿◕。)۶', '(⌒‿⌒)', 'ヾ(●´∀`●)', '(●\'◡\'●)', '(*^▽^*)', '(≧◡≦)', '(✿╹◡╹)', '(✧ω✧)', '(*≧ω≦*)', '(◕‿◕)', '(*≧▽≦)', 'ヾ(≧▽≦*)o', '(≧◡≦)', '(✯ᴗ✯)', 'ヾ(◍°∇°◍)ノ゙', '(◠‿◠)', '(◕‿◕)', '(。◕‿◕。)', '(*^▽^*)', '(*・ω・)ノ', '( ̄▽ ̄)ノ', 'ヾ(≧▽≦*)o', '(*´ω`*)', '(´・ω・`)', '(◕‿◕)', 'ヾ(・ω・`)'],
'Sad': ['(╥_╥)', '(T_T)', '(;_;)', '(ノ_<。)', '(´;ω;`)', '(ಥ﹏ಥ)', '(っ˘̩╭╮˘̩)っ', '(。•́︿•̀。)', '(◞‸◟)', '(´•ω•̥`)', '(๑•́ ₃ •̀๑)', '(。•́︿•̀。)', '(╥﹏╥)', '(っ˘̩╭╮˘̩)っ', '(ノAノ)', '( ´_ゝ`)', '( ╥ω╥ )', '(╯_╰)', '(・ω・)つ⊂(・ω・)', '(҂◡_◡)', '(⋋▂⋋)', '(ꈍᴗꈍ)', '(≚ᄌ≚)ƶƵ', '( ︶︿︶)', '(¬_¬)', '( ⟩ ⟩ ⟩', '(ꐦ°᷄д°᷅)', '(ꐦ𝅒_𝅒) ', '(ꐦ╭╮𝅒_𝅒)', '( 𝙨𝙤𝙗𝙨 )', '( ≧Д≦)', '( 🔴_🔴 )', '( ´_ゝ`)', '( ←_← )', '( →_→ )', '( ⁽₍₍₍_⁾₎₎₎ )', '(◞‸◟)', '(ㄒoㄒ)', '(T⌓T)', '(჻ጒ౪჻ )', '(₍₍⁽⁽(ી₍₍⁽⁽(ી ซง₎₎⁾⁾₎₎⁾⁾', '(u‿ฺu)', '( ◡︵◡)', '( ´_ゝ`)', '(μ_μ)', '(꒪⌓꒪)', '(。•́︿•̀。)', '(╥﹏╥)', '(◞‸◟)', '(;﹏;)', '(︶︹︶)', '(っ˘̩╭╮˘̩)っ', '(´°̥̥̥̥̥̥̥̥ω°̥̥̥̥̥̥̥̥`)', '( ✋˘ 😪 ˘ )', '(ー_ー )!!', '( ✋˘ ˘ )', '( ˘⌣˘ )', '( ˘︹˘ )', '(;′⌒`)', '(´胱圩鄄', '(T⌓T)', '(≖_≖ )', '( ゚д゚)', '( bothered )', '(noise)', '(; ̄Д ̄)', '(・_・;)', '(・・;)', '( ̄~ ̄;)', '(@_@)', '(ToT)', '(T_T)', '(T_T)', '(T_T)', '(_ _)', '..●', '(°°)', '(°△°)', '(°ω°)', '(´・・`)', '(´・ω・`)', '(´・ω・`)', '(´・ω・`)', '(。-_-)', '(´°̥̥̥̥̥̥̥̥ω°̥̥̥̥̥̥̥̥`)', '(T▽T)', '(T_T)', '(;_;)', '(╥_╥)', '(╯°□°)╯︵ ┻━┻', '┻━┻ ︵ ヽ(°□° )ノ ︵ ┻━┻'],
'Angry': ['(╬ Ò﹏Ó)', '(ノಠ益ಠ)ノ', '(ノ`Д´)ノ', '(`Д´)', '(≖_≖ )', '(눈_눈)', 'ヽ(`Д´)ノ', '(。•ˇ‸ˇ•。)', '(>人<)', '(¬_¬)', '(沉思)', '(ꐦ°᷄д°᷅)', '(╬゚◥益◤゚)', '(ノಠ ∩ಠ)ノ', '(≧Д≦)', '(ノ°△°)ノ︵┻━┻', '┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻', '(╯°□°)╯︵ ┻━┻', '╭∩╮(︶︿︶)╭∩╮', '(ಠ_ಠ)┌∩┐', '(ノಠ益ಠ)ノ彡┻━┻', '(╬ Ò﹏Ó)', '(ꐦ°᷄д°᷅)', '( ≧Д≦)', '(ꐦ╭╮𝅒_𝅒)', '(O_O)', '(#><)', '(><)', '(>﹏<)', '( ` ω ´ )', '( ` ω ´ )', '( ` ω ´ )', '٩(╬ʘ益ʘ╬)۶', '(◞≼◉◞ ༽ ✧', '( ` ∇ ´ )', '(メ▼へ▼)ノ', '( ` Д ´)', 'o(>< )o', 'o(>< )o', 'ε=ε=ε=ε=ε=ε=┌(; ̄◇ ̄)┘', 'ε=ε=ε=ε=ε=ε=┌(; ̄◇ ̄)┘', '(ノಠ益ಠ)ノ彡┻━┻', '(╬ Ò﹏Ó)', '(ꐦ°᷄д°᷅)', 'ε=ε=ε=ε=ε=ε=┌(; ̄◇ ̄)┘', 'ε=ε=ε=┌( >_<)┘', 'ε=ε=ε=┌( >_<)┘', '٩(╬ʘ益ʘ╬)۶', '(҂` ロ ´)╬', '(҂` ロ ´)╬', '୧(๑ ಠ 益 ಠ)૭', '୧(๑ ಠ 益 ಠ)૭', 'ψ(`∇´)ψ', 'ψ(`∇´)ψ', '(ノ ` Д)ノ', '(ノ ` Д)ノ', '(; ̄Д ̄)', '(; ̄Д ̄)', '( ` Д´)', '( ` Д´)', '༼ つ ◕_◕ ༽つ', '༼ つ ◕_◕ ༽つ', '( ` ω ´ )', '( ` ω ´ )', '( ` ∇ ´ )', '( ` ∇ ´ )', '(メ▼へ▼)ノ', '(メ▼へ▼)ノ', '(ꈍ﹃ꈍ)', '(ꈍ﹃ꈍ)', '(ꐦ°᷄д°᷅)', '(ꐦ°᷄д°᷅)', '٩(╬ʘ益ʘ╬)۶', '٩(╬ʘ益ʘ╬)۶', 'o(>< )o', 'o(>< )o'],
'Love': ['(♡°▽°♡)', '(◕‿◕)♡', '(✿ ♥‿♥)', '♡(ŐωŐ人)', '(´,,•ω•,,)♡', '(◍•ᴗ•◍)❤', '(灬♥ω♥灬)', '(◕ᴗ◕✿)', '(≧◡≦)♡', '( ´ ∀ `)ノ~ ♡', '♡^▽^♡', '(ღ˘⌣˘ღ)', '( ˘ ³˘)♥', '( ⚆ _ ⚆ )', '(♡˙︶˙♡)', '(♥ω♥*)', '(❤ω❤)', '( ´ ▽ ` )ノ♥', '(ɔˆ ³(ˆ⌣ˆc)', '(◡‿◡✿)', '( ˘ᴥ˘ )❤', '(¢aldara)', '(´,,•ω•,,)', '♡(˃͈ દ ˂͈ ༶ )', '( ˘ ³˘)♥', '( ´ ∀ `)♥', '( ´ ▽ ` )♥', '(´,,•ω•,,)♡', '(´,,•ω•,,)♡', '(◍•ᴗ•◍)❤', '(◍•ᴗ•◍)❤', '( ´ ∀ `)♥', '(*^^*)', '(◕‿◕)♡', '( ´ ▽ ` )b', '(*^▽^*)', '(*^▽^*)', '(✿ ♥‿♥)', '(*´ω`*)', '(*´ω`*)', '(*´ω`*)', '( ´ ▽ ` )♥', '( ´ ▽ ` )♥', '(。♥‿♥。)', '(。♥‿♥。)', '(。♥‿♥。)', '( ´ ∀ `)♥', '( ´ ∀ `)♥', '( ´ ∀ `)♥', '⊂(◉‿◉)つ', '(づ ̄ ³ ̄)づ', '(づ ̄ ³ ̄)づ', '(っ◔◡◔)っ ♥', '(っ◔◡◔)っ ♥', '( ´ ▽ ` )b', '(*^▽^*)', '(◕‿◕)♡', '(*^^*)', '(*^^*)', '(◕‿◕)♡', '(*^^*)', '(*^^*)', '(◕‿◕)♡', '(◕‿◕)♡', '( ´ ∀ `)♥', '( ´ ∀ `)♥', '( ´ ▽ ` )b', '(*^▽^*)', '(◕‿◕)♡', '(*^^*)', '(✿ ◕‿◕)', '(✿ ◕‿◕)', '(✿ ◕‿◕)', '(✿ ◕‿◕)', '(✿ ◕‿◕)', '(✿ ◕‿◕)', '(✿ ◕‿◕)', '(✿ ◕‿◕)', '(✿ ◕‿◕)', '(✿ ◕‿◕)', '(✿ ◕‿◕)', '(✿ ◕‿◕)', '(✿ ◕‿◕)', '(✿ ◕‿◕)', '(✿ ◕‿◕)', '(✿ ◕‿◕)'],
'Actions': ['┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻', '(╯°□°)╯︵ ┻━┻', '┬─┬ノ( º _ ºノ)', '¯\\_(ツ)_/¯', '(ノ°∀°)ノ⌒・*:.。', 'ᕕ( ᐛ )ᕗ', '(•̀ᴗ•́)و', '(ง •̀_•́)ง', '(☞゚ヮ゚)☞', '☜(゚ヮ゚☜)', '(⌐■_■)', '(•_•) ( •_•)>⌐■-■ (⌐■_■)', 'ヽ(•‿•)ノ', '(¬‿¬)', '( ͡° ͜ʖ ͡°)', '╭∩╮(︶︿︶)╭∩╮', '(ಠ_ಠ)┌∩┐', '┬─┬ ノ( ゜-゜ノ)', '(╯°□°)╯︵ ┻━┻ ︵ ┻━┻', '┻━┻ ︵ ¯\\ (ツ)/ ︵ ┻━┻', '(ノಠ ∀ ಠ)ノ⌒・*:.。', '(ಠ_ಠ)ಠ_ಠ)', '(ಠ‿ಠ)ಠ_ಠ)', '( ͡ಠ ʖ̯ ͡ಠ)', '┬─┬ ︵ /(.□. \\)', '┻━┻ ︵ ヽ(°□° )ノ ︵ ┻━┻', '┻━┻ ︵ ¯\\( °□° )/¯ ︵ ┻━┻', '(ノಥ,_」ಥ)ノ彡┻━┻', '┬─┬ ︵( /(.□. \\))', '(╯°□°)╯︵ ┻━┻', '(╯°Д°)╯︵ /(.□ . \\)', '┬─┬ ノ( º _ ºノ )', '┬─┬ ノ( ゜-゜ノ)', '( º _ º )', '( º _ º )', '( º _ º )', '╭∩╮(Ο_Ο)╭∩╮', '╭∩╮(Ο_Ο)╭∩╮', '╭∩╮(Ο_Ο)╭∩╮', '(☞゚ヮ゚)☞', '(☞゚ヮ゚)☞', '☜(゚ヮ゚☜)', '☜(゚ヮ゚☜)', '☜(゚ヮ゚☜)', 'ʕ •ᴥ• ʔ', 'ʕ •ᴥ• ʔ', 'ʕ •ᴥ• ʔ', '(づ ̄ ³ ̄)づ', '(づ ̄ ³ ̄)づ', '(つ≧▽≦)つ', '(つ≧▽≦)つ', '(つ≧▽≦)つ', '(つ≧▽≦)つ', '(ノ◕ヮ◕)ノ*:・゚✧', '(ノ◕ヮ◕)ノ*:・゚✧', '(*・ω・)ノ', '(*・ω・)ノ', '(ノ°∀°)ノ', '(ノ°∀°)ノ', '(ノ°∀°)ノ', '(ノ°∀°)ノ', 'ヾ(・ω・`)', 'ヾ(・ω・`)', '( ´ ▽ ` )ノ', '( ´ ▽ ` )ノ', '( ´ ▽ ` )ノ', '( ´ ▽ ` )ノ', '( ´ ▽ ` )b', '( ´ ▽ ` )b', 'ε=ε=ε=ε=ε=ε=┌(; ̄◇ ̄)┘', 'ε=ε=ε=ε=ε=ε=┌(; ̄◇ ̄)┘', 'ε=ε=ε=ε=ε=ε=┌(; ̄◇ ̄)┘', 'ε=ε=ε=ε=ε=ε=┌(; ̄◇ ̄)┘'],
'Animals': ['ʕ•ᴥ•ʔ', '(=^・ω・^=)', '(=^-ω-^=)', 'ʕ ꈍᴥꈍʔ', '(◕ᴥ◕)', 'U•ᴥ•U', '(ΦωΦ)', '₍ᐢ..ᐢ₎', 'ฅ^•ﻌ•^ฅ', '(🐕)', '(ㅇㅅㅇ❀)', '( ˘ᴥ˘ )', '(◉θ◉)', '(=o_o=)', 'ᶘ ᵒᴥᵒᶅ', 'ʕ̯•͡ᴥ•͡ʔ', 'ʕ>Toᴥo<ʔ', '(ᵔᴥᵔ)', 'ʕ•̫͡•ʔ', 'ʕ•ᴥ•ʔ', 'ʕ •ᴥ• ʔ', 'ʕ•́•̀ʔ', 'ʕ◉ᴥ◉ʔ', 'ʕ⁰̈ʔ', 'ʕಠᴥಠʔ', 'ʕ•̠͡•ʔ', 'ʕ•͡ˑ͓•ʔ', 'ʕ•̫͡•ʔ', 'ʕ•͓͡•ʔ', 'ʕꈍᴥꈍʔ', 'ʕ≧ᴥ≦ʔ', 'ʕつ◕ᴥ◕ʔつ', 'ʕง•ᴥ•ʔง', 'ʕᵔᴥᵔʔ', '(=`ω´=)', 'ฅ(• ɪ •)ฅ', 'ฅ(• wget •)ฅ', 'ฅ(=๏ᆺ๏=)ฅ', '(=^-ω-^=)', '(=^ェ^=)', '(=^・・^=)', '(=^・ェ・^=)', 'ฅ(•ㅅ•❀)ฅ', 'ฅ(•ㅅ•)ฅ', 'ฅ(×ω×)ฅ', 'ฅ(˘ω˘ )ฅ', 'ฅ( ꒪◦꒪)ฅ', 'ฅ(ミ⚈ ﻌ ⚈ミ)ฅ', 'ฅ(๑*д*๑)ฅ', 'ฅ(๑˃ᴗ˂)ฅ', 'ฅ(๑˙o˙๑)ฅ', 'ฅ(ミ◕ ༝ ◕ミ)ฅ', 'ฅ(ミ◉ ਊ ◉ミ)ฅ', 'ฅ(ミ⚈ llvm ⚈ミ)ฅ', 'ฅ(=ʘᆽʘ=)ฅ', 'ฅ(>_<)ฅ', '(=゚・゚=)ソ', '(=;゚∀゚=)', '(=´∀`)=', 'U・x・U', 'U・ﻌ・U', 'U・ᴥ・U', 'U^ェ^U', 'V●ᴥ●V', '( ˘ᴥ˘ )', '( ˘ᴥ˘ )', '( ˘ᴥ˘ )', 'ʕ•ᴥ•ʔ', 'ʕ•ᴥ•ʔ', 'ʕ•ᴥ•ʔ', '(=^・ω・^=)', '(=^・ω・^=)', '(=^・ω・^=)', '(=^-ω-^=)', '(=^-ω-^=)', '(=^-ω-^=)', 'ʕ ꈍᴥꈍʔ', 'ʕ ꈍᴥꈍʔ', 'ʕ ꈍᴥꈍʔ'],
'Lenny': ['( ͡° ͜ʖ ͡°)', '( ͡°( ͡° ͜ʖ( ͡° ͜ʖ ͡°)ʖ ͡°) ͡°)', '( ͡° ʖ̯ ͡°)', '( ͡ಠ ʖ̯ ͡ಠ)', '( ͡⚆ ͜ʖ ͡⚆)', '( ͡~ ͜ʖ ͡°)', '( ͡ʘ╭͜ʖ╮͡ʘ)', '( ͡°╭͜ʖ╮͡°)', '( ͝° ͜ʖ͡°)', '( ͡° ͜ʖ ͡°)( ͡° ͜ʖ ͡°)', 'ᕦ( ͡° ͜ʖ ͡°)ᕤ', '( ͡°ᴥ ͡°)', '( ͡ಠ╭͜ʖ╮͡ಠ)', '( ͡° ͜ʖ ͡°)ノ⌐■-■', '( ͡° ͜ʖ ͡°) □ -□ ', '(つ ͡° ͜ʖ ͡°)つ', 'ʕ •ᴥ• ʔ', '( ͡° ͜ʖ ͡°)ノ', '( ͡° ͜ʖ ͡°)ノ', '( ͡° ͜ʖ ͡°)ノ', '( ͡° ͜ʖ ͡°)ノ', '( ͡° ͜ʖ ͡°)ノ', '( ͡° ͜ʖ ͡°)ノ', '( ͡° ͜ʖ ͡°)ノ', '( ͡° ͜ʖ ͡°)ノ', '( ͡° ͜ʖ ͡°)ノ', '( ͡° ͜ʖ ͡°)ノ', '( ͡° ͜ʖ ͡°)ノ', '( ͡° ͜ʖ ͡°)ノ', '( ͡° ͜ʖ ͡°)ノ', '( ͡° ͜ʖ ͡°)ノ', '( ͡° ͜ʖ ͡°)ノ'],
'Table': ['┬─┬ノ( º _ ºノ)', '┬─┬ ︵ /(.□. \\)', '┻━┻ ︵ ヽ(°□° )ノ ︵ ┻━┻', '┻━┻ ︵ ¯\\( °□° )/¯ ︵ ┻━┻', '(╯°□°)╯︵ ┻━┻ ', '(ノಠ益ಠ)ノ彡┻━┻', '(╯°□°)╯︵ ┻━┻ ︵ ┻━┻', '┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻', '┬─┬ノ(ಠ_ಠノ)', '┻━┻ ︵ ¯\\_༼ ᴼ_o ༽_/¯ ︵ ┻━┻', '(ノಥ,_」ಥ)ノ彡┻━┻', '┬─┬ ︵(/ .o.\\/)'],
'Surprised': ['(☉_☉)', '(º_º)', '(⊙_☉)', '(⊙_⊙)', '(✧ω✧)', '(✧_✧)', '(O_O)', '(o_O)', '(°o°)', '(°ロ°)', '(O.o)', '(o_O)', '(๏_๏)', '(⊚_⊚)', '(◔_◔)', '(◉_◉)', '(◔ ‸ ◔)', '( : ౦ ‸ ౦ : )', '(O_o)', '(o_O)', '(o_O)', '(o_O)', '(o_O)', '(o_O)', '(o_O)', '(o_O)', '(o_O)', '(o_O)', '(o_O)', '(o_O)', '(o_O)', '(o_O)'],
'Helpless': ['¯\\_(ツ)_/¯', '╮(╯_╰)╭', '¯\\_(⊙_⊙)_/¯', '¯\\_( ͡° ͜ʖ ͡°)_/¯', '¯\\_(☯ᴥ☯)_/¯', '¯\\_(⊡_⊡)_/¯', '╮(╯▽╰)╭', '┐(´д`)┌', '┐(´~`)┌', '┐(´ー`)┌', '╮(╯_╰)╭', '¯\\_(ツ)_/¯', '¯\\_(ツ)_/¯', '¯\\_(ツ)_/¯', '¯\\_(ツ)_/¯', '¯\\_(ツ)_/¯', '¯\\_(ツ)_/¯', '¯\\_(ツ)_/¯', '¯\\_(ツ)_/¯', '¯\\_(ツ)_/¯', '¯\\_(ツ)_/¯', '¯\\_(ツ)_/¯', '¯\\_(ツ)_/¯', '¯\\_(ツ)_/¯', '¯\\_(ツ)_/¯', '¯\\_(ツ)_/¯', '¯\\_(ツ)_/¯', '¯\\_(ツ)_/¯', '¯\\_(ツ)_/¯', '¯\\_(ツ)_/¯'],
'Other': ['(づ ̄ ³ ̄)づ', '(つ≧▽≦)つ', '(ノ^_^)ノ', 'ヾ(・ω・`)', '( ´ ▽ ` )b', '(*^▽^*)', '(◕‿◕)♡', '(◍•ᴗ•◍)❤', 'ε=ε=ε=ε=ε=ε=┌(; ̄◇ ̄)┘', 'ψ(`∇´)ψ', '( ´ ∀ `)♥', '( ˘ ³˘)♥', '⊂(◉‿◉)つ', 'ヾ(●´∀`●)', '(*´ω`*)', '(´・ω・`)', '( •ω• )', '(*_*)', '(ू˃̣̣̥̥̥̣̣̥‸˂̣̣̥̥̥̣̣̥ )', 'φ(゜▽゜*)♪', '♪(´ε` )', '(^▽^)', '(^_^)b', 'ヾ(☆▽☆)', '(o_0)!', '(O_o)', '( ゚ヮ゚)', '(; ͡° ͜ʖ ͡°)', '(>人<)', '(X_X)', '(×_×)', '(×_×;)', '(*_*)', '(・・;)', '( ̄~ ̄;)', '(@_@)', '(o_O)', '(O_o)', '(o_o)', '(O_O)', '(o.O)', '(O.o)', '( :o)', '( :O)', '( :0)', '(:0)', '( :0 )', '( :O )', '( :o )', '( :0)', '( :O)', '( :o)']
};
const ASCII_ART = [
':)', ':(', ':D', ':P', ';)', ':O', 'XD', 'B)', ':|', ':/', '>:(', ':\'(', '<3', '\\o/', '-_-', '^_^', 'O_O', '>_<', 'T_T', 'UwU', 'OwO', ':^)',
'¯\\_(ツ)_/¯', '( ͡° ͜ʖ ͡°)', 'ಠ_ಠ', '(☞゚ヮ゚)☞', '☜(゚ヮ゚☜)', '(⌐■_■)', 'ヽ(•‿•)ノ', '(¬‿¬)', 'ʕ•ᴥ•ʔ', '(つゝ´)', '(◕‿◕)', '(≧◡≦)', '(ง’̀-‘́)ง',
'(╯°□°)╯︵ ┻━┻', '┬─┬ ノ( ゜-゜ノ)', '┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻', '( •_•) ( •_•)>⌐■-■ (⌐■_■)', '(╮°-°)╮┳━┳', '(╯°□°)╯︵ ┻━┻ ︵ ┻━┻',
'( ͡ᵔ ͜ʖ ͡ᵔ )', '( ͝° ͜ʖ͡°)', '( ͡ʘ╭͜ʖ╮͡ʘ)', '(͡° ͜ ͡°)', 'ʕ̯•͡ᴥ•͡ʔ', 'ᶘ ᵒᴥᵒᶅ', 'ʕ•ᴥ•ʔ', 'ʕ ꈍᴥꈍʔ', 'ʕ̯•͡ᴥ•͡ʔ',
'ヽ(=^・ω・^=)丿', 'UwU', 'OwO', '>w<', '.w.', 'nwn', '^w^', '◕‿◕', 'ಠ_ಠ', 'ಠ益ಠ', 'ಥ_ಥ', '( ≖‿≖)', '(¬‿¬ )',
'( ˘ ɜ˘)♬', '♫♪.ılılıll|̲̅̅●̲̅̅|̲̅̅=̲̅̅|̲̅̅●̲̅̅|llılılı.♪♫', '(♬ ゚∀゚)♪', 'ヾ(⌐■_■)ノ♪', '┏(・o・)┛♪┗ ( ・o・)┓♪',
'(▀̿Ĺ̯▀̿ ̿)', '( ͡° ͜ʖ ͡°)╭∩╮', '(╭☞•́•̀)╭☞', '(☞゚ヮ゚)☞', '☜(゚ヮ゚☜)', 'ಠ_ರೃ', 'ヽ(`Д´)ノ', '( ͝° ͜ʖ͡°)',
'ᕦ(ò_óˇ)ᕤ', 'ᕙ(⇀‸↼‶)ᕗ', 'ᕕ( ᐛ )ᕗ', '(•̀ᴗ•́)و', '(ง •̀_•́)ง', '(ಥ﹏ಥ)', '(╯°□°)╯︵ ┻━┻', '(ノಠ益ಠ)ノ',
':)', ':-)', ':(', ':-(', ':D', ':-D', ';)', ';-)', ':P', ':-P', ':p', ':-p', ':O', ':-O', ':o', ':-o', ':|', ':-|',
':/', ':-/', ':S', ':-S', ':$', ':-$', 'XD', 'xD', 'Xd', 'xd', 'X-D', 'x-D', 'X=P', 'x=p', '>:(', '>:-(', '>:)', '>:-)',
'>:D', '>:-D', '>:P', '>:-P', ':\'(', ':\'[', ':\'-(', ':\'-[', ':*)', ':-*)', 'B)', 'B-)', '8)', '8-)', 'B-)',
':3', ':-3', ':>', ':->', ':<', ':-<', ':]', ':-]', ':[', ':-[', ':}', ':-}', ':{', ':-{', '=)', '=]', '=D', '=P',
'=3', '=(', '=[', '=/', '=|', '=O', '=o', '=*', '=3', '=X', '=x', '>:3', '>:-3', '>:]', '>:-]', '>:[', '>:-[',
';-);]', ';-[', ';-P', ';-p', ';-D', ';-O', ';-o', ';-3', ';-*', ';-X', ';-x',
'(o) (o)', '(O) (O)', '(o_O)', '(O_o)', '(o_o)', '(O_O)', '(o.O)', '(O.o)', '(o_0)', '(O_0)', '(0_o)', '(0_O)',
'(o.0)', '(O.0)', '(o.o)', '(O.O)', '(o_0)', '(O_0)',
'(@_@)', '(+_+)', '(x_x)', '(X_X)', '(*_*)', '(=_=)', '(-_-)', '(>_<)',
'<3', '</3', '<\\3', '<//3', '^_^', '^^', '^.^', '^_^', '^_~', '^_~', '^^;', '^^;', '^_^;', '^_^;', '^^;;', '^^;;', '^_^;;', '^_^;;',
'o_O', 'O_o', 'o.o', 'O.O', 'o_o', 'O_O', 'o.O', 'O.o', 'o_0', 'O_0', '0_o', '0_O', 'o.0', 'O.0', 'o.o', 'O.O','-_-',
'T_T', 'ToT', 'T_T', 'ToT', 'T_T', 'ToT', 'T_T', 'ToT', 'T_T', 'ToT', 'T_T', 'ToT', 'T_T', 'ToT', 'T_T',
'UwU', 'OwO', 'uwu', 'owo', 'UwU', 'OwO', 'uwu', 'owo', 'UwU', 'OwO', 'uwu', 'owo', 'UwU', 'OwO',
':innocent:', ':angel:', ':wink:', ':blush:', ':slight_smile:', ':smile:', ':laughing:', ':relaxed:', ':smirk:', ':expressionless:',
':unamused:', ':rolling_eyes:', ':thinking:', ':zipper_mouth:', ':neutral_face:', ':no_mouth:', ':dizzy_face:', ':grimacing:',
':sweat_smile:', ':joy:', ':rofl:', ':smiley:', ':grinning:', ':smile:', ':grin:', ':laughing:', ':wink:', ':blush:',
':yum:', ':stuck_out_tongue:', ':stuck_out_tongue_winking_eye:', ':stuck_out_tongue_closed_eyes:', ':disappointed:', ':worried:',
':angry:', ':rage:', ':cry:', ':persevere:', ':triumph:', ':relieved:', ':sleepy:', ':tired_face:', ':dizzy:',
':hushed:', ':open_mouth:', ':astonished:', ':scream:', ':scream_cat:', ':flushed:', ':frowning:', ':anguished:',
':fearful:', ':cold_sweat:', ':disappointed_relieved:', ':weary:', ':sob:', ':confounded:', ':tired_face:',
':mask:', ':thermometer_face:', ':head_bandage:', ':sleeping:', ':zzz:', ':poop:', ':smiling_imp:', ':imp:',
':japanese_ogre:', ':japanese_goblin:', ':skull:', ':skull_and_crossbones:', ':ghost:', ':alien:', ':space_invader:',
':robot:', ':jack_o_lantern:'
];
// ═══════════════════════════════════════════════
// STORAGE KEYS
// ═══════════════════════════════════════════════
const K = {
settings: 'uni_settings',
customCss: 'uni_custom_css',
apiGlobal: 'uni_api_global',
emojiCache: 'uni_emoji_cache_v3',
emojiCacheTs: 'uni_emoji_cache_ts_v3',
recent: 'uni_recent_v1',
favEmoji: 'uni_favoriteEmojis_v2',
favSticker: 'uni_favoriteStickers_v2',
favGif: 'uni_favoriteGifs_v2',
favEmojiLeg: 'uni_favoriteEmojis',
favStickerLeg: 'uni_favoriteStickers',
favGifLeg: 'uni_favoriteGifs',
favMeta: 'uni_favMeta_v1',
statsCache: 'uni_stats_cache_v1',
statsCacheTs: 'uni_stats_cache_ts_v1'
};
const CACHE_TTL = 6 * 3600000;
const STATS_TTL = 30 * 60000;
// ═══════════════════════════════════════════════
// DEFAULTS
// ═══════════════════════════════════════════════
const DEFAULTS = {
emojiSize:48,stickerSize:256,gifSize:256,gifInsertSize:256,
gifPerPage:24,emojiPerPage:108,stickerPerPage:24,gifMinCol:128,
gifShowTitles:false,menuWidth:700,menuHeightPx:650,menuMaxHeightVh:75,
menuRadius:10,tileRadius:10,gapEmoji:8,gapLarge:10,
bg:'#2f3136',bgAlpha:1.0,text:'#dcddde',accent:'#5865f2',
borderColor:'#3a3b41',borderWidth:1,shadow:0.6,
scrollbarWidth:8, scrollbarThumb:'#454545', scrollbarTrack:'rgba(0,0,0,0.1)',
fontFamily:'"Segoe UI",Tahoma,Geneva,Verdana,sans-serif',
uiFontSize:14,tileBg:'#202225',hoverScale:1.05,glowStrength:10,
starColor:'#f1c40f',starBg:'rgba(0,0,0,.35)',starBorder:'#3a3b41',
starTop:6,starRight:6,starSize:28,starSmSize:22,
btnRadius:8,searchRadius:10,searchBorder:'#444',
paginationBg:'#202225',paginationActiveBg:'#5865f2',paginationColor:'#b9bbbe',
backdropBlur:0,zIndex:9999999,showGifStar:true,gifProvider:'local',
apiScope:'global',redditSubs:'gifs, HighQualityGifs, reactiongifs',
provTenor:true,provGiphy:true,provImgur:true,provTumblr:true,
redditIncludeImages:false,localGifUrls:'',
closeOnInsert:false,searchDebounceMs:200,
menuPositionX:'',menuPositionY:'',
insertFormat:'bbcode',insertTemplate:'[img=${size}]${url}[/img] ',
showTooltips:true,recentEnabled:true,recentMax:50,
tabOrder:'home,gifs,stickers,emojis',compactMode:false,
emojiSubTab:'custom',lazyLoadThreshold:150
};
// ═══════════════════════════════════════════════
// SETTINGS LOAD
// ═══════════════════════════════════════════════
const S = Object.assign({}, DEFAULTS, (() => {
try { return JSON.parse(GM_Get(K.settings, '{}')); } catch { return {}; }
})());
const saveS = () => GM_Set(K.settings, JSON.stringify(S));
// ═══════════════════════════════════════════════
// API KEYS (SECURE)
// ═══════════════════════════════════════════════
const apiSiteKey = `uni_api_site:${location.host}`;
const API_F = { giphy:'giphyKey', tenor:'tenorKey', imgur:'imgurClientId', tumblr:'tumblrKey' };
function readApi(sc) { try { return JSON.parse(GM_Get(sc==='site'?apiSiteKey:K.apiGlobal,'{}'))||{}; } catch { return {}; } }
function writeApi(sc,o) { GM_Set(sc==='site'?apiSiteKey:K.apiGlobal,JSON.stringify(o||{})); }
function getKey(p) { const f=API_F[p]; if(!f)return ''; const sc=S.apiScope||'global'; return readApi(sc)[f]||readApi(sc==='site'?'global':'site')[f]||''; }
function setKey(p,v) { const f=API_F[p]; if(!f)return; const sc=S.apiScope||'global'; const st=readApi(sc); st[f]=v||''; writeApi(sc,st); }
function hasKey(p) { return !!getKey(p); }
function maskK(k) { if(!k)return''; return k.length<=10?'●'.repeat(k.length):k.slice(0,4)+'●'.repeat(Math.max(4,k.length-8))+k.slice(-4); }
// ═══════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════
let allEmojis=[], currentEmojiPage=1;
let allStickers=[], currentStickerPage=1;
let allGifs=[], gifsLoaded=false, localGifStatus='';
let currentGifPage=1;
let activeTab='home';
let activeEmojiSub=S.emojiSubTab||'custom';
let favOnlyEmoji=false, favOnlyStickers=false, favOnlyGifs=false;
let stickersLoaded=false;
let emojiProg={loaded:0,total:EMOJI_COUNT,done:false,cached:false};
let _rendering=false; // Prevent re-entrant rendering
// Recent
let recentItems=(()=>{try{return JSON.parse(GM_Get(K.recent,'[]'));}catch{return[];}})();
function addRecent(item) {
if(!S.recentEnabled||!item)return;
const key=item.url||item.text||item.name;
if(!key)return;
recentItems=recentItems.filter(r=>(r.url||r.text||r.name)!==key);
recentItems.unshift({url:item.url||'',name:item.name||'',text:item.text||'',type:item.type||'emoji',isText:!!item.isText,ts:Date.now()});
if(recentItems.length>(S.recentMax||50))recentItems.length=S.recentMax||50;
try{GM_Set(K.recent,JSON.stringify(recentItems));}catch{}
}
// ═══════════════════════════════════════════════
// FAVORITES (FIXED)
// ═══════════════════════════════════════════════
const _favV2={
emoji: new Set(JSON.parse(GM_Get(K.favEmoji,'[]'))),
sticker: new Set(JSON.parse(GM_Get(K.favSticker,'[]'))),
gif: new Set(JSON.parse(GM_Get(K.favGif,'[]')))
};
const _favLeg={
emoji: new Set(JSON.parse(GM_Get(K.favEmojiLeg,'[]'))),
sticker: new Set(JSON.parse(GM_Get(K.favStickerLeg,'[]'))),
gif: new Set(JSON.parse(GM_Get(K.favGifLeg,'[]')))
};
let favMeta=(()=>{try{return JSON.parse(GM_Get(K.favMeta,'{}'))||{};}catch{return{};}})();
// Direct key mapping - no string construction
const FAV_SAVE_KEYS = {
emoji: K.favEmoji,
sticker: K.favSticker,
gif: K.favGif
};
const FAV_LEG_KEYS = {
emoji: K.favEmojiLeg,
sticker: K.favStickerLeg,
gif: K.favGifLeg
};
function saveFavSet(type) {
try {
GM_Set(FAV_SAVE_KEYS[type], JSON.stringify([..._favV2[type]]));
GM_Set(FAV_LEG_KEYS[type], JSON.stringify([..._favLeg[type]]));
} catch(e) {
console.error('[UE] Failed to save favorites:', e);
}
}
function saveFavMeta() {
try { GM_Set(K.favMeta, JSON.stringify(favMeta||{})); } catch {}
}
function isFav(type, item) {
if(!item||!type)return false;
const key = item.url || item.text || item.name;
if(!key) return false;
return (_favV2[type] && _favV2[type].has(key)) || (_favLeg[type] && _favLeg[type].has(item.name));
}
function favToggle(type, item) {
if(!item||!type)return;
const key = item.url || item.text || item.name;
if(!key)return;
try {
if(isFav(type, item)) {
// Remove
_favV2[type].delete(key);
_favLeg[type].delete(item.name);
if(favMeta[key]) { delete favMeta[key]; }
} else {
// Add
_favV2[type].add(key);
favMeta[key] = { name: item.name||'', type, ts: Date.now(), isText: !!item.isText, text: item.text||'' };
}
saveFavSet(type);
saveFavMeta();
} catch(e) {
console.error('[UE] favToggle error:', e);
}
}
function sortFavFirst(arr, type) {
const v2=_favV2[type], leg=_favLeg[type];
if(!v2) return arr;
return arr.slice().sort((a,b)=>{
const ak=a.url||a.text||a.name;
const bk=b.url||b.text||b.name;
const af=v2.has(ak)||(leg&&leg.has(a.name));
const bf=v2.has(bk)||(leg&&leg.has(b.name));
return af===bf ? a.originalIndex-b.originalIndex : af?-1:1;
});
}
function getFavList(tf) {
const arr=[];
try {
Object.entries(favMeta).forEach(([key,m])=>{
if(tf&&m.type!==tf)return;
arr.push({
url: m.isText ? '' : key,
text: m.isText ? (m.text||m.name||key) : '',
name: m.name||key.split('/').pop(),
type: m.type,
isText: !!m.isText,
originalIndex: m.ts||0,
animated: true
});
});
} catch(e) { console.error('[UE] getFavList error:', e); }
arr.sort((a,b)=>b.originalIndex-a.originalIndex);
return arr;
}
// ═══════════════════════════════════════════════
// UTILITIES
// ═══════════════════════════════════════════════
function hexRgba(h,a){const m=String(h).replace('#','');const s=m.length===3?m.split('').map(c=>c+c).join(''):m;const b=parseInt(s||'000000',16);return`rgba(${(b>>16)&255},${(b>>8)&255},${b&255},${Math.max(0,Math.min(1,Number(a)||1))})`;}
function debounce(fn,ms){let t;return(...a)=>{clearTimeout(t);t=setTimeout(()=>fn(...a),ms);};}
function ensurePage(len,pp,pg){const tp=Math.max(1,Math.ceil(len/pp));return{totalPages:tp,page:Math.min(Math.max(pg,1),tp)};}
function fetchJson(url,headers){
return new Promise((res,rej)=>{
GM_xmlhttpRequest({
method:'GET',url,headers:headers||{},timeout:12000,
onload:r=>{try{res(JSON.parse(r.responseText));}catch{rej(new Error('Parse'));}},
onerror:()=>rej(new Error('Network')),
ontimeout:()=>rej(new Error('Timeout'))
});
});
}
function fetchText(url){
return new Promise((res,rej)=>{
GM_xmlhttpRequest({method:'GET',url,timeout:8000,
onload:r=>res(r.responseText),onerror:()=>rej(new Error('Fail')),ontimeout:()=>rej(new Error('Timeout'))
});
});
}
function toArray(merged,type='emoji'){
let idx=0;
return Object.entries(merged).map(([name,val])=>{
const url=val&&typeof val==='object'?(val.url||''):(val||'');
if(!url)return null;
return{name,url,tags:Array.isArray(val?.tags)?val.tags:[],animated:val?.animated??/\.(gif|apng)($|\?)/i.test(url),type,originalIndex:idx++};
}).filter(Boolean);
}
function insertContent(item){
if(!item)return;
const size=item.type==='emoji'?(S.emojiSize||48):item.type==='sticker'?(S.stickerSize||256):(S.gifInsertSize||S.gifSize||256);
let code;
if(item.isText){
code=item.text||item.name||'';
} else if(S.insertFormat==='markdown') code=``;
else if(S.insertFormat==='html') code=`<img src="${item.url}" width="${size}" alt="${item.name||''}">`;
else if(S.insertFormat==='url') code=item.url;
else if(S.insertFormat==='custom'&&S.insertTemplate) code=S.insertTemplate.replace(/\$\{url\}/g,item.url||'').replace(/\$\{size\}/g,size).replace(/\$\{name\}/g,item.name||'');
else code=`[img=${size}]${item.url}[/img] `;
// Target Logic
let input = null;
// 1. Try specific tracker classes (previous request)
input = document.querySelector('textarea.form__textarea.chatV2__input');
// 2. Try IRC (The Lounge, Kiwi, generic)
if(!input) input = document.querySelector('#input, #chat-input, .irc-input, textarea.irc-input, input.irc-input');
// 3. Try standard Private Tracker IDs
if (!input) {
const ids=['chatbox__messages-create','new-comment__textarea','reply-comment','bbcode-message','bbcode-content','bbcode-signature','bbcode-about'];
input=document.querySelector(ids.map(id=>`textarea#${id}`).join(', '));
}
if(!input)return;
// Handle insertion
if(typeof input.selectionStart==='number' && typeof input.setRangeText==='function') {
input.setRangeText(code, input.selectionStart, input.selectionEnd, 'end');
} else {
input.value+=code;
}
input.focus();
try{input.dispatchEvent(new Event('input',{bubbles:true}));}catch{}
addRecent(item);
if(S.closeOnInsert)container.style.display='none';
}
async function copyText(t){
try{if(navigator.clipboard&&window.isSecureContext){await navigator.clipboard.writeText(t);return;}}catch{}
try{if(typeof GM_setClipboard==='function'){GM_setClipboard(t,'text');return;}}catch{}
const ta=document.createElement('textarea');ta.value=t;ta.style.cssText='position:fixed;opacity:0';
document.body.appendChild(ta);ta.select();document.execCommand('copy');ta.remove();
}
// Lazy loading
let _lo=null;
function lazyObs(){
if(_lo)return _lo;
_lo=new IntersectionObserver(entries=>{
for(const e of entries)if(e.isIntersecting&&e.target.dataset.src){e.target.src=e.target.dataset.src;delete e.target.dataset.src;_lo.unobserve(e.target);}
},{rootMargin:`${S.lazyLoadThreshold||150}px`});
return _lo;
}
function mkImg(url,alt){
const img=document.createElement('img');img.alt=alt||'';
if(S.showTooltips)img.title=alt||'';
img.dataset.src=url;img.src='data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
lazyObs().observe(img);return img;
}
function destroyLazy(){if(_lo){_lo.disconnect();_lo=null;}}
// Context menu (FIXED - proper cleanup)
let ctxEl=null;
let _ctxHideHandler=null;
function hideCtx(){if(ctxEl)ctxEl.style.display='none';}
function showCtx(items,x,y){
if(!ctxEl){
ctxEl=document.createElement('div');ctxEl.id='uni-ctx-menu';
ctxEl.addEventListener('contextmenu',e=>e.preventDefault());
document.body.appendChild(ctxEl);
_ctxHideHandler=e=>{
if(ctxEl&&ctxEl.style.display!=='none'&&!ctxEl.contains(e.target)){
hideCtx();
}
};
document.addEventListener('mousedown',_ctxHideHandler,true);
window.addEventListener('scroll',hideCtx,true);
document.addEventListener('keydown',e=>{if(e.key==='Escape')hideCtx();});
}
ctxEl.innerHTML='';
(items||[]).forEach(it=>{
const b=document.createElement('button');
b.innerHTML=it.icon?`<i class="fa-solid ${it.icon}"></i> ${it.label}`:it.label;
b.addEventListener('click',e=>{
e.preventDefault();
e.stopPropagation();
hideCtx();
// Use setTimeout to prevent the click from interfering with current rendering
setTimeout(()=>{
try{it.onClick?.();}catch(err){console.error('[UE] ctx action error:',err);}
},0);
});
ctxEl.appendChild(b);
});
ctxEl.style.display='block';
requestAnimationFrame(()=>{
if(!ctxEl)return;
const r=ctxEl.getBoundingClientRect();
ctxEl.style.left=Math.min(x,innerWidth-r.width-8)+'px';
ctxEl.style.top=Math.min(y,innerHeight-r.height-8)+'px';
});
}
function notice(msg){
let n=document.getElementById('uni-notice');
if(!n){
n=document.createElement('div');n.id='uni-notice';
Object.assign(n.style,{position:'absolute',top:'6px',left:'12px',right:'12px',
background:'rgba(0,0,0,.7)',color:'#fff',padding:'8px 14px',borderRadius:'8px',
fontSize:'12px',zIndex:'20',pointerEvents:'none',textAlign:'center',transition:'opacity .3s'});
container.appendChild(n);
}
n.textContent=msg;n.style.display='block';n.style.opacity='1';
clearTimeout(n._t);
n._t=setTimeout(()=>{n.style.opacity='0';setTimeout(()=>n.style.display='none',300);},3500);
}
// ═══════════════════════════════════════════════
// DOM
// ═══════════════════════════════════════════════
const old=document.getElementById('uni-emoji-menu');if(old)old.remove();
const container=document.createElement('div');container.id='uni-emoji-menu';document.body.appendChild(container);
// ═══════════════════════════════════════════════
// CSS
// ═══════════════════════════════════════════════
function cssVars(){
const s=S;
return`#uni-emoji-menu{--mw:${s.menuWidth}px;--mh:${s.menuHeightPx}px;--mvh:${s.menuMaxHeightVh}vh;--mr:${s.menuRadius}px;--tr:${s.tileRadius}px;--bg:${hexRgba(s.bg,s.bgAlpha)};--txt:${s.text};--acc:${s.accent};--bc:${s.borderColor};--bw:${s.borderWidth}px;--sh:${s.shadow};--et:${s.emojiSize}px;--gh:${s.gifSize}px;--gmc:${s.gifMinCol}px;--ge:${s.gapEmoji}px;--gl:${s.gapLarge}px;--ff:${s.fontFamily};--fs:${s.uiFontSize}px;--tbg:${s.tileBg};--hs:${s.hoverScale};--gs:${s.glowStrength}px;--sc:${s.starColor};--sbg:${s.starBg};--sbc:${s.starBorder};--st:${s.starTop}px;--sr:${s.starRight}px;--ss:${s.starSize}px;--sss:${s.starSmSize}px;--br:${s.btnRadius}px;--xr:${s.searchRadius}px;--xb:${s.searchBorder};--pb:${s.paginationBg};--pab:${s.paginationActiveBg};--pc:${s.paginationColor};--blur:${s.backdropBlur}px;--z:${s.zIndex};--sbw:${s.scrollbarWidth}px;--sbth:${s.scrollbarThumb};--sbt:${s.scrollbarTrack}}`;
}
const CSS=`
#uni-emoji-menu{position:fixed;left:60%;top:10%;width:var(--mw);height:var(--mh);max-height:var(--mvh);background:var(--bg);color:var(--txt);border-radius:var(--mr);border:var(--bw) solid var(--bc);box-shadow:0 8px 24px rgba(0,0,0,var(--sh));padding:10px;display:none;font-family:var(--ff);font-size:var(--fs);user-select:none;flex-direction:column;z-index:var(--z);backdrop-filter:blur(var(--blur));overflow:hidden;box-sizing:border-box}
#uni-emoji-menu *{box-sizing:border-box}
#uni-emoji-menu ::-webkit-scrollbar{width:var(--sbw)}
#uni-emoji-menu ::-webkit-scrollbar-track{background:var(--sbt);border-radius:2px}
#uni-emoji-menu ::-webkit-scrollbar-thumb{background:var(--sbth);border-radius:2px}
#uni-emoji-menu ::-webkit-scrollbar-thumb:hover{background:var(--acc)}
#uni-emoji-menu .hdr{display:flex;align-items:center;justify-content:center;position:relative;margin-bottom:4px;min-height:26px}
#uni-emoji-menu .hdr .title{font-weight:700;font-size:15px;display:flex;align-items:center;gap:6px}
#uni-emoji-menu .hdr .ver{color:#888;font-size:11px;font-weight:600}
#uni-emoji-menu .hdr .dot{width:7px;height:7px;border-radius:50%;background:#43b581;box-shadow:0 0 5px #43b581;display:inline-block}
#uni-emoji-menu .hdr .drag{position:absolute;left:6px;cursor:move;color:#aaa;font-size:18px}
#uni-emoji-menu .hdr .close{position:absolute;right:6px;cursor:pointer;color:#ff7373;font-size:16px;background:none;border:none;line-height:1}
#uni-emoji-menu .tabs{display:flex;gap:4px;margin:2px 0 4px;flex-wrap:wrap}
#uni-emoji-menu .tab{font-weight:600;font-size:12px;color:#ccc;background:none;border:none;border-radius:7px;padding:4px 10px;cursor:pointer;opacity:.7;transition:all .15s;display:flex;align-items:center;gap:5px;white-space:nowrap}
#uni-emoji-menu .tab:hover{background:#46484c;opacity:1}
#uni-emoji-menu .tab.active{background:#46484c;opacity:1;color:#fff}
#uni-emoji-menu .tab.st{margin-left:auto}
#uni-emoji-menu .toolbar{display:flex;align-items:center;gap:6px;margin:2px 6px;flex-wrap:wrap}
#uni-emoji-menu .search-row{display:flex;margin:2px 0 6px}
#uni-emoji-menu #uni-search{position:sticky;top:0;background:rgba(255,255,255,.03);color:var(--txt);height:32px;border:2px solid var(--xb);border-radius:var(--xr);width:100%;padding:5px 10px;font-size:12px;transition:all .15s}
#uni-emoji-menu #uni-search::placeholder{color:#777}
#uni-emoji-menu #uni-search:focus{background:rgba(255,255,255,.05);box-shadow:0 0 5px var(--acc);outline:none}
#uni-emoji-menu .sub-tabs{display:flex;gap:3px;margin:2px 4px 4px;flex-wrap:wrap}
#uni-emoji-menu .sub-tab{font-size:11px;font-weight:600;color:#bbb;background:#1a1b1e;border:1px solid var(--bc);border-radius:6px;padding:3px 8px;cursor:pointer;transition:all .12s;display:flex;align-items:center;gap:4px}
#uni-emoji-menu .sub-tab:hover{background:#2a2c30;color:#fff}
#uni-emoji-menu .sub-tab.active{background:var(--acc);color:#fff;border-color:var(--acc)}
#uni-emoji-menu .eg{display:grid;grid-template-columns:repeat(auto-fill,var(--et));gap:var(--ge);overflow-y:auto;flex:1;padding:4px 4px 8px;justify-content:start;}
#uni-emoji-menu .et{position:relative;width:var(--et);height:var(--et);display:flex;align-items:center;justify-content:center;background:var(--tbg);border-radius:6px;transition:transform .1s,box-shadow .1s; overflow: hidden;}
#uni-emoji-menu .et img{max-width:100%;max-height:100%;object-fit:contain;border-radius:6px;cursor:pointer}
#uni-emoji-menu .et:hover{transform:scale(var(--hs));box-shadow:0 0 var(--gs) var(--acc)}
#uni-emoji-menu .et .te{cursor:pointer;line-height:1;text-align:center;width:100%;height:100%;display:flex;align-items:center;justify-content:center;}
#uni-emoji-menu .sg{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:var(--gl);overflow-y:auto;flex:1;padding:6px 4px 10px;position:relative}
#uni-emoji-menu .gg{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--gmc),1fr));gap:var(--gl);overflow-y:auto;flex:1;padding:6px 4px 10px;position:relative}
#uni-emoji-menu .tl{position:relative;width:100%;height:110px;background:var(--tbg);border-radius:var(--tr);display:flex;align-items:center;justify-content:center;box-shadow:0 1px 3px rgba(0,0,0,.3);transition:transform .1s,box-shadow .1s;overflow:hidden}
#uni-emoji-menu .tl img{max-width:100%;max-height:100%;object-fit:contain;border-radius:var(--tr);cursor:pointer}
#uni-emoji-menu .tl:hover{transform:scale(var(--hs));box-shadow:0 0 var(--gs) var(--acc)}
#uni-emoji-menu .star,#uni-emoji-menu .ssm{position:absolute;top:var(--st);right:var(--sr);display:grid;place-items:center;background:var(--sbg);border:1px solid var(--sbc);color:var(--sc);border-radius:999px;cursor:pointer;line-height:1;opacity:0;transform:scale(.9);pointer-events:none;transition:opacity .12s,transform .12s}
#uni-emoji-menu .star{width:var(--ss);height:var(--ss);font-size:calc(var(--ss)*.55)}
#uni-emoji-menu .ssm{width:var(--sss);height:var(--sss);font-size:calc(var(--sss)*.55)}
#uni-emoji-menu .et:hover .ssm,#uni-emoji-menu .tl:hover .star{opacity:1;transform:scale(1);pointer-events:auto}
#uni-emoji-menu .star.active,#uni-emoji-menu .ssm.active{opacity:.6;pointer-events:auto}
#uni-emoji-menu .gt{position:absolute;left:5px;right:5px;bottom:5px;font-size:10px;color:#fff;background:rgba(0,0,0,.45);padding:2px 5px;border-radius:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;pointer-events:none}
#uni-emoji-menu .pill{background:#202225;border:1px solid var(--bc);color:#ddd;border-radius:var(--br);padding:4px 9px;cursor:pointer;font-weight:600;font-size:11px;transition:background .12s;display:flex;align-items:center;gap:4px}
#uni-emoji-menu .pill.active{background:var(--acc);border-color:var(--acc);color:#fff}
#uni-emoji-menu .pill:hover{background:#36393f}
#uni-emoji-menu .pb{background:#202225;border:1px solid var(--bc);color:#ddd;border-radius:var(--br);padding:4px 8px;cursor:pointer;font-weight:600;font-size:11px;transition:background .12s}
#uni-emoji-menu .pb:hover{background:#36393f}
#uni-emoji-menu .pb.active{background:var(--acc);color:#fff;border-color:var(--acc)}
#uni-emoji-menu .pager{display:flex;justify-content:center;gap:3px;margin-top:4px;flex-wrap:wrap}
#uni-emoji-menu .pager button{background:var(--pb);border:none;border-radius:3px;color:var(--pc);padding:4px 8px;font-size:12px;cursor:pointer;min-width:28px;font-weight:600;transition:all .15s}
#uni-emoji-menu .pager button:hover:not(.active){background:#36393f;color:#fff;box-shadow:0 0 4px var(--acc)}
#uni-emoji-menu .pager button.active{background:var(--pab);color:#fff;box-shadow:0 0 6px var(--acc)}
#uni-emoji-menu .pj{display:flex;justify-content:center;gap:5px;margin-top:5px}
#uni-emoji-menu .pj input{width:65px;padding:4px;font-size:12px;border:none;border-radius:12px;background:#202225;color:#ddd;text-align:center}
#uni-emoji-menu .pj button{background:var(--acc);border:none;border-radius:4px;padding:4px 10px;color:#fff;font-weight:600;font-size:12px;cursor:pointer}
#uni-emoji-menu .panel{display:none;overflow-y:auto;flex:1;padding:8px 6px}
#uni-emoji-menu .section{background:rgba(0,0,0,.15);border:1px solid var(--bc);border-radius:9px;padding:10px;margin:6px 0}
#uni-emoji-menu .section h3{margin:0 0 6px;font-size:13px;color:#fff;display:flex;align-items:center;gap:6px}
#uni-emoji-menu .row{display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin:3px 0}
#uni-emoji-menu .btn{background:var(--acc);border:none;border-radius:7px;padding:4px 10px;color:#fff;font-weight:600;font-size:11px;cursor:pointer;transition:filter .12s;display:inline-flex;align-items:center;gap:4px}
#uni-emoji-menu .btn.sec{background:#202225;border:1px solid var(--bc);color:#ddd}
#uni-emoji-menu .btn.warn{background:#b93131}
#uni-emoji-menu .btn:hover{filter:brightness(.9)}
#uni-emoji-menu .badge{font-size:10px;margin-left:4px}
#uni-emoji-menu .badge-ok{color:#43b581}
#uni-emoji-menu .badge-err{color:#ff6464}
#uni-emoji-menu .badge-warn{color:#faa61a}
#uni-emoji-menu .empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;text-align:center;opacity:.8;padding:14px;font-size:13px}
#uni-emoji-menu .home-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:6px}
#uni-emoji-menu .home-card{background:rgba(0,0,0,.15);border:1px solid var(--bc);border-radius:7px;padding:9px}
#uni-emoji-menu .home-card h4{margin:0 0 5px;font-size:12px;display:flex;align-items:center;gap:5px}
#uni-emoji-menu .chip{display:inline-block;background:#202225;border:1px solid var(--bc);border-radius:999px;padding:2px 7px;font-size:10px;margin:1px 3px 1px 0}
#uni-emoji-menu .link-btn{display:inline-flex;align-items:center;gap:5px;background:#202225;border:1px solid var(--bc);color:#ddd;border-radius:7px;padding:5px 10px;font-size:11px;font-weight:600;cursor:pointer;text-decoration:none;transition:all .12s}
#uni-emoji-menu .link-btn:hover{background:#36393f;color:#fff;border-color:var(--acc)}
#uni-emoji-menu .stat-num{font-size:18px;font-weight:700;color:var(--acc);margin-right:4px}
#uni-emoji-menu .api-disp{font-family:monospace;font-size:11px;color:#888;background:#1a1b1e;border:1px solid var(--bc);border-radius:5px;padding:4px 7px;word-break:break-all}
#uni-emoji-menu .api-disp.has{color:#43b581}
#uni-emoji-menu input.api-key{width:100%;padding:5px 7px;border:1px solid var(--bc);border-radius:5px;background:#1a1b1e;color:#e3e5e8;font-size:12px}
#uni-emoji-menu input.mini{width:70px;padding:3px 5px;border:1px solid var(--bc);border-radius:5px;background:#1a1b1e;color:#e3e5e8;font-size:12px}
#uni-emoji-menu input.fi{width:100%;max-width:350px;padding:4px 7px;border:1px solid var(--bc);border-radius:5px;background:#1a1b1e;color:#e3e5e8;font-size:12px}
#uni-emoji-menu input[type="color"]{width:28px;height:24px;padding:0;border:none;border-radius:5px;cursor:pointer}
#uni-emoji-menu select.sel{padding:4px 7px;border-radius:5px;border:1px solid var(--bc);background:#202225;color:#e3e5e8;font-size:12px}
#uni-emoji-menu .range{display:flex;align-items:center;gap:6px}
#uni-emoji-menu .range input[type="range"]{flex:1;max-width:180px}
#uni-emoji-menu .range .rv{min-width:42px;text-align:right;font-size:11px;opacity:.7}
#uni-emoji-menu textarea.area{width:100%;min-height:80px;background:#1f2124;color:#e8e8e8;border:1px solid var(--bc);border-radius:7px;padding:6px;font-size:11px;resize:vertical}
#uni-emoji-menu label.cb{display:flex;align-items:center;gap:5px;font-size:12px;cursor:pointer}
#uni-emoji-menu .recent-grid{display:grid;grid-template-columns:repeat(auto-fill,44px);gap:5px;padding:4px 0}
#uni-emoji-menu .recent-grid img,.recent-grid .ri{width:44px;height:44px;object-fit:contain;border-radius:5px;cursor:pointer;background:var(--tbg);display:flex;align-items:center;justify-content:center;}
#uni-emoji-menu .recent-grid img:hover,.recent-grid .ri:hover{transform:scale(1.1);box-shadow:0 0 6px var(--acc)}
#uni-ctx-menu{position:fixed;z-index:2147483647;background:#2b2d31;color:#e3e5e8;border:1px solid #2a2c30;border-radius:7px;min-width:170px;box-shadow:0 8px 28px rgba(0,0,0,.5);padding:3px;display:none}
#uni-ctx-menu button{width:100%;text-align:left;background:transparent;border:none;color:#e3e5e8;padding:6px 9px;border-radius:5px;cursor:pointer;font-weight:600;font-size:11px;display:flex;align-items:center;gap:6px}
#uni-ctx-menu button:hover{background:#3a3c43}
#uni-ctx-menu button i{width:14px;text-align:center;font-size:11px;opacity:.8}
#global-emoji-button.emoji-button{position:fixed;right:15px;bottom:35px;z-index:2147483647;cursor:pointer;font-size:38px;display:inline-flex;align-items:center;justify-content:center;transition:transform .2s,filter .2s;user-select:none;filter:grayscale(100%)}
#global-emoji-button.emoji-button:hover{transform:scale(1.1);filter:grayscale(0%)}
`;
GM_addStyle(cssVars()+CSS);
let _vt=null;
function applyVars(){
if(!_vt){_vt=document.createElement('style');_vt.id='uni-vars';document.head.appendChild(_vt);}
_vt.textContent=cssVars();
container.classList.toggle('compact',!!S.compactMode);
if(S.menuPositionX)container.style.left=S.menuPositionX;
if(S.menuPositionY)container.style.top=S.menuPositionY;
}
function applyCustomCss(css){let t=document.getElementById('uni-custom-css');if(!t){t=document.createElement('style');t.id='uni-custom-css';document.head.appendChild(t);}t.textContent=css||'';GM_Set(K.customCss,css||'');}
{const c=GM_Get(K.customCss,'');if(c?.trim())applyCustomCss(c);}
// ═══════════════════════════════════════════════
// HTML SKELETON
// ═══════════════════════════════════════════════
container.innerHTML=`
<div class="hdr">
<span class="drag" title="Drag"><i class="fa-solid fa-arrows-up-down-left-right"></i></span>
<span class="title"><i><img src="https://ptpimg.me/91xfz9.gif" style="width: 27px; height: 25px;" /></i> Ultimate Emojis <span class="ver">v${VERSION}</span> <span class="dot"></span></span>
<button class="close" title="Close"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="tabs" id="tb"></div>
<div class="panel" id="p-home"></div>
<div class="toolbar" id="tb-e" style="display:none"><button class="pill" id="e-fav"><i class="fa-solid fa-star" style="color: #e7a423;"></i></button><span style="flex:1"></span><span class="badge" id="e-st"></span></div>
<div class="sub-tabs" id="e-subs" style="display:none"></div>
<div class="search-row" id="sr" style="display:none"><input type="text" id="uni-search" placeholder="Search..." autocomplete="off"></div>
<div class="eg" id="ge" style="display:none"></div>
<div class="toolbar" id="tb-g" style="display:none">
<span style="opacity:.5;font-size:10px"><i class="fa-solid fa-globe"></i></span>
<button class="pb" data-p="local"><i class="fa-solid fa-hard-drive"></i> Local</button>
<button class="pb" data-p="tenor"><i class="fa-solid fa-t"></i> Tenor</button>
<button class="pb" data-p="giphy"><i class="fa-solid fa-g"></i> GIPHY</button>
<button class="pb" data-p="imgur"><i class="fa-solid fa-image"></i> Imgur</button>
<button class="pb" data-p="reddit"><i class="fa-brands fa-reddit"></i> Reddit</button>
<button class="pb" data-p="tumblr"><i class="fa-brands fa-tumblr"></i> Tumblr</button>
<span style="flex:1"></span>
<button class="pill" id="g-fav"><i class="fa-solid fa-star" style="color: #e7a423;"></i></button>
</div>
<div class="toolbar" id="tb-s" style="display:none"><button class="pill" id="s-fav"><i class="fa-solid fa-star" style="color: #e7a423;"></i></button></div>
<div class="sg" id="gs" style="display:none"></div>
<div class="gg" id="gg" style="display:none"></div>
<div class="pager" id="pager" style="display:none"></div>
<div class="pj" id="pj" style="display:none"><input type="number" id="pji" min="1" placeholder="Pg"><button id="pjg">Go</button></div>
<div class="panel" id="p-set" style="display:none"></div>
`;
const $=s=>container.querySelector(s);
const gE=$('#ge'),gS=$('#gs'),gG=$('#gg');
const pager=$('#pager'),pjDiv=$('#pj'),pjIn=$('#pji'),pjGo=$('#pjg');
const searchIn=$('#uni-search'),sRow=$('#sr');
const tbE=$('#tb-e'),tbG=$('#tb-g'),tbS=$('#tb-s');
const eSubs=$('#e-subs');
const hPanel=$('#p-home'),sPanel=$('#p-set');
const tabBar=$('#tb');
const eSt=$('#e-st');
// ═══════════════════════════════════════════════
// TABS
// ═══════════════════════════════════════════════
const TAB_D={home:{l:'Home',i:'fa-house'},gifs:{l:'GIFs',i:'fa-film'},stickers:{l:'Stickers',i:'fa-note-sticky'},emojis:{l:'Emojis',i:'fa-face-smile'},recent:{l:'Recent',i:'fa-clock-rotate-left'}};
function buildTabs(){
tabBar.innerHTML='';
const ord=(S.tabOrder||'home,gifs,stickers,emojis').split(',').map(s=>s.trim()).filter(Boolean);
if(S.recentEnabled&&!ord.includes('recent'))ord.push('recent');
ord.forEach(k=>{const d=TAB_D[k];if(!d)return;const b=document.createElement('div');b.className='tab'+(k===activeTab?' active':'');b.dataset.tab=k;b.innerHTML=`<i class="fa-solid ${d.i}"></i> ${d.l}`;b.addEventListener('click',()=>switchTab(k));tabBar.appendChild(b);});
const sb=document.createElement('div');sb.className='tab st'+(activeTab==='settings'?' active':'');sb.dataset.tab='settings';sb.innerHTML='<i class="fa-solid fa-gear"></i>';sb.addEventListener('click',()=>switchTab('settings'));tabBar.appendChild(sb);
}
function setTabUI(t){tabBar.querySelectorAll('.tab').forEach(b=>b.classList.toggle('active',b.dataset.tab===t));}
function hideAll(){[hPanel,gE,gS,gG,sPanel].forEach(e=>e.style.display='none');[tbE,tbG,tbS,sRow,pager,pjDiv,eSubs].forEach(e=>e.style.display='none');}
// ═══════════════════════════════════════════════
// PAGINATION
// ═══════════════════════════════════════════════
function renderPager(total,cur,goTo){
pager.innerHTML='';pager.style.display='';pjDiv.style.display='';
total=Math.max(1,total);
const mk=(l,fn,act=false)=>{const b=document.createElement('button');b.textContent=l;if(act)b.classList.add('active');b.addEventListener('click',e=>{e.preventDefault();e.stopPropagation();fn();});return b;};
if(cur>1)pager.appendChild(mk('‹',()=>goTo(cur-1)));
let s=Math.max(1,cur-3),e=Math.min(total,s+6);if(e-s<6)s=Math.max(1,e-6);
if(s>1){pager.appendChild(mk('1',()=>goTo(1)));if(s>2)pager.appendChild(mk('…',()=>{}));}
for(let i=s;i<=e;i++)pager.appendChild(mk(String(i),()=>goTo(i),i===cur));
if(e<total){if(e<total-1)pager.appendChild(mk('…',()=>{}));pager.appendChild(mk(String(total),()=>goTo(total)));}
if(cur<total)pager.appendChild(mk('›',()=>goTo(cur+1)));
}
// ═══════════════════════════════════════════════
// EMOJI LOADING
// ═══════════════════════════════════════════════
async function fetchFast(urls){for(const u of urls){try{return await fetchJson(u);}catch{}}throw new Error('All failed');}
async function loadEmojis(){
const ts=Number(GM_Get(K.emojiCacheTs,'0'));
if(Date.now()-ts<CACHE_TTL){
try{const c=JSON.parse(GM_Get(K.emojiCache,'null'));if(c&&Object.keys(c).length>0){allEmojis=toArray(c,'emoji');emojiProg={loaded:EMOJI_COUNT,total:EMOJI_COUNT,done:true,cached:true};updEmojiUI();return;}}catch{}
}
emojiProg={loaded:0,total:EMOJI_COUNT,done:false,cached:false};updEmojiUI();
const all=[],BATCH=8;
for(let b=0;b<EMOJI_URL_GROUPS.length;b+=BATCH){
const chunk=EMOJI_URL_GROUPS.slice(b,b+BATCH);
const res=await Promise.allSettled(chunk.map(g=>fetchFast(g)));
res.forEach(r=>{if(r.status==='fulfilled'&&r.value)all.push(r.value);emojiProg.loaded++;});
updEmojiUI();
if(all.length>0){allEmojis=toArray(Object.assign({},...all),'emoji');if(activeTab==='emojis'&&activeEmojiSub==='custom'&&!_rendering)showEmojiPage(currentEmojiPage);}
}
if(all.length>0){const m=Object.assign({},...all);allEmojis=toArray(m,'emoji');try{GM_Set(K.emojiCache,JSON.stringify(m));GM_Set(K.emojiCacheTs,String(Date.now()));}catch{}}
emojiProg.done=true;updEmojiUI();
}
function updEmojiUI(){
if(!eSt)return;
const{loaded,total,done,cached}=emojiProg;
eSt.innerHTML=done?`<i class="fa-solid fa-circle-check"></i> ${allEmojis.length}${cached?' (cached)':''}`:`<i class="fa-solid fa-spinner fa-spin"></i> ${loaded}/${total}`;
eSt.className='badge '+(done?'badge-ok':'badge-warn');
}
async function loadStickers(){try{const r=await Promise.all(STICKERS_JSON_URLS.map(u=>fetchJson(u)));allStickers=toArray(Object.assign({},...r.filter(Boolean)),'sticker');}catch(e){console.error('Stickers:',e);}stickersLoaded=true;}
async function loadLocalGifs(){
const urls=String(S.localGifUrls||'').split(/\r?\n|,/).map(s=>s.trim()).filter(Boolean);
if(!urls.length){allGifs=[];gifsLoaded=true;localGifStatus='No packs';return;}
try{const r=await Promise.allSettled(urls.map(u=>fetchJson(u)));const ok=r.filter(r=>r.status==='fulfilled').map(r=>r.value);allGifs=toArray(Object.assign({},...ok),'gif');gifsLoaded=true;localGifStatus=`${ok.length}/${urls.length} loaded`;}
catch{allGifs=[];gifsLoaded=true;localGifStatus='Failed';}
}
// ═══════════════════════════════════════════════
// EMOJI SUB-CATEGORY DATA
// ═══════════════════════════════════════════════
function getSubData(){
if(activeEmojiSub==='custom')return allEmojis;
let items=[],idx=0;
if(activeEmojiSub==='emojipedia'){
Object.entries(EMOJIPEDIA).forEach(([grp,emojis])=>{
if(!emojis)return;
emojis.forEach(e=>{if(e)items.push({name:e,url:'',text:e,isText:true,tags:[grp.toLowerCase()],type:'emoji',originalIndex:idx++,animated:false});});
});
} else if(activeEmojiSub==='symbols'){
Object.entries(SYMBOL_DATA).forEach(([grp,emojis])=>{
if(!emojis)return;
emojis.forEach(e=>{if(e)items.push({name:e,url:'',text:e,isText:true,tags:[grp.toLowerCase()],type:'emoji',originalIndex:idx++,animated:false});});
});
} else if(activeEmojiSub==='flags'){
if(FLAG_DATA)FLAG_DATA.forEach((e,i)=>{if(e)items.push({name:e,url:'',text:e,isText:true,tags:['flag'],type:'emoji',originalIndex:i,animated:false});});
} else if(activeEmojiSub==='kaomoji'){
Object.entries(KAOMOJI).forEach(([grp,kms])=>{
kms.forEach(k=>{items.push({name:k,url:'',text:k,isText:true,tags:[grp.toLowerCase(),'kaomoji'],type:'emoji',originalIndex:idx++,animated:false});});
});
} else if(activeEmojiSub==='ascii'){
ASCII_ART.forEach((a,i)=>{items.push({name:a,url:'',text:a,isText:true,tags:['ascii'],type:'emoji',originalIndex:i,animated:false});});
}
return items;
}
function buildSubTabs(){
eSubs.innerHTML='';
Object.entries(EMOJI_CATS).forEach(([id,cat])=>{
const b=document.createElement('div');
b.className='sub-tab'+(activeEmojiSub===id?' active':'');
b.innerHTML=`<i class="fa-solid ${cat.icon}"></i> ${cat.label}`;
b.title=cat.desc;
b.addEventListener('click',()=>{activeEmojiSub=id;S.emojiSubTab=id;saveS();showEmojiPage(1);});
eSubs.appendChild(b);
});
}
// ═══════════════════════════════════════════════
// EMOJI PAGE (FIXED - guard against re-entrant)
// ═══════════════════════════════════════════════
function showEmojiPage(page){
if(_rendering)return;
_rendering=true;
try{
activeTab='emojis';setTabUI('emojis');hideAll();
tbE.style.display='';eSubs.style.display='';sRow.style.display='';gE.style.display='';
buildSubTabs();
const pp=S.emojiPerPage||108;
const term=searchIn.value.trim().toLowerCase();
let data=getSubData();
if(term)data=data.filter(e=>e.name.toLowerCase().includes(term)||(e.tags&&e.tags.some(t=>String(t).toLowerCase().includes(term))));
if(favOnlyEmoji)data=data.filter(e=>isFav('emoji',e));
if(activeEmojiSub==='custom')data=sortFavFirst(data,'emoji');
const{totalPages,page:p}=ensurePage(data.length,pp,page);
currentEmojiPage=p;
const items=data.slice((p-1)*pp,p*pp);
const frag=document.createDocumentFragment();
gE.innerHTML='';
items.forEach(item=>{
const tile=document.createElement('div');tile.className='et';
if(item.isText){
const span=document.createElement('span');span.className='te';span.textContent=item.text||item.name;
if(S.showTooltips)span.title=item.name;span.style.cursor='pointer';
span.addEventListener('click',e=>{e.stopPropagation();insertContent(item);tile.style.opacity='.5';setTimeout(()=>tile.style.opacity='1',180);});
span.addEventListener('contextmenu',e=>{
e.preventDefault();e.stopPropagation();
showCtx([
{icon:'fa-copy',label:'Copy',onClick:()=>{copyText(item.text||item.name);notice('Copied!');}},
{icon:'fa-star',label:isFav('emoji',item)?'Unfavorite':'Favorite',onClick:()=>{favToggle('emoji',item);showEmojiPage(currentEmojiPage);}}
],e.clientX,e.clientY);
});
tile.appendChild(span);
} else {
const img=mkImg(item.url,item.name);
img.addEventListener('click',e=>{e.stopPropagation();insertContent(item);tile.style.opacity='.5';setTimeout(()=>tile.style.opacity='1',180);});
img.addEventListener('contextmenu',e=>{
e.preventDefault();e.stopPropagation();
showCtx([
{icon:'fa-star',label:isFav('emoji',item)?'Unfavorite':'Favorite',onClick:()=>{favToggle('emoji',item);showEmojiPage(currentEmojiPage);}},
{icon:'fa-link',label:'Copy URL',onClick:()=>{copyText(item.url);notice('Copied!');}},
{icon:'fa-code',label:'Copy BBCode',onClick:()=>{copyText(`[img=${S.emojiSize}]${item.url}[/img]`);notice('Copied!');}}
],e.clientX,e.clientY);
});
tile.appendChild(img);
const star=document.createElement('button');star.className='ssm'+(isFav('emoji',item)?' active':'');
star.innerHTML='<i class="fa-solid fa-star"></i>';
star.addEventListener('click',e=>{e.preventDefault();e.stopPropagation();favToggle('emoji',item);showEmojiPage(currentEmojiPage);});
tile.appendChild(star);
}
frag.appendChild(tile);
});
gE.appendChild(frag);
if(!items.length)gE.innerHTML='<div class="empty"><i class="fa-solid fa-ghost"></i> No emojis found</div>';
renderPager(totalPages,currentEmojiPage,p=>showEmojiPage(p));
searchIn.placeholder=`Search ${EMOJI_CATS[activeEmojiSub]?.label||'emojis'}…`;
$('#e-fav').classList.toggle('active',favOnlyEmoji);
updEmojiUI();
} catch(err) {
console.error('[UE] showEmojiPage error:', err);
gE.innerHTML='<div class="empty"><i class="fa-solid fa-triangle-exclamation"></i> Render error</div>';
} finally {
_rendering=false;
}
}
// ═══════════════════════════════════════════════
// STICKER PAGE
// ═══════════════════════════════════════════════
function showStickerPage(page){
if(_rendering)return;_rendering=true;
try{
activeTab='stickers';setTabUI('stickers');hideAll();
tbS.style.display='';sRow.style.display='';gS.style.display='';
if(!stickersLoaded){gS.innerHTML='<div class="empty"><i class="fa-solid fa-spinner fa-spin"></i> Loading…</div>';return;}
const pp=S.stickerPerPage||24;const term=searchIn.value.trim().toLowerCase();
let list=term?allStickers.filter(e=>e.name.toLowerCase().includes(term)||(e.tags&&e.tags.some(t=>String(t).toLowerCase().includes(term)))):allStickers;
if(favOnlyStickers)list=list.filter(e=>isFav('sticker',e));
list=sortFavFirst(list,'sticker');
const{totalPages,page:p}=ensurePage(list.length,pp,page);
currentStickerPage=p;const items=list.slice((p-1)*pp,p*pp);
const frag=document.createDocumentFragment();gS.innerHTML='';
items.forEach(item=>{
const tile=document.createElement('div');tile.className='tl';
const img=mkImg(item.url,item.name);
img.addEventListener('click',e=>{e.stopPropagation();insertContent(item);tile.style.opacity='.5';setTimeout(()=>tile.style.opacity='1',180);});
img.addEventListener('contextmenu',e=>{e.preventDefault();e.stopPropagation();showCtx([{icon:'fa-star',label:isFav('sticker',item)?'Unfavorite':'Favorite',onClick:()=>{favToggle('sticker',item);showStickerPage(currentStickerPage);}},{icon:'fa-link',label:'Copy URL',onClick:()=>{copyText(item.url);notice('Copied!');}}],e.clientX,e.clientY);});
const star=document.createElement('button');star.className='star'+(isFav('sticker',item)?' active':'');star.innerHTML='<i class="fa-solid fa-star"></i>';
star.addEventListener('click',e=>{e.preventDefault();e.stopPropagation();favToggle('sticker',item);showStickerPage(currentStickerPage);});
tile.appendChild(img);tile.appendChild(star);frag.appendChild(tile);
});
gS.appendChild(frag);
if(!items.length)gS.innerHTML='<div class="empty"><i class="fa-solid fa-ghost"></i> No stickers</div>';
renderPager(totalPages,currentStickerPage,p=>showStickerPage(p));
searchIn.placeholder='Search stickers…';
$('#s-fav').classList.toggle('active',favOnlyStickers);
}catch(err){console.error('[UE] showStickerPage error:',err);}
finally{_rendering=false;}
}
// ═══════════════════════════════════════════════
// GIF PROVIDERS
// ═══════════════════════════════════════════════
const tenorCache=new Map();
async function fetchTenorPage(q,page,limit){
const key=getKey('tenor');if(!key)throw new Error('No Tenor API key');
const ck=`${q}::${limit}`;if(!tenorCache.has(ck))tenorCache.set(ck,{pages:new Map(),next:new Map()});
const st=tenorCache.get(ck);
if(st.pages.has(page))return{items:st.pages.get(page),hasNext:!!st.next.get(page)};
for(let p=1;p<=page;p++){
if(st.pages.has(p))continue;
const pos=p===1?null:st.next.get(p-1)||null;
const ep=q?'search':'featured';
const params=[`key=${encodeURIComponent(key)}`,`limit=${limit}`,'media_filter=gif','contentfilter=medium'];
if(q)params.push(`q=${encodeURIComponent(q)}`);if(pos)params.push(`pos=${encodeURIComponent(pos)}`);
const json=await fetchJson(`https://tenor.googleapis.com/v2/${ep}?${params.join('&')}`);
const items=(json?.results||[]).map((r,i)=>{const mf=r.media_formats||{};return{name:r.content_description||'tenor',url:mf.gif?.url||mf.mediumgif?.url||mf.tinygif?.url||'',tags:[],animated:true,type:'gif',originalIndex:i};}).filter(x=>x.url);
st.pages.set(p,items);st.next.set(p,json?.next||'');
}
return{items:st.pages.get(page)||[],hasNext:!!st.next.get(page)};
}
async function fetchGiphy({q,page,limit}){const key=getKey('giphy');if(!key)throw new Error('No GIPHY key');const off=(page-1)*limit;const ep=q?`search?api_key=${encodeURIComponent(key)}&q=${encodeURIComponent(q)}&limit=${limit}&offset=${off}&rating=pg-13`:`trending?api_key=${encodeURIComponent(key)}&limit=${limit}&offset=${off}&rating=pg-13`;const json=await fetchJson(`https://api.giphy.com/v1/gifs/${ep}`);const data=json?.data||[];const total=Math.min(json?.pagination?.total_count||data.length,5000);return{items:data.map((g,i)=>{const imgs=g.images||{};return{name:g.title||'giphy',url:imgs.downsized_medium?.url||imgs.original?.url||'',tags:[],animated:true,type:'gif',originalIndex:i};}).filter(x=>x.url),total};}
async function fetchReddit({q,page,limit}){const subs=String(S.redditSubs||'').split(/[, ]+/).filter(Boolean).slice(0,50).join('+')||'gifs';const pp=Math.min(limit*page,100);const url=q?`https://www.reddit.com/r/${subs}/search.json?q=${encodeURIComponent(q)}&restrict_sr=1&sort=relevance&t=year&limit=${pp}&raw_json=1`:`https://www.reddit.com/r/${subs}/hot.json?limit=${pp}&raw_json=1`;const json=await fetchJson(url);const ai=!!S.redditIncludeImages;const list=(json?.data?.children||[]).map((ch,i)=>{const d=ch?.data||{},u=d.url_overridden_by_dest||d.url||'';if(/\.gif($|\?)/i.test(u)||(ai&&/\.(jpe?g|png|webp)($|\?)/i.test(u)))return{name:d.title||'reddit',url:u,tags:[],animated:/\.gif/i.test(u),type:'gif',originalIndex:i};return null;}).filter(Boolean);const s=(page-1)*limit;return{items:list.slice(s,s+limit),total:list.length};}
async function fetchTumblr({q,page,limit}){const key=getKey('tumblr');if(!key)throw new Error('No Tumblr key');const json=await fetchJson(`https://api.tumblr.com/v2/tagged?tag=${encodeURIComponent(q||'gif')}&api_key=${encodeURIComponent(key)}&limit=${limit}`);return{items:(json?.response||[]).flatMap((p,i)=>(p.photos||[]).filter(ph=>/\.gif($|\?)/i.test(ph?.original_size?.url||'')).map(ph=>({name:p.summary||'tumblr',url:ph.original_size.url,tags:[],animated:true,type:'gif',originalIndex:i}))).slice(0,limit),total:page*limit+limit};}
async function fetchImgur({q,page,limit}){const cid=getKey('imgur');if(!cid)throw new Error('No Imgur Client-ID');const h={Authorization:`Client-ID ${cid}`};const url=q?`https://api.imgur.com/3/gallery/search/?q=${encodeURIComponent(q)}`:`https://api.imgur.com/3/gallery/hot/viral/${Math.max(0,page-1)}.json`;const data=await fetchJson(url,h);const list=(data?.data||[]).flatMap(e=>{if(e.images?.length)return e.images.filter(i=>i.type==='image/gif').map(i=>i.link);if(e.type==='image/gif'&&e.link)return[e.link];return[];});const s=q?(page-1)*limit:0;return{items:list.slice(s,s+limit).map((u,i)=>({name:'imgur',url:u,tags:[],animated:true,type:'gif',originalIndex:i})),total:list.length};}
function provOn(p){return p==='local'||p==='reddit'||!!S['prov'+p.charAt(0).toUpperCase()+p.slice(1)];}
// ═══════════════════════════════════════════════
// GIF PAGE (FIXED)
// ═══════════════════════════════════════════════
function mkGifTile(item,parent){
const tile=document.createElement('div');tile.className='tl';
const img=mkImg(item.url,item.name);
if(S.gifShowTitles){const t=document.createElement('div');t.className='gt';t.textContent=item.name;tile.appendChild(t);}
if(S.showGifStar){
const star=document.createElement('button');star.className='star'+(isFav('gif',item)?' active':'');star.innerHTML='<i class="fa-solid fa-star"></i>';
star.addEventListener('click',e=>{e.preventDefault();e.stopPropagation();favToggle('gif',item);showGifPage(currentGifPage);});
tile.appendChild(star);
}
img.addEventListener('click',e=>{e.stopPropagation();insertContent(item);tile.style.opacity='.5';setTimeout(()=>tile.style.opacity='1',180);});
img.addEventListener('contextmenu',e=>{e.preventDefault();e.stopPropagation();showCtx([{icon:'fa-link',label:'Copy URL',onClick:()=>{copyText(item.url);notice('Copied!');}},{icon:'fa-star',label:isFav('gif',item)?'Unfavorite':'Favorite',onClick:()=>{favToggle('gif',item);showGifPage(currentGifPage);}}],e.clientX,e.clientY);});
tile.appendChild(img);parent.appendChild(tile);
}
async function showGifPage(page){
if(_rendering)return;_rendering=true;
try{
activeTab='gifs';setTabUI('gifs');hideAll();
tbG.style.display='';sRow.style.display='';gG.style.display='';
tbG.querySelectorAll('.pb').forEach(b=>{b.style.display=provOn(b.dataset.p)?'':'none';b.classList.toggle('active',b.dataset.p===S.gifProvider);});
const pp=Math.max(1,S.gifPerPage||24);
gG.innerHTML='<div class="empty"><i class="fa-solid fa-spinner fa-spin"></i> Loading…</div>';
if(favOnlyGifs){
const q=searchIn.value.trim().toLowerCase();
let fvs=getFavList('gif');if(q)fvs=fvs.filter(x=>(x.name||'').toLowerCase().includes(q));
const{totalPages,page:p}=ensurePage(fvs.length,pp,page);currentGifPage=p;
gG.innerHTML='';const items=fvs.slice((p-1)*pp,p*pp);
if(!items.length)gG.innerHTML='<div class="empty"><i class="fa-solid fa-heart-crack"></i> No favorites</div>';
else items.forEach(i=>mkGifTile(i,gG));
renderPager(totalPages,currentGifPage,p=>showGifPage(p));
searchIn.placeholder='Search favorites…';$('#g-fav').classList.toggle('active',true);
return;
}
$('#g-fav').classList.toggle('active',false);
const q=searchIn.value.trim();
let items=[],totalPages=1;
if(S.gifProvider==='local'){
if(!gifsLoaded)await loadLocalGifs();
if(!String(S.localGifUrls||'').trim()){gG.innerHTML='<div class="empty"><i class="fa-solid fa-folder-open"></i> Add GIF packs in Settings</div>';renderPager(1,1,()=>{});return;}
const term=q.toLowerCase();let list=term?allGifs.filter(e=>e.name.toLowerCase().includes(term)||(e.tags&&e.tags.some(t=>String(t).toLowerCase().includes(term)))):allGifs;
list=sortFavFirst(list,'gif');
const r=ensurePage(list.length,pp,page);totalPages=r.totalPages;currentGifPage=r.page;items=list.slice((r.page-1)*pp,r.page*pp);
} else if(S.gifProvider==='tenor'){
const{items:ti,hasNext}=await fetchTenorPage(q,page,pp);currentGifPage=page;items=ti;totalPages=page+(hasNext?1:0);
} else if(S.gifProvider==='giphy'){
const r=await fetchGiphy({q,page,limit:pp});currentGifPage=page;items=r.items;totalPages=Math.max(1,Math.ceil(r.total/pp));
} else if(S.gifProvider==='imgur'){
const r=await fetchImgur({q,page,limit:pp});currentGifPage=page;items=r.items;totalPages=Math.max(1,Math.ceil(r.total/pp));
} else if(S.gifProvider==='reddit'){
const r=await fetchReddit({q,page,limit:pp});currentGifPage=page;items=r.items;totalPages=Math.max(1,Math.ceil(r.total/pp));
} else if(S.gifProvider==='tumblr'){
const r=await fetchTumblr({q,page,limit:pp});currentGifPage=page;items=r.items;totalPages=Math.max(1,Math.ceil(r.total/pp));
}
gG.innerHTML='';
if(!items.length)gG.innerHTML='<div class="empty"><i class="fa-solid fa-ghost"></i> No results</div>';
else items.forEach(i=>mkGifTile(i,gG));
renderPager(totalPages,currentGifPage,p=>showGifPage(p));
searchIn.placeholder=`Search GIFs — ${S.gifProvider}`;
}catch(e){
gG.innerHTML=`<div class="empty"><i class="fa-solid fa-triangle-exclamation"></i> ${e.message}<br><span style="opacity:.5;font-size:10px">Check API keys in Settings</span></div>`;
renderPager(1,1,()=>{});
}finally{_rendering=false;}
}
// ═══════════════════════════════════════════════
// RECENT
// ═══════════════════════════════════════════════
function showRecent(){
activeTab='recent';setTabUI('recent');hideAll();hPanel.style.display='block';
hPanel.innerHTML=`<div class="section"><h3><i class="fa-solid fa-clock-rotate-left"></i> Recently Used</h3>`;
if(!recentItems.length){hPanel.innerHTML+='<div style="opacity:.6;padding:10px">Nothing here yet.</div></div>';return;}
const grid=document.createElement('div');grid.className='recent-grid';
recentItems.forEach(item=>{
if(item.isText||!item.url){
const span=document.createElement('div');span.className='ri';span.textContent=item.text||item.name||'?';
span.addEventListener('click',()=>insertContent(item));grid.appendChild(span);
} else {
const img=document.createElement('img');img.src=item.url;img.alt=item.name||'';
img.addEventListener('click',()=>insertContent(item));grid.appendChild(img);
}
});
hPanel.querySelector('.section').appendChild(grid);
const cb=document.createElement('button');cb.className='btn warn';cb.innerHTML='<i class="fa-solid fa-trash"></i> Clear';cb.style.marginTop='6px';
cb.addEventListener('click',()=>{if(!confirm('Clear recent?'))return;recentItems=[];GM_Set(K.recent,'[]');showRecent();});
hPanel.querySelector('.section').appendChild(cb);
}
// ═══════════════════════════════════════════════
// HOME
// ═══════════════════════════════════════════════
async function showHome(){
activeTab='home';setTabUI('home');hideAll();hPanel.style.display='block';
const pk=p=>hasKey(p)?'<span class="badge-ok"><i class="fa-solid fa-circle-check"></i> saved</span>':'<span class="badge-err"><i class="fa-solid fa-circle-xmark"></i> not set</span>';
hPanel.innerHTML=`
<div class="section"><h3><i class="fa-solid fa-chart-line"></i> Live Stats</h3>
<div class="home-grid">
<div class="home-card"><h4><i class="fa-solid fa-download"></i> Installs</h4><div id="h-inst"><i class="fa-solid fa-spinner fa-spin"></i></div></div>
<div class="home-card"><h4><i class="fa-solid fa-code-branch"></i> Version</h4><div>Local: <b>v${VERSION}</b></div><div id="h-rem">Remote: <i class="fa-solid fa-spinner fa-spin"></i></div></div>
<div class="home-card"><h4><i class="fa-solid fa-calendar-check"></i> Updated</h4><div id="h-upd"><i class="fa-solid fa-spinner fa-spin"></i></div></div>
</div>
<div class="row" style="margin-top:8px">
<a class="link-btn" href="${LINKS.github}" target="_blank"><i class="fa-brands fa-github"></i> GitHub</a>
<a class="link-btn" href="${LINKS.openuserjs}" target="_blank"><i class="fa-solid fa-globe"></i> OpenUserJS</a>
<a class="link-btn" href="${LINKS.download}" target="_blank"><i class="fa-solid fa-download"></i> Download</a>
<button class="btn sec" id="h-ref"><i class="fa-solid fa-arrows-rotate"></i> Refresh</button>
</div></div>
<div class="section"><h3><i class="fa-solid fa-cubes"></i> Content</h3>
<div class="home-grid">
<div class="home-card"><h4><i class="fa-solid fa-face-smile"></i> Emojis</h4>
<div class="chip">Custom: ${allEmojis.length}</div>
<div class="chip">Fav: ${_favV2.emoji.size}</div>
</div>
<div class="home-card"><h4><i class="fa-solid fa-note-sticky"></i> Other</h4>
<div class="chip">Stickers: ${stickersLoaded?allStickers.length:'…'}</div>
<div class="chip">Local GIFs: ${localGifStatus||'–'}</div>
<div class="chip">Recent: ${recentItems.length}</div>
</div>
<div class="home-card"><h4><i class="fa-solid fa-plug"></i> Providers</h4>
<div>Tenor: ${pk('tenor')}</div><div>GIPHY: ${pk('giphy')}</div><div>Imgur: ${pk('imgur')}</div><div>Tumblr: ${pk('tumblr')}</div>
</div></div></div>`;
hPanel.querySelector('#h-ref')?.addEventListener('click',async()=>{GM_Set(K.statsCacheTs,'0');showHome();});
// Fetch stats
try{
const ts=Number(GM_Get(K.statsCacheTs,'0'));
let stats;
if(Date.now()-ts<STATS_TTL){try{stats=JSON.parse(GM_Get(K.statsCache,'null'));}catch{}}
if(!stats){
try{
const txt=await fetchText(LINKS.openuserjs);
const installs=txt.match(/Installs[:\s]*<[^>]*>(\d[\d,]*)/i)?.[1]?.replace(/,/g,'')||null;
const updated=txt.match(/Updated[:\s]*<[^>]*>([^<]+)/i)?.[1]?.trim()||null;
let rv=null;try{const mt=await fetchText(LINKS.meta);rv=mt.match(/@version\s+([^\r\n]+)/)?.[1]?.trim();}catch{}
stats={installs:installs||'?',updated:updated||'?',remoteVersion:rv||'?'};
GM_Set(K.statsCache,JSON.stringify(stats));GM_Set(K.statsCacheTs,String(Date.now()));
}catch{stats={installs:'?',updated:'?',remoteVersion:'?'};}
}
const hi=hPanel.querySelector('#h-inst'),hr=hPanel.querySelector('#h-rem'),hu=hPanel.querySelector('#h-upd');
if(hi)hi.innerHTML=`<span class="stat-num">${stats.installs}</span> installs`;
if(hr){const isNew=stats.remoteVersion&&stats.remoteVersion!=='?'&&stats.remoteVersion!==VERSION;hr.innerHTML=`Remote: <b>v${stats.remoteVersion}</b> ${isNew?'<span class="badge-warn"><i class="fa-solid fa-arrow-up"></i> Update!</span>':'<span class="badge-ok"><i class="fa-solid fa-check"></i></span>'}`;}
if(hu)hu.innerHTML=`<i class="fa-solid fa-clock"></i> ${stats.updated}`;
}catch{}
}
// ═══════════════════════════════════════════════
// SETTINGS (EXPANDED)
// ═══════════════════════════════════════════════
function showSettings(){
activeTab='settings';setTabUI('settings');hideAll();sPanel.style.display='block';
sPanel.innerHTML=`
<div class="section"><h3><i class="fa-solid fa-sliders"></i> General</h3>
<div class="row"><label class="cb"><input type="checkbox" id="x-close" ${S.closeOnInsert?'checked':''}> Close on insert</label></div>
<div class="row"><label class="cb"><input type="checkbox" id="x-compact" ${S.compactMode?'checked':''}> Compact mode</label></div>
<div class="row"><label class="cb"><input type="checkbox" id="x-tips" ${S.showTooltips?'checked':''}> Tooltips</label></div>
<div class="row"><label class="cb"><input type="checkbox" id="x-gstar" ${S.showGifStar?'checked':''}> GIF star</label></div>
<div class="row"><label class="cb"><input type="checkbox" id="x-gtitles" ${S.gifShowTitles?'checked':''}> GIF titles</label></div>
</div>
<div class="section"><h3><i class="fa-solid fa-palette"></i> Appearance</h3>
<div style="opacity:.6;font-size:10px;margin-bottom:6px">Customize the look and feel of the menu.</div>
<h4 style="margin:6px 0 4px;color:var(--acc)"><i class="fa-solid fa-gem"></i> Menu</h4>
<div class="row"><label style="min-width:100px">Width</label><input type="number" class="mini" id="s-mw" value="${S.menuWidth}"> px</div>
<div class="row"><label style="min-width:100px">Height</label><input type="number" class="mini" id="s-mh" value="${S.menuHeightPx}"> px</div>
<div class="row"><label style="min-width:100px">Border Radius</label><input type="number" class="mini" id="s-mr" value="${S.menuRadius}"> px</div>
<div class="row"><label style="min-width:100px">Border Width</label><input type="number" class="mini" id="s-bw" value="${S.borderWidth}"> px</div>
<div class="row"><label style="min-width:100px">Shadow</label><input type="range" min="0" max="1" step="0.1" id="s-sh" value="${S.shadow}"><span class="rv">${S.shadow}</span></div>
<div class="row"><label style="min-width:100px">Backdrop Blur</label><input type="number" class="mini" id="s-blur" value="${S.backdropBlur}"> px</div>
<h4 style="margin:8px 0 4px;color:var(--acc)"><i class="fa-solid fa-droplet"></i> Colors</h4>
<div class="row"><label style="min-width:100px">Background</label><input type="color" id="s-bg" value="${S.bg}"></div>
<div class="row"><label style="min-width:100px">Text</label><input type="color" id="s-txt" value="${S.text}"></div>
<div class="row"><label style="min-width:100px">Accent</label><input type="color" id="s-acc" value="${S.accent}"></div>
<div class="row"><label style="min-width:100px">Border</label><input type="color" id="s-bc" value="${S.borderColor}"></div>
<div class="row"><label style="min-width:100px">Tile BG</label><input type="color" id="s-tbg" value="${S.tileBg}"></div>
<h4 style="margin:8px 0 4px;color:var(--acc)"><i class="fa-solid fa-grip"></i> Grid & Tiles</h4>
<div class="row"><label style="min-width:100px">Emoji Size</label><input type="number" class="mini" id="s-es" value="${S.emojiSize}"> px</div>
<div class="row"><label style="min-width:100px">GIF Height</label><input type="number" class="mini" id="s-gs" value="${S.gifSize}"> px</div>
<div class="row"><label style="min-width:100px">Emoji Gap</label><input type="number" class="mini" id="s-ge" value="${S.gapEmoji}"> px</div>
<div class="row"><label style="min-width:100px">GIF Gap</label><input type="number" class="mini" id="s-gl" value="${S.gapLarge}"> px</div>
<div class="row"><label style="min-width:100px">Tile Radius</label><input type="number" class="mini" id="s-tr" value="${S.tileRadius}"> px</div>
<div class="row"><label style="min-width:100px">Hover Scale</label><input type="range" min="1" max="1.2" step="0.01" id="s-hs" value="${S.hoverScale}"><span class="rv">${S.hoverScale}</span></div>
<h4 style="margin:8px 0 4px;color:var(--acc)"><i class="fa-solid fa-grip-lines-vertical"></i> Scrollbar</h4>
<div class="row"><label style="min-width:100px">Width</label><input type="number" class="mini" id="s-sbw" value="${S.scrollbarWidth}"> px</div>
<div class="row"><label style="min-width:100px">Thumb Color</label><input type="color" id="s-sbth" value="${S.scrollbarThumb}"></div>
<div class="row"><label style="min-width:100px">Track Color</label><input type="color" id="s-sbt" value="${S.scrollbarTrack}"></div>
<div class="row" style="margin-top:10px"><button class="btn" id="s-apply"><i class="fa-solid fa-check"></i> Apply Changes</button></div>
</div>
<div class="section"><h3><i class="fa-solid fa-code"></i> Insert Format</h3>
<div class="row"><label style="min-width:100px">Format</label><select class="sel" id="s-fmt">
<option value="bbcode" ${S.insertFormat==='bbcode'?'selected':''}>BBCode</option>
<option value="markdown" ${S.insertFormat==='markdown'?'selected':''}>Markdown</option>
<option value="html" ${S.insertFormat==='html'?'selected':''}>HTML</option>
<option value="url" ${S.insertFormat==='url'?'selected':''}>URL Only</option>
</select></div>
</div>
<div class="section"><h3><i class="fa-solid fa-key"></i> API Keys</h3>
<div style="opacity:.6;font-size:10px;margin-bottom:6px">Stored locally only</div>
${['tenor','giphy','imgur','tumblr'].map(p=>`<div class="row"><label style="min-width:80px">${p}</label><span class="api-disp ${hasKey(p)?'has':''}">${hasKey(p)?maskK(getKey(p)):'Not set'}</span>
<button class="btn sec" data-ed="${p}"><i class="fa-solid fa-pen"></i></button>
<button class="btn" data-ts="${p}"><i class="fa-solid fa-vial"></i></button>
<span class="badge" id="st-${p}">${hasKey(p)?'OK':''}</span></div>
<div id="er-${p}" style="display:none" class="row"><input type="password" class="api-key" id="ki-${p}" autocomplete="off" placeholder="Paste key…">
<button class="btn" data-sv="${p}"><i class="fa-solid fa-check"></i></button>
<button class="btn sec" data-cn="${p}"><i class="fa-solid fa-xmark"></i></button>
<button class="btn warn" data-dl="${p}"><i class="fa-solid fa-trash"></i></button></div>`).join('')}
</div>
<div class="section"><h3><i class="fa-solid fa-hard-drive"></i> Local GIFs</h3>
<textarea class="area" id="x-lg">${S.localGifUrls||''}</textarea>
<div class="row"><button class="btn" id="x-slg"><i class="fa-solid fa-check"></i> Save</button><span class="badge" id="x-lgst"></span></div>
</div>
<div class="section"><h3><i class="fa-solid fa-triangle-exclamation"></i> Danger</h3>
<div class="row"><button class="btn warn" id="x-ra"><i class="fa-solid fa-rotate-left"></i> Reset Appearance</button>
<button class="btn warn" id="x-rall"><i class="fa-solid fa-bomb"></i> Reset ALL</button>
<button class="btn sec" id="x-cc"><i class="fa-solid fa-broom"></i> Clear Cache</button></div>
</div>`;
// Bind General
const bk=(id,key,cb2)=>{const el=sPanel.querySelector('#'+id);if(el)el.addEventListener('change',()=>{S[key]=el.checked;saveS();cb2?.();});};
bk('x-close','closeOnInsert');bk('x-compact','compactMode',()=>applyVars());bk('x-tips','showTooltips');bk('x-gstar','showGifStar');bk('x-gtitles','gifShowTitles');
// Bind Appearance Inputs
const bindNum = (id, key) => {
const el = sPanel.querySelector('#'+id);
if(el) el.addEventListener('change', () => {
let v = parseFloat(el.value);
if(!isNaN(v)){ S[key] = v; saveS(); applyVars(); if(el.nextElementSibling && el.nextElementSibling.classList.contains('rv')) el.nextElementSibling.textContent = v; }
});
};
const bindColor = (id, key) => {
const el = sPanel.querySelector('#'+id);
if(el) el.addEventListener('input', () => { S[key] = el.value; saveS(); applyVars(); });
};
// Menu
bindNum('s-mw', 'menuWidth');
bindNum('s-mh', 'menuHeightPx');
bindNum('s-mr', 'menuRadius');
bindNum('s-bw', 'borderWidth');
bindNum('s-blur', 'backdropBlur');
// Range for Shadow
const shEl = sPanel.querySelector('#s-sh');
if(shEl) shEl.addEventListener('input', (e) => {
let v = parseFloat(e.target.value);
S.shadow = v; saveS(); applyVars();
e.target.nextElementSibling.textContent = v;
});
// Colors
bindColor('s-bg', 'bg');
bindColor('s-txt', 'text');
bindColor('s-acc', 'accent');
bindColor('s-bc', 'borderColor');
bindColor('s-tbg', 'tileBg');
// Grid
bindNum('s-es', 'emojiSize');
bindNum('s-gs', 'gifSize');
bindNum('s-ge', 'gapEmoji');
bindNum('s-gl', 'gapLarge');
bindNum('s-tr', 'tileRadius');
// Range for Hover Scale
const hsEl = sPanel.querySelector('#s-hs');
if(hsEl) hsEl.addEventListener('input', (e) => {
let v = parseFloat(e.target.value);
S.hoverScale = v; saveS(); applyVars();
e.target.nextElementSibling.textContent = v;
});
// Scrollbar
bindNum('s-sbw', 'scrollbarWidth');
bindColor('s-sbth', 'scrollbarThumb');
bindColor('s-sbt', 'scrollbarTrack');
sPanel.querySelector('#s-apply')?.addEventListener('click', ()=>{ applyVars(); notice('Theme updated'); });
// Bind Format
const fmtSel = sPanel.querySelector('#s-fmt');
if(fmtSel) fmtSel.addEventListener('change', () => { S.insertFormat = fmtSel.value; saveS(); });
// Bind API
['tenor','giphy','imgur','tumblr'].forEach(p=>{
sPanel.querySelector(`[data-ed="${p}"]`)?.addEventListener('click',()=>{const r=sPanel.querySelector(`#er-${p}`);if(r)r.style.display='flex';});
sPanel.querySelector(`[data-cn="${p}"]`)?.addEventListener('click',()=>{const r=sPanel.querySelector(`#er-${p}`);if(r)r.style.display='none';});
sPanel.querySelector(`[data-sv="${p}"]`)?.addEventListener('click',()=>{const v=sPanel.querySelector(`#ki-${p}`)?.value?.trim();if(!v)return;setKey(p,v);showSettings();notice(`${p} key saved`);});
sPanel.querySelector(`[data-dl="${p}"]`)?.addEventListener('click',()=>{if(!confirm(`Delete ${p} key?`))return;setKey(p,'');showSettings();notice(`${p} key deleted`);});
sPanel.querySelector(`[data-ts="${p}"]`)?.addEventListener('click',async()=>{
const st=sPanel.querySelector(`#st-${p}`);if(st){st.textContent='…';st.className='badge badge-warn';}
try{const k=sPanel.querySelector(`#ki-${p}`)?.value?.trim()||getKey(p);if(!k)throw 0;let ok=false;
if(p==='tenor')ok=!!(await fetchJson(`https://tenor.googleapis.com/v2/featured?key=${encodeURIComponent(k)}&limit=1&media_filter=gif`))?.results;
if(p==='giphy')ok=!!(await fetchJson(`https://api.giphy.com/v1/gifs/trending?api_key=${encodeURIComponent(k)}&limit=1`))?.data;
if(p==='imgur')ok=!!(await fetchJson('https://api.imgur.com/3/credits',{Authorization:`Client-ID ${k}`}))?.data;
if(p==='tumblr')ok=!!(await fetchJson(`https://api.tumblr.com/v2/tagged?tag=gif&api_key=${encodeURIComponent(k)}&limit=1`))?.response;
if(st){st.textContent=ok?'Working':'Fail';st.className=`badge ${ok?'badge-ok':'badge-err'}`;}
}catch{if(st){st.textContent='Fail';st.className='badge badge-err';}}
});
});
sPanel.querySelector('#x-slg')?.addEventListener('click',async()=>{S.localGifUrls=sPanel.querySelector('#x-lg')?.value?.trim()||'';saveS();gifsLoaded=false;await loadLocalGifs();const st=sPanel.querySelector('#x-lgst');if(st){st.textContent=localGifStatus;st.className='badge badge-ok';}});
sPanel.querySelector('#x-ra')?.addEventListener('click',()=>{if(!confirm('Reset appearance?'))return;Object.assign(S,DEFAULTS);saveS();applyVars();showSettings();});
sPanel.querySelector('#x-rall')?.addEventListener('click',()=>{
if(!confirm('Reset EVERYTHING?'))return;
Object.assign(S,DEFAULTS);saveS();applyVars();
_favV2.emoji.clear();_favV2.sticker.clear();_favV2.gif.clear();
_favLeg.emoji.clear();_favLeg.sticker.clear();_favLeg.gif.clear();
favMeta={};
Object.values(K).forEach(k=>{try{GM_Set(k,k.includes('Meta')?'{}':'[]');}catch{}});
saveFavMeta();writeApi('global',{});writeApi('site',{});
applyCustomCss('');recentItems=[];GM_Set(K.recent,'[]');
GM_Set(K.emojiCache,'');GM_Set(K.emojiCacheTs,'0');
showSettings();notice('Everything reset');
});
sPanel.querySelector('#x-cc')?.addEventListener('click',()=>{GM_Set(K.emojiCache,'');GM_Set(K.emojiCacheTs,'0');notice('Cache cleared');});
}
// ═══════════════════════════════════════════════
// TAB SWITCHING
// ═══════════════════════════════════════════════
function switchTab(t){
activeTab=t;
if(t==='home')showHome();
else if(t==='emojis')showEmojiPage(currentEmojiPage);
else if(t==='stickers')showStickerPage(currentStickerPage);
else if(t==='gifs')showGifPage(currentGifPage);
else if(t==='recent')showRecent();
else if(t==='settings')showSettings();
}
// ═══════════════════════════════════════════════
// SEARCH
// ═══════════════════════════════════════════════
searchIn.addEventListener('input',debounce(()=>{
if(activeTab==='emojis')showEmojiPage(1);
else if(activeTab==='stickers')showStickerPage(1);
else if(activeTab==='gifs')showGifPage(1);
},S.searchDebounceMs||200));
pjGo.addEventListener('click',()=>{const v=parseInt(pjIn.value,10)||1;if(activeTab==='emojis')showEmojiPage(v);else if(activeTab==='stickers')showStickerPage(v);else if(activeTab==='gifs')showGifPage(v);});
pjIn.addEventListener('keydown',e=>{if(e.key==='Enter')pjGo.click();});
// Fav buttons
$('#e-fav').addEventListener('click',e=>{e.stopPropagation();favOnlyEmoji=!favOnlyEmoji;showEmojiPage(1);});
$('#s-fav').addEventListener('click',e=>{e.stopPropagation();favOnlyStickers=!favOnlyStickers;showStickerPage(1);});
$('#g-fav').addEventListener('click',e=>{e.stopPropagation();favOnlyGifs=!favOnlyGifs;showGifPage(1);});
// GIF providers
tbG.addEventListener('click',e=>{
const b=e.target.closest('.pb');if(!b)return;
e.stopPropagation();
const p=b.dataset.p;if(!provOn(p))return;
if(['tenor','giphy','imgur','tumblr'].includes(p)&&!hasKey(p)){switchTab('settings');notice(`Add ${p} key in Settings`);return;}
S.gifProvider=p;saveS();showGifPage(1);
});
// ═══════════════════════════════════════════════
// FLOATING BUTTON
// ═══════════════════════════════════════════════
const BE=[ '😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣', '😊', '😇', '🙂', '🙃', '😉', '😌', '😍', '🥰', '😘', '😗', '😙', '😚', '😋', '😛', '😝', '😜', '🤪', '🤨', '🧐', '🤓', '😎', '🥸', '🤩', '🥳', '😏', '😒', '😞', '😔', '😟', '😕', '🙁', '☹️', '😣', '😖', '😫', '😩', '🥺', '😢', '😭', '😤', '😠', '😡', '🤬', '🤯', '😳', '🥵', '🥶', '😱', '😨', '😰', '😥', '😓', '🫣', '🫡', '🤗', '🤔', '🫢', '🤭', '🤫', '🤥', '😶', '😐', '😑', '😬', '🫨', '🫠', '🙄', '😯', '😦', '😧', '😮', '😲', '🥱', '😴', '🤤', '😪', '😵', '😵💫', '🫥', '🤐', '🥴', '🤢', '🤮', '🤧', '😷', '🤒', '🤕', '🤑', '🤠', '😈', '👿', '👹', '👺', '🤡', '💩', '👻', '💀', '☠️', '👽', '👾', '🤖', '🐵', '🐒', '🐶', '🐕', '🐩', '🐱', '🐈', '🦁', '🐯', '🐅', '🐆', '🐴', '🐎', '🦄', '🐮', '🐷', '🐖', '🐗', '🐽', '🐏', '🐑', '🐐', '🐪', '🐫', '🦙', '🐘', '🐭', '🐁', '🐀', '🐹', '🐰', '🐇', '🐿️', '🦫', '🦔', '🦇', '🐻', '🐨', '🐼', '🦥', '🦦', '🦨', '🦘', '🦡', '🐸', '🐊', '🐢', '🦎', '🐍', '🐲', '🐉', '🐳', '🐋', '🐬', '🦭', '🦈', '🐟', '🐠', '🐡', '🐙', '🦑', '🦐', '🦞', '🦀', '🐌', '🦋', '🐛', '🐜', '🐝', '🪲', '🐞', '🦗', '🪳', '🕷️'];
let eBtn=document.getElementById('global-emoji-button');
if(!eBtn){eBtn=document.createElement('span');eBtn.id='global-emoji-button';eBtn.className='emoji-button';eBtn.innerHTML='😂';eBtn.setAttribute('role','button');eBtn.tabIndex=0;document.body.appendChild(eBtn);}
eBtn.onmouseenter=()=>{eBtn.innerHTML=BE[Math.random()*BE.length|0];eBtn.style.filter='grayscale(0%)';eBtn.style.transform='scale(1.1)';};
eBtn.onmouseleave=()=>{eBtn.style.filter='grayscale(80%)';eBtn.style.transform='scale(1)';};
eBtn.onclick=e=>{e.stopPropagation();const h=getComputedStyle(container).display==='none';container.style.display=h?'flex':'none';if(h)switchTab('home');};
// Close & keyboard
container.querySelector('.close').addEventListener('click',()=>container.style.display='none');
document.addEventListener('keydown',e=>{
if(e.key==='Escape'){container.style.display='none';hideCtx();}
if((e.ctrlKey||e.metaKey)&&e.altKey&&e.key.toLowerCase()==='e'){e.preventDefault();const h=getComputedStyle(container).display==='none';container.style.display=h?'flex':'none';if(h)switchTab('home');}
});
// Drag
{let d=false,ox=0,oy=0;
container.querySelector('.drag').addEventListener('mousedown',e=>{d=true;const r=container.getBoundingClientRect();ox=e.clientX-r.left;oy=e.clientY-r.top;e.preventDefault();});
document.addEventListener('mousemove',e=>{if(!d)return;container.style.left=(e.clientX-ox)+'px';container.style.top=(e.clientY-oy)+'px';container.style.right='auto';container.style.bottom='auto';});
document.addEventListener('mouseup',()=>{if(d){d=false;S.menuPositionX=container.style.left;S.menuPositionY=container.style.top;saveS();}});
}
// Memory cleanup
new MutationObserver(()=>{if(container.style.display==='none')destroyLazy();}).observe(container,{attributes:true,attributeFilter:['style']});
window.addEventListener('beforeunload',destroyLazy);
// ═══════════════════════════════════════════════
// INIT
// ═══════════════════════════════════════════════
applyVars();buildTabs();showHome();
loadEmojis();loadStickers();
if(S.localGifUrls)loadLocalGifs();
})();