KG_Full_Emoticons

Display a popup panel with every available emoticon on the site, remembering the last selected emoticon per category by name.

当前为 2025-03-21 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         KG_Full_Emoticons
// @namespace    http://klavogonki.ru/
// @version      1.5
// @description  Display a popup panel with every available emoticon on the site, remembering the last selected emoticon per category by name.
// @match        *://klavogonki.ru/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=klavogonki.ru
// @grant        none
// ==/UserScript==

(function () {
  // State management
  const state = {
    eventListeners: [],
    activeCategory: localStorage.getItem("activeCategory") || "Boys",
    isPopupCreated: false,
    categoryHistory: [],
    currentSortedEmoticons: [],
    lastFocusedInput: null,
    latestCategoryRequest: null,
    lastKeyTimes: {},
    lastUsedEmoticons: JSON.parse(localStorage.getItem("lastUsedEmoticons")) || {}
  };

  // Helper function to handle double key presses
  function handleDoubleKeyPress(e, targetKey, threshold, callback) {
    const now = Date.now();
    if (e.code === targetKey) {
      if (now - (state.lastKeyTimes[targetKey] || 0) < threshold) {
        e.preventDefault();
        callback();
        state.lastKeyTimes[targetKey] = 0; // Reset after triggering
      } else {
        state.lastKeyTimes[targetKey] = now;
      }
    } else {
      // Reset the timing for the target key using dot notation
      state.lastKeyTimes.Semicolon = 0;
    }
  }

  // Constants
  const UI = {
    borderRadius: '0.2em',
    boxShadow: '0 8px 30px rgba(0, 0, 0, 0.12), 0 4px 6px rgba(0, 0, 0, 0.04), 0 2px 2px rgba(0, 0, 0, 0.08)'
  };

  const categories = {
    Boys: [
      "hello",
      "hi",
      "smile",
      "wink",
      "biggrin",
      "laugh",
      "happy",
      "cool",
      "rofl",
      "rofl2",
      "rolleyes",
      "spiteful",
      "crazy",
      "acute",
      "silence",
      "tongue",
      "whistle",
      "music",
      "ph34r",
      "excl",
      "no",
      "yes",
      "ok",
      "bye",
      "victory",
      "good",
      "clapping",
      "dance",
      "cult",
      "power",
      "boystroking",
      "complaugh",
      "badcomp",
      "gamer",
      "first",
      "second",
      "third",
      "formula1",
      "friends",
      "popcorn",
      "tea",
      "beer",
      "grats",
      "birthday",
      "holmes",
      "kidtruck",
      "musketeer",
      "pioneer",
      "mellow",
      "dry",
      "sleep",
      "cry",
      "sick",
      "sorry",
      "unsure",
      "boredom",
      "facepalm",
      "scare",
      "blink",
      "sad",
      "confuse",
      "nervous",
      "wacko",
      "huh",
      "ohmy",
      "megashok",
      "shok",
      "bad",
      "russian",
      "dash",
      "angry",
      "angry2",
    ],
    Girls: [
      "cheerful",
      "cheerleader",
      "clapgirl",
      "curtsey",
      "enjoygift",
      "girlblum",
      "girlconfuse",
      "girlcrazy",
      "girlcry",
      "girlicecream",
      "girlimpossible",
      "girlinlove",
      "girlkiss",
      "girlkissboy",
      "girlmad",
      "girlmusic",
      "girlnervous",
      "girlnotebook",
      "girlobserve",
      "girlrevolve",
      "girlsad",
      "umbrage",
      "girlscare",
      "girlshighfive",
      "girlsick",
      "girlsilence",
      "girlstop",
      "girlstroking",
      "girlsuper",
      "girltea",
      "girlwacko",
      "girlwink",
      "girlwitch",
      "girlwonder",
      "goody",
      "hairdryer",
      "hiya",
      "hysteric",
      "kgagainstaz",
      "kgrace",
      "primp",
      "respect",
      "spruceup",
      "spruceup1",
      "supergirl",
      "tender",
      "angrygirl",
      "girldevil",
    ],
    Christmas: [
      "firework",
      "confetti",
      "cheers",
      "wine",
      "champ",
      "champ2",
      "santa",
      "santa2",
      "santa3",
      "snowhand",
      "snowhit",
      "heyfrombag",
      "snowgirlwave",
      "snowball",
      "snegurochka",
      "santasnegurka",
      "snowman",
      "merrychristmas",
      "spruce",
      "moose",
      "christmasevil",
    ],
    Inlove: [
      "inlove",
      "hug",
      "boykiss",
      "wecheers",
      "wedance",
      "adultery",
      "cave",
      "leisure",
      "wedding",
      "airkiss",
      "kissed",
      "flowers",
      "grose",
      "flowers2",
      "rose",
      "smell",
      "frog",
      "girlfrog",
      "rocker",
      "serenade",
      "val",
      "girlval",
      "bemine",
      "heartcake",
      "heart2",
      "girlheart2",
      "girllove",
      "nolove",
      "heart",
      "blush",
      "wub",
    ],
    Army: [
      "uzi",
      "ak47",
      "barret",
      "chaingun",
      "pogranminigun",
      "partizan",
      "dandy",
      "gangster",
      "mafia",
      "foolrifle",
      "cowboy",
      "armyscare",
      "armystar",
      "armyfriends",
      "armytongue",
      "soldier",
      "bayanist",
      "pogranmail",
      "pogran",
      "pogranflowers",
      "pogranrose",
      "pograntort",
      "girlpogran",
      "pogranmama",
      "budenov",
      "captain",
      "vdv",
      "comandos",
      "kirpich",
      "girlvdv",
      "girlranker",
      "ranker",
      "girlrogatka",
      "rogatka",
      "radistka",
      "prival",
      "vtik",
      "vpered",
      "tank",
      "fly",
    ],
    Halloween: [
      "alien",
      "ghost",
      "cyborg",
      "robot",
      "terminator",
      "turtle",
      "batman",
      "bebebe",
      "bite",
      "corsair",
      "girlpirate",
      "indigenous",
      "clown",
      "jester",
      "death",
      "paladin",
      "pirate",
      "dwarf",
      "pirates",
      "witch",
      "wizard",
      "spider",
      "diablo",
      "vampire",
      "carpet",
    ],
    Favourites: []
  }

  const categoryEmojis = {
    Boys: "😃", Girls: "👩‍🦰", Christmas: "🎄", Inlove: "❤️", Army: "🔫", Halloween: "🎃", Favourites: "🌟"
  };

  // Initialize state
  const bodyLightness = getLightness(window.getComputedStyle(document.body).backgroundColor);
  const colors = {
    popupBackground: getAdjustedBackground("popupBackground"),
    defaultButton: getAdjustedBackground("defaultButton"),
    hoverButton: getAdjustedBackground("hoverButton"),
    activeButton: getAdjustedBackground("activeButton"),
    selectedButton: getAdjustedBackground("selectedButton")
  };

  // Initialize last used emoticons
  Object.keys(categories).forEach(cat => {
    if (!Object.prototype.hasOwnProperty.call(state.lastUsedEmoticons, cat) || !categories[cat].includes(state.lastUsedEmoticons[cat])) {
      state.lastUsedEmoticons[cat] = categories[cat][0] || '';
    }
  });

  // UI/Color utility functions
  function getLightness(color) {
    const match = color.match(/\d+/g);
    if (match && match.length === 3) {
      const [r, g, b] = match.map(Number);
      const max = Math.max(r, g, b) / 255;
      const min = Math.min(r, g, b) / 255;
      return Math.round(((max + min) / 2) * 100);
    }
    return 0;
  }

  function getAdjustedBackground(type) {
    const adjustments = {
      popupBackground: 10, defaultButton: 15, hoverButton: 25, activeButton: 35, selectedButton: 45
    };
    const adjustment = adjustments[type] || 0;
    const adjustedLightness = bodyLightness < 50 ? bodyLightness + adjustment : bodyLightness - adjustment;
    return `hsl(0, 0%, ${adjustedLightness}%)`;
  }

  // Data management functions
  function loadFavoriteEmoticons() {
    categories.Favourites = JSON.parse(localStorage.getItem("favoriteEmoticons")) || [];
  }

  function loadEmoticonUsageData() {
    return JSON.parse(localStorage.getItem("emoticonUsageData")) || {};
  }

  function saveEmoticonUsageData(data) {
    localStorage.setItem("emoticonUsageData", JSON.stringify(data));
  }

  function incrementEmoticonUsage(emoticon) {
    const data = loadEmoticonUsageData();
    data[state.activeCategory] = data[state.activeCategory] || {};
    data[state.activeCategory][emoticon] = (data[state.activeCategory][emoticon] || 0) + 1;
    saveEmoticonUsageData(data);
  }

  function getSortedEmoticons(category) {
    const usage = loadEmoticonUsageData()[category] || {};
    return categories[category].slice().sort((a, b) => (usage[b] || 0) - (usage[a] || 0));
  }

  function isEmoticonFavorite(emoticon) {
    const fav = JSON.parse(localStorage.getItem("favoriteEmoticons")) || [];
    return fav.includes(emoticon);
  }

  // Page context utility
  function getPageContext() {
    const path = window.location.pathname;
    const hash = window.location.hash;
    const searchParams = new URLSearchParams(window.location.search);
    const gmid = searchParams.get('gmid');
    const profileMatch = hash.match(/#\/(\d+)\//);

    return {
      isForum: path.includes('/forum/'),
      isGamelist: path.includes('/gamelist/'),
      isGame: !!gmid,
      isProfile: path === '/u/' && !!profileMatch,
      gmid: gmid || null,
      profileId: profileMatch?.[1] || null
    };
  }

  // Event handlers
  function onFocusIn(e) {
    if (e.target.matches("textarea, input.text, input#message-input")) {
      state.lastFocusedInput = e.target;
    }
  }

  function onKeyDown(e) {
    // Close popup if Ctrl+V is detected, assuming paste intention
    if (e.code === 'KeyV' && e.ctrlKey) {
      const popup = document.querySelector(".emoticons-popup");
      if (popup) {
        removeEmoticonsPopup();
      }
      return; // Allow the paste to proceed normally
    }

    // Use the helper function for detecting a double semicolon press
    if (e.code === 'Semicolon') {
      handleDoubleKeyPress(e, 'Semicolon', 500, function () {
        // Remove duplicated trailing character from the focused text field, if available
        if (state.lastFocusedInput) {
          let value = state.lastFocusedInput.value;
          // If the last two characters are identical, remove them; otherwise remove one character
          if (value.length >= 2 && value.slice(-1) === value.slice(-2, -1)) {
            value = value.slice(0, -2);
          } else if (value.length >= 1) {
            value = value.slice(0, -1);
          }
          state.lastFocusedInput.value = value;
          // Set the cursor at the end of the updated value
          const pos = value.length;
          state.lastFocusedInput.setSelectionRange(pos, pos);
        }
        toggleEmoticonsPopup();
      });
    } else {
      // Reset the semicolon double press timer
      state.lastKeyTimes.Semicolon = 0;
    }
  }

  function onMouseUp(e) {
    // Check for ctrl+click on text inputs (textarea or input with class "text")
    if (e.ctrlKey && e.button === 0 && e.target.matches("textarea, input.text, input#message-input")) {
      e.preventDefault();
      toggleEmoticonsPopup();
    }
  }

  function closePopupOnKeydown(e) {
    const popup = document.querySelector(".emoticons-popup");
    // Close popup if the key is Escape or KeyQ (using e.code for layout independence)
    if (popup && (e.code === 'Escape' || e.code === 'KeyQ')) {
      e.preventDefault();
      removeEmoticonsPopup();
    }
  }

  function closePopupOnClickOutside(e) {
    const popup = document.querySelector(".emoticons-popup");
    if (popup && !popup.contains(e.target)) {
      removeEmoticonsPopup();
    }
  }

  function getEmoticonCode(emoticon) {
    const { isForum } = getPageContext();
    // Check if there is a focused element and if it is a textarea
    if (isForum && state.lastFocusedInput && state.lastFocusedInput.tagName.toLowerCase() === "textarea") {
      // Use bbcode format for textarea on forum pages
      return `[img]https://klavogonki.ru/img/smilies/${emoticon}.gif[/img] `;
    } else {
      // Otherwise, use the colon-based format
      return `:${emoticon}: `;
    }
  }

  function insertEmoticonCode(emoticon) {
    const context = getPageContext();
    let targetInput = state.lastFocusedInput;

    if (!targetInput) {
      if (context.isForum) targetInput = document.getElementById('fast-reply_textarea');
      else if (context.isGamelist) targetInput = document.querySelector('#chat-general.chat .messages input.text');
      else if (context.isGame) targetInput = document.querySelector('[id^="chat-game"].chat .messages input.text');

      if (!targetInput) {
        const labels = {
          isForum: "the forum", isProfile: "the profile", isGamelist: "general chat", isGame: "game chat"
        };
        const detected = Object.entries(labels)
          .filter(([key]) => context[key])
          .map(([_, value]) => value)
          .join(", ");
        alert(`Please focus on a text field in ${detected}.`);
        return;
      }
      targetInput.focus();
      state.lastFocusedInput = targetInput;
    }

    const code = getEmoticonCode(emoticon);
    const pos = targetInput.selectionStart || 0;
    targetInput.value = targetInput.value.slice(0, pos) + code + targetInput.value.slice(pos);
    targetInput.setSelectionRange(pos + code.length, pos + code.length);
    targetInput.focus();
  }

  // Event listeners cleanup
  function removeEventListeners() {
    state.eventListeners.forEach(({ event, handler }) => {
      document.removeEventListener(event, handler);
    });
    state.eventListeners = [];
  }

  // Animation utilities
  function toggleContainerSmoothly(container, action) {
    if (action === "show") {
      document.body.appendChild(container);
      requestAnimationFrame(() => {
        container.style.opacity = "1";
      });
    } else {
      container.style.opacity = "0";
      setTimeout(() => container.remove(), 300);
    }
  }

  // Popup control
  function removeEmoticonsPopup() {
    const popup = document.querySelector(".emoticons-popup");
    if (popup) {
      removeEventListeners();
      toggleContainerSmoothly(popup, "hide");
      state.isPopupCreated = false;
    }
  }

  function toggleEmoticonsPopup() {
    if (state.isPopupCreated) {
      removeEmoticonsPopup();
    } else {
      setTimeout(() => {
        createEmoticonsPopup(state.activeCategory);
      }, 10);
    }
  }

  // UI creation
  function createEmoticonsPopup(category) {
    if (state.isPopupCreated) return;
    loadFavoriteEmoticons();

    const popup = document.createElement("div");
    popup.className = "emoticons-popup";
    popup.style.setProperty('border-radius', '0.4em', 'important');
    popup.style.setProperty('box-shadow', UI.boxShadow, 'important');
    Object.assign(popup.style, {
      opacity: "0",
      transition: "opacity 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)",
      position: "fixed",
      display: "grid",
      gridTemplateRows: "50px auto",
      gap: "10px",
      backgroundColor: colors.popupBackground,
      padding: "10px",
      zIndex: "2000",
      top: "20vh",
      left: "50vw",
      transform: "translateX(-50%)",
      maxWidth: "50vw",
      minWidth: "630px",
      width: "50vw",
      maxHeight: "50vh",
      overflow: "hidden"
    });

    const headerButtons = document.createElement("div");
    headerButtons.classList.add("header-buttons");
    Object.assign(headerButtons.style, {
      display: "flex",
      flexDirection: "row",
      justifyContent: "space-between"
    });

    // Create buttons
    const createBtn = (className, title, innerHTML, bgColor, clickHandler) => {
      const btn = document.createElement("button");
      btn.classList.add(className);
      btn.title = title;
      btn.innerHTML = innerHTML;
      btn.style.setProperty('border-radius', UI.borderRadius, 'important');
      Object.assign(btn.style, {
        border: "none",
        background: bgColor,
        cursor: "pointer",
        boxSizing: "border-box",
        width: "50px",
        height: "50px",
        margin: "0 5px",
        fontSize: "1.4em"
      });
      if (clickHandler) btn.addEventListener("click", clickHandler);
      return btn;
    };

    const clearButton = createBtn(
      'clear-button',
      "Clear usage data",
      "🗑️",
      "hsl(40deg 50% 15%)",
      () => {
        if (confirm("Clear emoticon usage data?")) {
          localStorage.removeItem("emoticonUsageData");
        }
      }
    );

    const closeButton = createBtn(
      'close-button',
      "Close emoticons panel (or press 'q')",
      "❌",
      "hsl(0deg 50% 15%)",
      removeEmoticonsPopup
    );

    headerButtons.appendChild(clearButton);
    headerButtons.appendChild(createCategoryContainer());
    headerButtons.appendChild(closeButton);
    popup.appendChild(headerButtons);

    createEmoticonsContainer(category).then((container) => {
      popup.appendChild(container);
      requestAnimationFrame(updateEmoticonHighlight);
    });

    popup.addEventListener("dblclick", removeEmoticonsPopup);

    const eventListenersArray = [
      { event: "keydown", handler: navigateEmoticons },
      { event: "keydown", handler: switchEmoticonCategory },
      { event: "keydown", handler: closePopupOnKeydown },
      { event: "click", handler: closePopupOnClickOutside }
    ];

    eventListenersArray.forEach(({ event, handler }) => {
      state.eventListeners.push({ event, handler });
      document.addEventListener(event, handler);
    });

    document.body.appendChild(popup);
    toggleContainerSmoothly(popup, "show");
    state.isPopupCreated = true;
  }

  function createCategoryContainer() {
    const container = document.createElement("div");
    container.className = "category-buttons";
    Object.assign(container.style, {
      display: "flex",
      justifyContent: "center",
    });

    for (let cat in categories) {
      if (Object.prototype.hasOwnProperty.call(categories, cat)) {
        const btn = document.createElement("button");
        btn.classList.add("category-button");
        btn.innerHTML = categoryEmojis[cat];
        btn.dataset.category = cat;
        btn.title = cat;
        btn.style.setProperty("border-radius", UI.borderRadius, "important");
        Object.assign(btn.style, {
          background: (cat === state.activeCategory ? colors.activeButton : colors.defaultButton),
          border: "none",
          cursor: "pointer",
          width: "50px",
          height: "50px",
          fontSize: "1.4em",
          margin: "0 5px"
        });

        // Special handling for "Favourites"
        if (cat === "Favourites") {
          if (categories.Favourites.length === 0) {
            btn.style.opacity = "0.5";
            btn.style.pointerEvents = "none";
          }
          btn.addEventListener("click", handleFavouritesClick);
        }

        btn.addEventListener("click", (e) => handleCategoryClick(cat, e));
        btn.addEventListener("mouseout", () => handleCategoryMouseOut(btn, cat));
        btn.addEventListener("mouseover", () => {
          btn.style.background = colors.hoverButton;
        });

        container.appendChild(btn);
      }
    }
    return container;
  }

  // Category handling
  function handleCategoryClick(cat, e) {
    if (!e.shiftKey && !e.ctrlKey) {
      changeActiveCategoryOnClick(cat);
    }
  }

  function handleCategoryMouseOut(btn, cat) {
    btn.style.background = (cat === state.activeCategory ? colors.activeButton : colors.defaultButton);
    if (cat === "Favourites") {
      btn.style.opacity = categories.Favourites.length ? "" : "0.5";
    }
  }

  function handleFavouritesClick(e) {
    if (e.ctrlKey) {
      localStorage.removeItem("favoriteEmoticons");
      categories.Favourites = [];
      updateEmoticonHighlight();
      if (state.categoryHistory.length) {
        state.activeCategory = state.categoryHistory.pop();
        localStorage.setItem("activeCategory", state.activeCategory);
        updateCategoryButtonsState(state.activeCategory);
        updateEmoticonsContainer();
      }
    }
  }

  function updateCategoryButtonsState(newCategory) {
    document.querySelectorAll(".category-buttons button").forEach((btn) => {
      btn.style.background = btn.dataset.category === newCategory ? colors.activeButton : colors.defaultButton;
      if (btn.dataset.category === "Favourites") {
        if (categories.Favourites.length === 0) {
          btn.style.opacity = "0.5";
          btn.style.pointerEvents = "none";
        } else {
          btn.style.removeProperty("opacity");
          btn.style.removeProperty("pointer-events");
        }
      }
    });
  }

  function changeActiveCategoryOnClick(newCategory) {
    if (newCategory === "Favourites" && categories.Favourites.length === 0) return;
    if (state.activeCategory !== "Favourites") {
      state.categoryHistory.push(state.activeCategory);
    }
    state.activeCategory = newCategory;
    localStorage.setItem("activeCategory", state.activeCategory);
    state.currentSortedEmoticons = getSortedEmoticons(state.activeCategory);
    updateCategoryButtonsState(state.activeCategory);
    updateEmoticonsContainer();
  }

  // Emoticon container creation
  async function createEmoticonsContainer(category) {
    const container = document.createElement("div");
    container.className = "emoticon-buttons";

    state.currentSortedEmoticons = getSortedEmoticons(category);

    const promises = [];
    state.currentSortedEmoticons.forEach((emoticon) => {
      const btn = document.createElement("button");
      btn.classList.add('emoticon-button');
      const imgSrc = `/img/smilies/${emoticon}.gif`;
      btn.innerHTML = `<img src="${imgSrc}" alt="${emoticon}">`;
      btn.title = emoticon;

      btn.style.setProperty('border-radius', UI.borderRadius, 'important');
      Object.assign(btn.style, {
        position: 'relative',
        border: "none",
        cursor: "pointer",
        filter: emoticon === state.lastUsedEmoticons[state.activeCategory] ? "sepia(0.7)" : "none",
        background: emoticon === state.lastUsedEmoticons[state.activeCategory]
          ? colors.selectedButton
          : colors.defaultButton
      });

      // Preload the image
      promises.push(new Promise(resolve => {
        const img = new Image();
        img.onload = resolve;
        img.src = imgSrc;
      }));

      // Mouseover: change background to hover color
      btn.addEventListener("mouseover", () => {
        btn.style.background = colors.hoverButton;
      });

      // Mouseout: reset background based on state
      btn.addEventListener("mouseout", () => {
        if (emoticon === state.lastUsedEmoticons[state.activeCategory]) {
          btn.style.background = colors.selectedButton;
        } else if (state.activeCategory !== "Favourites" && isEmoticonFavorite(emoticon)) {
          btn.style.background = colors.activeButton;
        } else {
          btn.style.background = colors.defaultButton;
        }
      });

      btn.addEventListener("click", (e) => {
        e.stopPropagation();
        if (e.shiftKey) {
          insertEmoticonCode(emoticon);
        } else if (e.ctrlKey) {
          const fav = JSON.parse(localStorage.getItem("favoriteEmoticons")) || [];
          const pos = fav.indexOf(emoticon);
          if (category === "Favourites" && pos !== -1) {
            fav.splice(pos, 1);
            categories.Favourites.splice(pos, 1);
          } else if (category !== "Favourites" && !fav.includes(emoticon)) {
            fav.push(emoticon);
            categories.Favourites.push(emoticon);
          }
          localStorage.setItem("favoriteEmoticons", JSON.stringify(fav));
          updateCategoryButtonsState(category);
          if (category === "Favourites") updateEmoticonsContainer();
        } else {
          insertEmoticonCode(emoticon);
          incrementEmoticonUsage(emoticon);
          state.lastUsedEmoticons[state.activeCategory] = emoticon;
          localStorage.setItem("lastUsedEmoticons", JSON.stringify(state.lastUsedEmoticons));
          removeEmoticonsPopup();
        }
        updateEmoticonHighlight();
      });

      container.appendChild(btn);
    });

    await Promise.all(promises);
    const { maxImageWidth, maxImageHeight } = await calculateMaxImageDimensions(state.currentSortedEmoticons);
    Object.assign(container.style, {
      display: "grid",
      gap: "10px",
      scrollbarWidth: "none",
      overflowY: "auto",
      overflowX: "hidden",
      maxHeight: "calc(-80px + 50vh)",
      gridTemplateColumns: `repeat(auto-fit, minmax(${maxImageWidth}px, 1fr))`,
      gridAutoRows: `minmax(${maxImageHeight}px, auto)`
    });
    return container;
  }

  async function calculateMaxImageDimensions(emoticonsImages) {
    const minValue = 34;
    const imageDimensions = await Promise.all(
      emoticonsImages.map((imageName) => {
        return new Promise((resolve) => {
          const img = new Image();
          img.onload = () => resolve({ width: img.width, height: img.height });
          img.src = `/img/smilies/${imageName}.gif`;
        });
      })
    );
    const maxWidth = Math.max(minValue, ...imageDimensions.map(img => img.width));
    const maxHeight = Math.max(minValue, ...imageDimensions.map(img => img.height));
    return { maxImageWidth: maxWidth, maxImageHeight: maxHeight };
  }

  function updateEmoticonsContainer() {
    const requestTimestamp = Date.now();
    state.latestCategoryRequest = requestTimestamp;

    // Remove all old containers
    document.querySelectorAll(".emoticon-buttons").forEach(container => container.remove());

    createEmoticonsContainer(state.activeCategory).then((container) => {
      // Ensure this is still the latest request before appending
      if (state.latestCategoryRequest !== requestTimestamp) return;

      const popup = document.querySelector(".emoticons-popup");
      if (popup) {
        popup.appendChild(container);
        updateEmoticonHighlight();
      }
    });
  }

  // Navigation and selection
  function updateEmoticonHighlight() {
    requestAnimationFrame(() => {
      const buttons = document.querySelectorAll(".emoticon-buttons button");
      buttons.forEach((btn) => {
        const emoticon = btn.title;
        const isSelected = emoticon === state.lastUsedEmoticons[state.activeCategory];
        if (isSelected) {
          btn.style.background = colors.selectedButton;
          btn.style.filter = "sepia(0.7)";
        } else {
          btn.style.filter = "none";
          if (state.activeCategory !== "Favourites" && isEmoticonFavorite(emoticon)) {
            btn.style.background = colors.activeButton;
          } else {
            btn.style.background = colors.defaultButton;
          }
        }
      });
    });
  }

  function updateActiveEmoticon(direction) {
    const currentIndex = state.currentSortedEmoticons.indexOf(state.lastUsedEmoticons[state.activeCategory]);
    let newIndex = currentIndex === -1 ? 0 : currentIndex + direction;

    // Handle wrapping
    if (newIndex < 0) newIndex = state.currentSortedEmoticons.length - 1;
    if (newIndex >= state.currentSortedEmoticons.length) newIndex = 0;

    // Update state
    state.lastUsedEmoticons[state.activeCategory] = state.currentSortedEmoticons[newIndex];
    localStorage.setItem("lastUsedEmoticons", JSON.stringify(state.lastUsedEmoticons));

    // Update UI
    updateEmoticonHighlight();
  }

  function navigateEmoticons(e) {
    const popup = document.querySelector(".emoticons-popup");
    if (!popup || !state.currentSortedEmoticons || state.currentSortedEmoticons.length === 0) return;

    const handledKeys = new Set(['Enter', 'Semicolon', 'ArrowLeft', 'KeyJ', 'ArrowRight', 'KeyK']);
    if (!handledKeys.has(e.code)) return;

    e.preventDefault();

    if (e.code === "Enter" || e.code === "Semicolon") {
      const emoticon = state.lastUsedEmoticons[state.activeCategory];
      if (emoticon && state.currentSortedEmoticons.includes(emoticon)) {
        insertEmoticonCode(emoticon);
        incrementEmoticonUsage(emoticon);
        if (!e.shiftKey) removeEmoticonsPopup();
      }
    }
    else if (e.code === "ArrowLeft" || e.code === "KeyJ") {
      updateActiveEmoticon(-1); // Move left
    }
    else if (e.code === "ArrowRight" || e.code === "KeyK") {
      updateActiveEmoticon(1); // Move right
    }
  }

  function switchEmoticonCategory(e) {
    const emoticonPopup = document.querySelector(".emoticons-popup");
    if (!emoticonPopup || (!["Tab", "KeyH", "KeyL"].includes(e.code) && !(e.code === "Tab" && e.shiftKey))) return;

    e.preventDefault();
    const keys = Object.keys(categories);
    const favs = JSON.parse(localStorage.getItem("favoriteEmoticons")) || [];
    const navKeys = favs.length === 0 ? keys.filter(key => key !== "Favourites") : keys;
    let idx = navKeys.indexOf(state.activeCategory);
    if (idx === -1) idx = 0;

    let newIdx = ((e.code === "Tab" && !e.shiftKey) || e.code === "KeyL") && idx < navKeys.length - 1 ? idx + 1 :
      ((e.code === "KeyH" || (e.code === "Tab" && e.shiftKey)) && idx > 0) ? idx - 1 : idx;
    if (newIdx === idx) return;

    const next = navKeys[newIdx];
    state.currentSortedEmoticons = getSortedEmoticons(next);
    localStorage.setItem("activeCategory", next);
    changeActiveCategoryOnClick(next);
  }

  // Set up main event listeners
  document.addEventListener("focusin", onFocusIn);
  document.addEventListener("mouseup", onMouseUp);
  document.addEventListener("keydown", onKeyDown);
})();