Discourse 表情扩展 (Emoji Extension for Discourse) lite

为 Discourse 论坛添加表情选择器功能 (Add emoji picker functionality to Discourse forums)

当前为 2025-10-23 提交的版本,查看 最新版本

// ==UserScript==
// @name         Discourse 表情扩展 (Emoji Extension for Discourse) lite
// @namespace    https://github.com/stevessr/bug-v3
// @version      1.1.9-fix1
// @description  为 Discourse 论坛添加表情选择器功能 (Add emoji picker functionality to Discourse forums)
// @author       stevessr
// @match        https://linux.do/*
// @match        https://meta.discourse.org/*
// @match        https://*.discourse.org/*
// @match        http://localhost:5173/*
// @exclude      https://linux.do/a/*
// @match        https://idcflare.com/*
// @grant        none
// @license      MIT
// @homepageURL  https://github.com/stevessr/bug-v3
// @supportURL   https://github.com/stevessr/bug-v3/issues
// @run-at       document-end
// ==/UserScript==

;(function () {
  'use strict'
  ;(function () {
    const __defProp = Object.defineProperty
    const __esmMin = (fn, res) => () => (fn && (res = fn((fn = 0))), res)
    const __export = all => {
      const target = {}
      for (const name in all)
        __defProp(target, name, {
          get: all[name],
          enumerable: true
        })
      return target
    }
    async function fetchPackagedJSON() {
      try {
        if (typeof fetch === 'undefined') return null
        const res = await fetch('/assets/defaultEmojiGroups.json', { cache: 'no-cache' })
        if (!res.ok) return null
        return await res.json()
      } catch (err) {
        return null
      }
    }
    async function loadDefaultEmojiGroups() {
      const packaged = await fetchPackagedJSON()
      if (packaged && Array.isArray(packaged.groups)) return packaged.groups
      return []
    }
    const init_defaultEmojiGroups_loader = __esmMin(() => {})
    function loadDataFromLocalStorage() {
      try {
        const groupsData = localStorage.getItem(STORAGE_KEY)
        let emojiGroups = []
        if (groupsData)
          try {
            const parsed = JSON.parse(groupsData)
            if (Array.isArray(parsed) && parsed.length > 0) emojiGroups = parsed
          } catch (e) {
            console.warn('[Userscript] Failed to parse stored emoji groups:', e)
          }
        if (emojiGroups.length === 0) emojiGroups = []
        const settingsData = localStorage.getItem(SETTINGS_KEY)
        let settings = { ...DEFAULT_USER_SETTINGS }
        if (settingsData)
          try {
            const parsed = JSON.parse(settingsData)
            if (parsed && typeof parsed === 'object')
              settings = {
                ...settings,
                ...parsed
              }
          } catch (e) {
            console.warn('[Userscript] Failed to parse stored settings:', e)
          }
        emojiGroups = emojiGroups.filter(g => g.id !== 'favorites')
        console.log('[Userscript] Loaded data from localStorage:', {
          groupsCount: emojiGroups.length,
          emojisCount: emojiGroups.reduce((acc, g) => acc + (g.emojis?.length || 0), 0),
          settings
        })
        return {
          emojiGroups,
          settings
        }
      } catch (error) {
        console.error('[Userscript] Failed to load from localStorage:', error)
        return {
          emojiGroups: [],
          settings: { ...DEFAULT_USER_SETTINGS }
        }
      }
    }
    async function loadDataFromLocalStorageAsync() {
      try {
        const local = loadDataFromLocalStorage()
        if (local.emojiGroups && local.emojiGroups.length > 0) return local
        const remoteUrl = localStorage.getItem('emoji_extension_remote_config_url')
        if (remoteUrl && typeof remoteUrl === 'string' && remoteUrl.trim().length > 0)
          try {
            const controller = new AbortController()
            const timeout = setTimeout(() => controller.abort(), 5e3)
            const res = await fetch(remoteUrl, { signal: controller.signal })
            clearTimeout(timeout)
            if (res && res.ok) {
              const json = await res.json()
              const groups = Array.isArray(json.emojiGroups)
                ? json.emojiGroups
                : Array.isArray(json.groups)
                  ? json.groups
                  : null
              const settings =
                json.settings && typeof json.settings === 'object' ? json.settings : local.settings
              if (groups && groups.length > 0) {
                try {
                  localStorage.setItem(STORAGE_KEY, JSON.stringify(groups))
                } catch (e) {
                  console.warn(
                    '[Userscript] Failed to persist fetched remote groups to localStorage',
                    e
                  )
                }
                return {
                  emojiGroups: groups.filter(g => g.id !== 'favorites'),
                  settings
                }
              }
            }
          } catch (err) {
            console.warn('[Userscript] Failed to fetch remote default config:', err)
          }
        try {
          const runtime = await loadDefaultEmojiGroups()
          const source = runtime && runtime.length ? runtime : []
          const filtered = JSON.parse(JSON.stringify(source)).filter(g => g.id !== 'favorites')
          try {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered))
          } catch (e) {}
          return {
            emojiGroups: filtered,
            settings: local.settings
          }
        } catch (e) {
          console.error('[Userscript] Failed to load default groups in async fallback:', e)
          return {
            emojiGroups: [],
            settings: local.settings
          }
        }
      } catch (error) {
        console.error('[Userscript] loadDataFromLocalStorageAsync failed:', error)
        return {
          emojiGroups: [],
          settings: { ...DEFAULT_USER_SETTINGS }
        }
      }
    }
    function saveDataToLocalStorage(data) {
      try {
        if (data.emojiGroups) localStorage.setItem(STORAGE_KEY, JSON.stringify(data.emojiGroups))
        if (data.settings) localStorage.setItem(SETTINGS_KEY, JSON.stringify(data.settings))
      } catch (error) {
        console.error('[Userscript] Failed to save to localStorage:', error)
      }
    }
    function addEmojiToUserscript(emojiData) {
      try {
        const data = loadDataFromLocalStorage()
        let userGroup = data.emojiGroups.find(g => g.id === 'user_added')
        if (!userGroup) {
          userGroup = {
            id: 'user_added',
            name: '用户添加',
            icon: '⭐',
            order: 999,
            emojis: []
          }
          data.emojiGroups.push(userGroup)
        }
        if (!userGroup.emojis.some(e => e.url === emojiData.url || e.name === emojiData.name)) {
          userGroup.emojis.push({
            packet: Date.now(),
            name: emojiData.name,
            url: emojiData.url
          })
          saveDataToLocalStorage({ emojiGroups: data.emojiGroups })
          console.log('[Userscript] Added emoji to user group:', emojiData.name)
        } else console.log('[Userscript] Emoji already exists:', emojiData.name)
      } catch (error) {
        console.error('[Userscript] Failed to add emoji:', error)
      }
    }
    function syncFromManager() {
      try {
        const managerGroups = localStorage.getItem('emoji_extension_manager_groups')
        const managerSettings = localStorage.getItem('emoji_extension_manager_settings')
        let updated = false
        if (managerGroups) {
          const groups = JSON.parse(managerGroups)
          if (Array.isArray(groups)) {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(groups))
            updated = true
          }
        }
        if (managerSettings) {
          const settings = JSON.parse(managerSettings)
          if (typeof settings === 'object') {
            localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings))
            updated = true
          }
        }
        if (updated) console.log('[Userscript] Synced data from manager')
        return updated
      } catch (error) {
        console.error('[Userscript] Failed to sync from manager:', error)
        return false
      }
    }
    function trackEmojiUsage(emojiName, emojiUrl) {
      try {
        const key = `${emojiName}|${emojiUrl}`
        const statsData = localStorage.getItem(USAGE_STATS_KEY)
        let stats = {}
        if (statsData)
          try {
            stats = JSON.parse(statsData)
          } catch (e) {
            console.warn('[Userscript] Failed to parse usage stats:', e)
          }
        if (!stats[key])
          stats[key] = {
            count: 0,
            lastUsed: 0
          }
        stats[key].count++
        stats[key].lastUsed = Date.now()
        localStorage.setItem(USAGE_STATS_KEY, JSON.stringify(stats))
      } catch (error) {
        console.error('[Userscript] Failed to track emoji usage:', error)
      }
    }
    function getPopularEmojis(limit = 20) {
      try {
        const statsData = localStorage.getItem(USAGE_STATS_KEY)
        if (!statsData) return []
        const stats = JSON.parse(statsData)
        return Object.entries(stats)
          .map(([key, data]) => {
            const [name, url] = key.split('|')
            return {
              name,
              url,
              count: data.count,
              lastUsed: data.lastUsed
            }
          })
          .sort((a, b) => b.count - a.count)
          .slice(0, limit)
      } catch (error) {
        console.error('[Userscript] Failed to get popular emojis:', error)
        return []
      }
    }
    function clearEmojiUsageStats() {
      try {
        localStorage.removeItem(USAGE_STATS_KEY)
        console.log('[Userscript] Cleared emoji usage statistics')
      } catch (error) {
        console.error('[Userscript] Failed to clear usage stats:', error)
      }
    }
    let STORAGE_KEY, SETTINGS_KEY, USAGE_STATS_KEY, DEFAULT_USER_SETTINGS
    const init_userscript_storage = __esmMin(() => {
      init_defaultEmojiGroups_loader()
      STORAGE_KEY = 'emoji_extension_userscript_data'
      SETTINGS_KEY = 'emoji_extension_userscript_settings'
      USAGE_STATS_KEY = 'emoji_extension_userscript_usage_stats'
      DEFAULT_USER_SETTINGS = {
        imageScale: 30,
        gridColumns: 4,
        outputFormat: 'markdown',
        forceMobileMode: false,
        defaultGroup: 'nachoneko',
        showSearchBar: true,
        enableFloatingPreview: true,
        enableCalloutSuggestions: true,
        enableBatchParseImages: true
      }
    })
    let userscriptState
    const init_state = __esmMin(() => {
      init_userscript_storage()
      userscriptState = {
        emojiGroups: [],
        settings: { ...DEFAULT_USER_SETTINGS },
        emojiUsageStats: {}
      }
    })
    function createEl(tag, opts) {
      const el = document.createElement(tag)
      if (opts) {
        if (opts.width) el.style.width = opts.width
        if (opts.height) el.style.height = opts.height
        if (opts.className) el.className = opts.className
        if (opts.text) el.textContent = opts.text
        if (opts.placeholder && 'placeholder' in el) el.placeholder = opts.placeholder
        if (opts.type && 'type' in el) el.type = opts.type
        if (opts.value !== void 0 && 'value' in el) el.value = opts.value
        if (opts.style) el.style.cssText = opts.style
        if (opts.src && 'src' in el) el.src = opts.src
        if (opts.attrs) for (const k in opts.attrs) el.setAttribute(k, opts.attrs[k])
        if (opts.dataset) for (const k in opts.dataset) el.dataset[k] = opts.dataset[k]
        if (opts.innerHTML) el.innerHTML = opts.innerHTML
        if (opts.title) el.title = opts.title
        if (opts.alt && 'alt' in el) el.alt = opts.alt
        if (opts.id) el.id = opts.id
      }
      return el
    }
    const init_createEl = __esmMin(() => {})
    init_state()
    init_userscript_storage()
    init_createEl()
    function extractEmojiFromImage(img, titleElement) {
      const url = img.src
      if (!url || !url.startsWith('http')) return null
      let name = ''
      const parts = (titleElement.textContent || '').split('·')
      if (parts.length > 0) name = parts[0].trim()
      if (!name || name.length < 2) name = img.alt || img.title || extractNameFromUrl$1(url)
      name = name.trim()
      if (name.length === 0) name = '表情'
      return {
        name,
        url
      }
    }
    function extractEmojiDataFromLightboxWrapper(lightboxWrapper) {
      const results = []
      const anchor = lightboxWrapper.querySelector('a.lightbox')
      const img = lightboxWrapper.querySelector('img')
      if (!anchor || !img) return results
      const title = anchor.getAttribute('title') || ''
      const originalUrl = anchor.getAttribute('href') || ''
      const downloadUrl = anchor.getAttribute('data-download-href') || ''
      const imgSrc = img.getAttribute('src') || ''
      let name = title || img.getAttribute('alt') || ''
      if (!name || name.length < 2)
        name = extractNameFromUrl$1(originalUrl || downloadUrl || imgSrc)
      name = name.replace(/\\.(webp|jpg|jpeg|png|gif)$/i, '').trim() || '表情'
      const urlToUse = originalUrl || downloadUrl || imgSrc
      if (urlToUse && urlToUse.startsWith('http'))
        results.push({
          name,
          url: urlToUse
        })
      return results
    }
    function extractNameFromUrl$1(url) {
      try {
        const nameWithoutExt = (new URL(url).pathname.split('/').pop() || '').replace(
          /\.[^/.]+$/,
          ''
        )
        const decoded = decodeURIComponent(nameWithoutExt)
        if (/^[0-9a-f]{8,}$/i.test(decoded) || decoded.length < 2) return '表情'
        return decoded || '表情'
      } catch {
        return '表情'
      }
    }
    function createAddButton$1(emojiData) {
      const link = createEl('a', {
        className: 'image-source-link emoji-add-link',
        style: `
    color: #ffffff;
    text-decoration: none;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    font-size: inherit;
    font-family: inherit;
    background: linear-gradient(135deg, #4f46e5, #7c3aed);
    border: 2px solid #ffffff;
    border-radius: 6px;
    padding: 4px 8px;
    margin: 0 2px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
    transition: all 0.2s ease;
    font-weight: 600;
  `
      })
      link.addEventListener('mouseenter', () => {
        if (!link.innerHTML.includes('已添加') && !link.innerHTML.includes('失败')) {
          link.style.background = 'linear-gradient(135deg, #3730a3, #5b21b6)'
          link.style.transform = 'scale(1.05)'
          link.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.3)'
        }
      })
      link.addEventListener('mouseleave', () => {
        if (!link.innerHTML.includes('已添加') && !link.innerHTML.includes('失败')) {
          link.style.background = 'linear-gradient(135deg, #4f46e5, #7c3aed)'
          link.style.transform = 'scale(1)'
          link.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.2)'
        }
      })
      link.innerHTML = `
    <svg class="fa d-icon d-icon-plus svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="width: 1em; height: 1em; fill: currentColor; margin-right: 4px;">
      <path d="M12 4c.55 0 1 .45 1 1v6h6c.55 0 1 .45 1 1s-.45 1-1 1h-6v6c0 .55-.45 1-1 1s-1-.45-1-1v-6H5c-.55 0-1-.45-1-1s.45-1 1-1h6V5c0-.55.45-1 1-1z"/>
    </svg>添加表情
  `
      link.title = '添加到用户表情'
      link.addEventListener('click', async e => {
        e.preventDefault()
        e.stopPropagation()
        const originalHTML = link.innerHTML
        const originalStyle = link.style.cssText
        try {
          addEmojiToUserscript(emojiData)
          link.innerHTML = `
        <svg class="fa d-icon d-icon-check svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="width: 1em; height: 1em; fill: currentColor; margin-right: 4px;">
          <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
        </svg>已添加
      `
          link.style.background = 'linear-gradient(135deg, #10b981, #059669)'
          link.style.color = '#ffffff'
          link.style.border = '2px solid #ffffff'
          link.style.boxShadow = '0 2px 4px rgba(16, 185, 129, 0.3)'
          setTimeout(() => {
            link.innerHTML = originalHTML
            link.style.cssText = originalStyle
          }, 2e3)
        } catch (error) {
          console.error('[Emoji Extension Userscript] Failed to add emoji:', error)
          link.innerHTML = `
        <svg class="fa d-icon d-icon-times svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="width: 1em; height: 1em; fill: currentColor; margin-right: 4px;">
          <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
        </svg>失败
      `
          link.style.background = 'linear-gradient(135deg, #ef4444, #dc2626)'
          link.style.color = '#ffffff'
          link.style.border = '2px solid #ffffff'
          link.style.boxShadow = '0 2px 4px rgba(239, 68, 68, 0.3)'
          setTimeout(() => {
            link.innerHTML = originalHTML
            link.style.cssText = originalStyle
          }, 2e3)
        }
      })
      return link
    }
    function processLightbox(lightbox) {
      if (lightbox.querySelector('.emoji-add-link')) return
      const img = lightbox.querySelector('.mfp-img')
      const title = lightbox.querySelector('.mfp-title')
      if (!img || !title) return
      const emojiData = extractEmojiFromImage(img, title)
      if (!emojiData) return
      const addButton = createAddButton$1(emojiData)
      const sourceLink = title.querySelector('a.image-source-link')
      if (sourceLink) {
        const separator = document.createTextNode(' · ')
        title.insertBefore(separator, sourceLink)
        title.insertBefore(addButton, sourceLink)
      } else {
        title.appendChild(document.createTextNode(' · '))
        title.appendChild(addButton)
      }
    }
    function processAllLightboxes() {
      document.querySelectorAll('.mfp-wrap.mfp-gallery').forEach(lightbox => {
        if (
          lightbox.classList.contains('mfp-wrap') &&
          lightbox.classList.contains('mfp-gallery') &&
          lightbox.querySelector('.mfp-img')
        )
          processLightbox(lightbox)
      })
    }
    function initOneClickAdd() {
      console.log('[Emoji Extension Userscript] Initializing one-click add functionality')
      setTimeout(processAllLightboxes, 500)
      new MutationObserver(mutations => {
        let hasNewLightbox = false
        mutations.forEach(mutation => {
          if (mutation.type === 'childList')
            mutation.addedNodes.forEach(node => {
              if (node.nodeType === Node.ELEMENT_NODE) {
                const element = node
                if (element.classList && element.classList.contains('mfp-wrap'))
                  hasNewLightbox = true
              }
            })
        })
        if (hasNewLightbox) setTimeout(processAllLightboxes, 100)
      }).observe(document.body, {
        childList: true,
        subtree: true
      })
      document.addEventListener('visibilitychange', () => {
        if (!document.hidden) setTimeout(processAllLightboxes, 200)
      })
      initBatchParseButtons()
    }
    function createBatchParseButton(cookedElement) {
      const button = createEl('button', {
        className: 'emoji-batch-parse-button',
        style: `
      display: inline-flex;
      align-items: center;
      gap: 6px;
      background: linear-gradient(135deg, #f59e0b, #d97706);
      color: #fff;
      border-radius: 8px;
      padding: 8px 12px;
      margin: 10px 0;
      font-weight: 600;
      cursor: pointer;
      border: none;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
      transition: all 0.2s ease;
    `
      })
      button.innerHTML = `
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="width: 1em; height: 1em; fill: currentColor;">
      <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
    </svg>
    一键解析并添加所有图片
  `
      button.title = '解析当前内容中的所有图片并添加到用户表情'
      button.addEventListener('mouseenter', () => {
        if (!button.disabled) {
          button.style.background = 'linear-gradient(135deg, #d97706, #b45309)'
          button.style.transform = 'scale(1.02)'
        }
      })
      button.addEventListener('mouseleave', () => {
        if (!button.disabled && !button.innerHTML.includes('已处理')) {
          button.style.background = 'linear-gradient(135deg, #f59e0b, #d97706)'
          button.style.transform = 'scale(1)'
        }
      })
      button.addEventListener('click', async e => {
        e.preventDefault()
        e.stopPropagation()
        const originalHTML = button.innerHTML
        const originalStyle = button.style.cssText
        try {
          button.innerHTML = '正在解析...'
          button.style.background = 'linear-gradient(135deg, #6b7280, #4b5563)'
          button.disabled = true
          const lightboxWrappers = cookedElement.querySelectorAll('.lightbox-wrapper')
          const allEmojiData = []
          lightboxWrappers.forEach(wrapper => {
            const items = extractEmojiDataFromLightboxWrapper(wrapper)
            allEmojiData.push(...items)
          })
          if (allEmojiData.length === 0) throw new Error('未找到可解析的图片')
          let successCount = 0
          for (const emojiData of allEmojiData)
            try {
              addEmojiToUserscript(emojiData)
              successCount++
            } catch (e$1) {
              console.error('[Userscript OneClick] 添加图片失败', emojiData.name, e$1)
            }
          button.innerHTML = `
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="width: 1em; height: 1em; fill: currentColor;">
          <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
        </svg>
        已处理 ${successCount}/${allEmojiData.length} 张图片
      `
          button.style.background = 'linear-gradient(135deg, #10b981, #059669)'
          setTimeout(() => {
            button.innerHTML = originalHTML
            button.style.cssText = originalStyle
            button.disabled = false
          }, 3e3)
        } catch (error) {
          console.error('[Userscript OneClick] Batch parse failed:', error)
          button.innerHTML = `
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="width: 1em; height: 1em; fill: currentColor;">
          <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
        </svg>
        解析失败
      `
          button.style.background = 'linear-gradient(135deg, #ef4444, #dc2626)'
          setTimeout(() => {
            button.innerHTML = originalHTML
            button.style.cssText = originalStyle
            button.disabled = false
          }, 3e3)
        }
      })
      return button
    }
    function processCookedContent(cookedElement) {
      if (cookedElement.querySelector('.emoji-batch-parse-button')) return
      if (!cookedElement.querySelector('.lightbox-wrapper')) return
      const batchButton = createBatchParseButton(cookedElement)
      cookedElement.insertBefore(batchButton, cookedElement.firstChild)
    }
    function processCookedContents() {
      document.querySelectorAll('.cooked').forEach(element => {
        if (element.classList.contains('cooked') && element.querySelector('.lightbox-wrapper'))
          processCookedContent(element)
      })
    }
    function initBatchParseButtons() {
      setTimeout(processCookedContents, 500)
      new MutationObserver(mutations => {
        let hasNewCooked = false
        mutations.forEach(mutation => {
          if (mutation.type === 'childList')
            mutation.addedNodes.forEach(node => {
              if (node.nodeType === Node.ELEMENT_NODE) {
                const element = node
                if (element.classList && element.classList.contains('cooked')) hasNewCooked = true
                if (element.querySelectorAll && element.querySelectorAll('.cooked').length > 0)
                  hasNewCooked = true
              }
            })
        })
        if (hasNewCooked) setTimeout(processCookedContents, 100)
      }).observe(document.body, {
        childList: true,
        subtree: true
      })
    }
    init_createEl()
    init_userscript_storage()
    function extractNameFromUrl(url) {
      try {
        const nameWithoutExt = (new URL(url).pathname.split('/').pop() || '').replace(
          /\.[^/.]+$/,
          ''
        )
        const decoded = decodeURIComponent(nameWithoutExt)
        if (/^[0-9a-f]{8,}$/i.test(decoded) || decoded.length < 2) return '表情'
        return decoded || '表情'
      } catch {
        return '表情'
      }
    }
    function createAddButton(data) {
      const button = createEl('a', {
        className: 'emoji-add-link',
        style: `
      color:#fff;
      border-radius:6px;
      padding:4px 8px;
      margin:0 2px;
      display:inline-flex;
      align-items:center;
      font-weight:600;
      text-decoration:none;
      border: 1px solid rgba(255,255,255,0.7);
      cursor: pointer;
    `,
        title: '添加到未分组表情'
      })
      button.innerHTML = `添加表情`
      function addToUngrouped(emoji) {
        const data$1 = loadDataFromLocalStorage()
        let group = data$1.emojiGroups.find(g => g.id === 'ungrouped')
        if (!group) {
          group = {
            id: 'ungrouped',
            name: '未分组',
            icon: '📦',
            order: 999,
            emojis: []
          }
          data$1.emojiGroups.push(group)
        }
        if (!group.emojis.some(e => e.url === emoji.url || e.name === emoji.name)) {
          group.emojis.push({
            packet: Date.now(),
            name: emoji.name,
            url: emoji.url
          })
          saveDataToLocalStorage({ emojiGroups: data$1.emojiGroups })
        }
      }
      button.addEventListener('click', e => {
        e.preventDefault()
        e.stopPropagation()
        try {
          addToUngrouped({
            name: data.name,
            url: data.url
          })
          const original = button.textContent || ''
          button.textContent = '已添加'
          button.style.background = 'linear-gradient(135deg,#10b981,#059669)'
          setTimeout(() => {
            button.textContent = original || '添加表情'
            button.style.background = ''
          }, 1500)
        } catch (err) {
          console.warn('[Userscript] add emoji failed', err)
          const original = button.textContent || ''
          button.textContent = '失败'
          button.style.background = 'linear-gradient(135deg,#ef4444,#dc2626)'
          setTimeout(() => {
            button.textContent = original || '添加表情'
            button.style.background = 'linear-gradient(135deg, #4f46e5, #7c3aed)'
          }, 1500)
        }
      })
      return button
    }
    function addEmojiButtonToPswp(container) {
      const topBar =
        container.querySelector('.pswp__top-bar') ||
        (container.classList.contains('pswp__top-bar') ? container : null)
      if (!topBar) return
      if (topBar.querySelector('.emoji-add-link')) return
      const originalBtn = topBar.querySelector('.pswp__button--original-image')
      const downloadBtn = topBar.querySelector('.pswp__button--download-image')
      let imgUrl = ''
      if (originalBtn?.href) imgUrl = originalBtn.href
      else if (downloadBtn?.href) imgUrl = downloadBtn.href
      if (!imgUrl) return
      let name = ''
      const captionTitle = document.querySelector('.pswp__caption-title')
      if (captionTitle?.textContent?.trim()) name = captionTitle.textContent.trim()
      if (!name) {
        if (originalBtn?.title) name = originalBtn.title
        else if (downloadBtn?.title) name = downloadBtn.title
      }
      if (!name || name.length < 2) name = extractNameFromUrl(imgUrl)
      name = name.trim() || '表情'
      const addButton = createAddButton({
        name,
        url: imgUrl
      })
      if (downloadBtn?.parentElement)
        downloadBtn.parentElement.insertBefore(addButton, downloadBtn.nextSibling)
      else topBar.appendChild(addButton)
    }
    function scanForPhotoSwipeTopBar() {
      document.querySelectorAll('.pswp__top-bar').forEach(topBar => addEmojiButtonToPswp(topBar))
    }
    function observePhotoSwipeTopBar() {
      scanForPhotoSwipeTopBar()
      function debounce(fn, wait = 100) {
        let timer = null
        return (...args) => {
          if (timer !== null) window.clearTimeout(timer)
          timer = window.setTimeout(() => {
            timer = null
            fn(...args)
          }, wait)
        }
      }
      const debouncedScan = debounce(scanForPhotoSwipeTopBar, 100)
      const observer = new MutationObserver(mutations => {
        for (const m of mutations) {
          if (m.type === 'childList' && (m.addedNodes.length > 0 || m.removedNodes.length > 0)) {
            debouncedScan()
            return
          }
          if (m.type === 'attributes') {
            debouncedScan()
            return
          }
        }
      })
      observer.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: false
      })
      return observer
    }
    function initPhotoSwipeTopbarUserscript() {
      scanForPhotoSwipeTopBar()
      observePhotoSwipeTopBar()
    }
    function ensureStyleInjected(id, css) {
      const style = document.createElement('style')
      style.id = id
      style.textContent = css
      document.documentElement.appendChild(style)
    }
    const init_injectStyles = __esmMin(() => {})
    init_injectStyles()
    const da = document.addEventListener
    const calloutKeywords = [
      'note',
      'abstract',
      'summary',
      'tldr',
      'info',
      'todo',
      'tip',
      'hint',
      'success',
      'check',
      'done',
      'question',
      'help',
      'faq',
      'warning',
      'caution',
      'attention',
      'failure',
      'fail',
      'missing',
      'danger',
      'error',
      'bug',
      'example',
      'quote',
      'cite'
    ].sort()
    const ICONS$1 = {
      info: {
        icon: 'ℹ️',
        color: 'rgba(2, 122, 255, 0.06)',
        svg: '<svg class="fa d-icon d-icon-far-lightbulb svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#far-lightbulb"></use></svg>'
      },
      tip: {
        icon: '💡',
        color: 'rgba(0, 191, 188, 0.06)',
        svg: '<svg class="fa d-icon d-icon-fire-flame-curved svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#fire-flame-curved"></use></svg>'
      },
      faq: {
        icon: '❓',
        color: 'rgba(236, 117, 0, 0.06)',
        svg: '<svg class="fa d-icon d-icon-far-circle-question svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#far-circle-question"></use></svg>'
      },
      question: {
        icon: '🤔',
        color: 'rgba(236, 117, 0, 0.06)',
        svg: '<svg class="fa d-icon d-icon-far-circle-question svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#far-circle-question"></use></svg>'
      },
      note: {
        icon: '📝',
        color: 'rgba(8, 109, 221, 0.06)',
        svg: '<svg class="fa d-icon d-icon-far-pen-to-square svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#far-pen-to-square"></use></svg>'
      },
      abstract: {
        icon: '📋',
        color: 'rgba(0, 191, 188, 0.06)',
        svg: '<svg class="fa d-icon d-icon-far-clipboard svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#far-clipboard"></use></svg>'
      },
      todo: {
        icon: '☑️',
        color: 'rgba(2, 122, 255, 0.06)',
        svg: '<svg class="fa d-icon d-icon-far-circle-check svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#far-circle-check"></use></svg>'
      },
      success: {
        icon: '🎉',
        color: 'rgba(68, 207, 110, 0.06)',
        svg: '<svg class="fa d-icon d-icon-check svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#check"></use></svg>'
      },
      warning: {
        icon: '⚠️',
        color: 'rgba(236, 117, 0, 0.06)',
        svg: '<svg class="fa d-icon d-icon-triangle-exclamation svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#triangle-exclamation"></use></svg>'
      },
      failure: {
        icon: '❌',
        color: 'rgba(233, 49, 71, 0.06)',
        svg: '<svg class="fa d-icon d-icon-xmark svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#xmark"></use></svg>'
      },
      danger: {
        icon: '☠️',
        color: 'rgba(233, 49, 71, 0.06)',
        svg: '<svg class="fa d-icon d-icon-bolt svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#bolt"></use></svg>'
      },
      bug: {
        icon: '🐛',
        color: 'rgba(233, 49, 71, 0.06)',
        svg: '<svg class="fa d-icon d-icon-bug svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#bug"></use></svg>'
      },
      example: {
        icon: '🔎',
        color: 'rgba(120, 82, 238, 0.06)',
        svg: '<svg class="fa d-icon d-icon-list svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#list"></use></svg>'
      },
      quote: {
        icon: '💬',
        color: 'rgba(158, 158, 158, 0.06)',
        svg: '<svg class="fa d-icon d-icon-quote-left svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#quote-left"></use></svg>'
      }
    }
    const DEFAULT_ICON = {
      icon: '📝',
      color: 'var(--secondary-low)',
      svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor"><path d="M490.3 40.4C512.2 62.27 512.2 97.73 490.3 119.6L460.3 149.7 362.3 51.72 392.4 21.66C414.3-.2135 449.7-.2135 471.6 21.66L490.3 40.4zM172.4 241.7L339.7 74.34 437.7 172.3 270.3 339.6C264.2 345.8 256.7 350.4 248.4 352.1L159.6 372.9C152.1 374.7 144.3 373.1 138.6 367.4C132.9 361.7 131.3 353.9 133.1 346.4L153.9 257.6C155.6 249.3 160.2 241.8 166.4 235.7L172.4 241.7zM96 64C42.98 64 0 106.1 0 160V416C0 469 42.98 512 96 512H352C405 512 448 469 448 416V320H400V416C400 442.5 378.5 464 352 464H96C69.54 464 48 442.5 48 416V160C48 133.5 69.54 112 96 112H192V64H96z"/></svg>'
    }
    ICONS$1.summary = ICONS$1.tldr = ICONS$1.abstract
    ICONS$1.hint = ICONS$1.tip
    ICONS$1.check = ICONS$1.done = ICONS$1.success
    ICONS$1.help = ICONS$1.faq
    ICONS$1.caution = ICONS$1.attention = ICONS$1.warning
    ICONS$1.fail = ICONS$1.missing = ICONS$1.failure
    ICONS$1.error = ICONS$1.danger
    ICONS$1.cite = ICONS$1.quote
    let suggestionBox = null
    let activeSuggestionIndex = 0
    function createSuggestionBox() {
      if (suggestionBox) return
      suggestionBox = document.createElement('div')
      suggestionBox.id = 'userscript-callout-suggestion-box'
      document.body.appendChild(suggestionBox)
      injectStyles$1()
    }
    function injectStyles$1() {
      ensureStyleInjected(
        'userscript-callout-suggestion-styles',
        `
  #userscript-callout-suggestion-box {
    position: absolute;
    background-color: var(--secondary);
    border: 1px solid #444;
    border-radius: 6px;
    box-shadow: 0 4px 8px rgba(0,0,0,0.2);
    z-index: 999999;
    padding: 5px;
    display: none;
    font-size: 14px;
    max-height: 200px;
    overflow-y: auto;
  }
  .userscript-suggestion-item {
    padding: 8px 12px;
    cursor: pointer;
    color: var(--primary-high);
    border-radius: 4px;
    display: flex;
    align-items: center;
  }
  .userscript-suggestion-item:hover, .userscript-suggestion-item.active {
    background-color: var(--primary-low) !important;
  }
  `
      )
    }
    function hideSuggestionBox() {
      if (suggestionBox) suggestionBox.style.display = 'none'
    }
    function updateActiveSuggestion() {
      if (!suggestionBox) return
      suggestionBox.querySelectorAll('.userscript-suggestion-item').forEach((it, idx) => {
        it.classList.toggle('active', idx === activeSuggestionIndex)
        if (idx === activeSuggestionIndex) it.scrollIntoView({ block: 'nearest' })
      })
    }
    function applyCompletion(element, selectedKeyword) {
      if (element instanceof HTMLTextAreaElement) {
        const text = element.value
        const selectionStart = element.selectionStart || 0
        const textBeforeCursor = text.substring(0, selectionStart)
        let triggerIndex = textBeforeCursor.lastIndexOf('[')
        if (triggerIndex === -1) triggerIndex = textBeforeCursor.lastIndexOf('[')
        if (triggerIndex === -1) triggerIndex = textBeforeCursor.lastIndexOf('【')
        if (triggerIndex === -1) return
        const newText = `[!${selectedKeyword}]`
        const textAfter = text.substring(selectionStart)
        element.value = textBeforeCursor.substring(0, triggerIndex) + newText + textAfter
        element.selectionStart = element.selectionEnd = triggerIndex + newText.length
        element.dispatchEvent(new Event('input', { bubbles: true }))
      } else if (element.classList.contains('ProseMirror')) {
        const newText = `[!${selectedKeyword}]`
        try {
          const selection = window.getSelection()
          if (!selection || selection.rangeCount === 0) return
          const range = selection.getRangeAt(0)
          const textBeforeCursor =
            range.startContainer.textContent?.substring(0, range.startOffset) || ''
          let triggerIndex = textBeforeCursor.lastIndexOf('[')
          if (triggerIndex === -1) triggerIndex = textBeforeCursor.lastIndexOf('[')
          if (triggerIndex === -1) triggerIndex = textBeforeCursor.lastIndexOf('【')
          if (triggerIndex === -1) return
          const deleteRange = document.createRange()
          deleteRange.setStart(range.startContainer, triggerIndex)
          deleteRange.setEnd(range.startContainer, range.startOffset)
          deleteRange.deleteContents()
          const textNode = document.createTextNode(newText)
          deleteRange.insertNode(textNode)
          const newRange = document.createRange()
          newRange.setStartAfter(textNode)
          newRange.collapse(true)
          selection.removeAllRanges()
          selection.addRange(newRange)
          element.dispatchEvent(new Event('input', { bubbles: true }))
        } catch (e) {
          console.error('ProseMirror completion failed', e)
        }
      }
    }
    function getCursorXY(element, position) {
      if (element instanceof HTMLTextAreaElement) {
        const mirrorId = 'userscript-textarea-mirror-div'
        let mirror = document.getElementById(mirrorId)
        const rect = element.getBoundingClientRect()
        if (!mirror) {
          mirror = document.createElement('div')
          mirror.id = mirrorId
          document.body.appendChild(mirror)
        }
        const style = window.getComputedStyle(element)
        const props = [
          'boxSizing',
          'fontFamily',
          'fontSize',
          'fontWeight',
          'letterSpacing',
          'lineHeight',
          'textTransform',
          'textAlign',
          'direction',
          'paddingTop',
          'paddingRight',
          'paddingBottom',
          'paddingLeft',
          'borderTopWidth',
          'borderRightWidth',
          'borderBottomWidth',
          'borderLeftWidth'
        ]
        const ms = mirror.style
        props.forEach(p => {
          ms[p] = style.getPropertyValue(p)
        })
        ms.position = 'absolute'
        ms.left = `${rect.left + window.scrollX}px`
        ms.top = `${rect.top + window.scrollY}px`
        ms.width = `${rect.width}px`
        ms.height = `${rect.height}px`
        ms.overflow = 'hidden'
        ms.visibility = 'hidden'
        ms.whiteSpace = 'pre-wrap'
        ms.wordWrap = 'break-word'
        ms.boxSizing = style.getPropertyValue('box-sizing') || 'border-box'
        const cursorPosition = position !== void 0 ? position : element.selectionEnd
        mirror.textContent = element.value.substring(0, cursorPosition)
        const span = document.createElement('span')
        span.textContent = '​'
        mirror.appendChild(span)
        const spanRect = span.getBoundingClientRect()
        const offsetX = span.offsetLeft - element.scrollLeft
        const offsetY = span.offsetTop - element.scrollTop
        return {
          x: spanRect.left + window.scrollX,
          y: spanRect.top + window.scrollY,
          bottom: spanRect.bottom + window.scrollY,
          offsetX,
          offsetY
        }
      } else {
        const selection = window.getSelection()
        if (!selection || selection.rangeCount === 0) {
          const rect$1 = element.getBoundingClientRect()
          return {
            x: rect$1.left + window.scrollX,
            y: rect$1.top + window.scrollY,
            bottom: rect$1.bottom + window.scrollY,
            offsetX: 0,
            offsetY: 0
          }
        }
        const rect = selection.getRangeAt(0).getBoundingClientRect()
        return {
          x: rect.left + window.scrollX,
          y: rect.top + window.scrollY,
          bottom: rect.bottom + window.scrollY,
          offsetX: 0,
          offsetY: 0
        }
      }
    }
    function updateSuggestionBox(element, matches, triggerIndex) {
      if (!suggestionBox || matches.length === 0) {
        hideSuggestionBox()
        return
      }
      suggestionBox.innerHTML = matches
        .map((keyword, index) => {
          const iconData = ICONS$1[keyword] || DEFAULT_ICON
          const backgroundColor = iconData.color || 'transparent'
          const iconColor = iconData.color
            ? iconData.color.replace('rgba', 'rgb').replace(/, [0-9.]+\)/, ')')
            : 'var(--primary-medium)'
          return `\n      <div class="userscript-suggestion-item" data-index="${index}" data-key="${keyword}" style="background-color:${backgroundColor}">\n        ${(iconData.svg || DEFAULT_ICON.svg).replace('<svg', `<svg style="color: ${iconColor};"`)}\n        <span style="margin-left:8px">${keyword}</span>\n      </div>`
        })
        .join('')
      suggestionBox.querySelectorAll('.userscript-suggestion-item').forEach(item => {
        item.addEventListener('mousedown', e => {
          e.preventDefault()
          const idx = item.dataset.key
          if (!idx) return
          applyCompletion(element, idx)
          hideSuggestionBox()
        })
      })
      const cursorPos = getCursorXY(element, triggerIndex)
      const margin = 6
      const prevVisibility = suggestionBox.style.visibility
      suggestionBox.style.display = 'block'
      suggestionBox.style.visibility = 'hidden'
      const boxRect = suggestionBox.getBoundingClientRect()
      const spaceBelow = window.innerHeight - (cursorPos.bottom - window.scrollY)
      const left = cursorPos.x
      let top = cursorPos.y + margin
      if (spaceBelow < boxRect.height + margin) top = cursorPos.y - boxRect.height - margin
      const cursorViewportX = cursorPos.x - window.scrollX
      const viewportWidth = window.innerWidth
      const spaceRight = viewportWidth - cursorViewportX
      const spaceLeft = cursorViewportX
      let finalLeft = left
      if (spaceRight < boxRect.width + margin && spaceLeft >= boxRect.width + margin)
        finalLeft = cursorPos.x - boxRect.width
      const minLeft = window.scrollX + 0
      const maxLeft = window.scrollX + viewportWidth - boxRect.width - margin
      if (finalLeft < minLeft) finalLeft = minLeft
      if (finalLeft > maxLeft) finalLeft = maxLeft
      suggestionBox.style.left = `${finalLeft}px`
      suggestionBox.style.top = `${top}px`
      suggestionBox.style.visibility = prevVisibility || ''
      suggestionBox.style.display = 'block'
      activeSuggestionIndex = 0
      updateActiveSuggestion()
    }
    function handleInput(event) {
      const target = event.target
      if (!target) return
      if (target instanceof HTMLTextAreaElement) {
        const textarea = target
        const text = textarea.value
        const selectionStart = textarea.selectionStart || 0
        const match = text.substring(0, selectionStart).match(/(?:\[|[|【])(?:!|!)?([a-z]*)$/i)
        if (match) {
          const keyword = match[1].toLowerCase()
          const filtered = calloutKeywords.filter(k => k.startsWith(keyword))
          const triggerIndex = selectionStart - match[0].length
          if (filtered.length > 0) updateSuggestionBox(textarea, filtered, triggerIndex)
          else hideSuggestionBox()
        } else hideSuggestionBox()
      } else if (target.classList?.contains('ProseMirror')) {
        const selection = window.getSelection()
        if (!selection || selection.rangeCount === 0) {
          hideSuggestionBox()
          return
        }
        const range = selection.getRangeAt(0)
        const match = (
          range.startContainer.textContent?.substring(0, range.startOffset) || ''
        ).match(/(?:\[|[|【])(?:!|!)?([a-z]*)$/i)
        if (match) {
          const keyword = match[1].toLowerCase()
          const filtered = calloutKeywords.filter(k => k.startsWith(keyword))
          const triggerIndex = range.startOffset - match[0].length
          if (filtered.length > 0) updateSuggestionBox(target, filtered, triggerIndex)
          else hideSuggestionBox()
        } else hideSuggestionBox()
      }
    }
    function handleKeydown(event) {
      if (!suggestionBox || suggestionBox.style.display === 'none') return
      const items = suggestionBox.querySelectorAll('.userscript-suggestion-item')
      if (items.length === 0) return
      if (['ArrowDown', 'ArrowUp', 'Tab', 'Enter', 'Escape'].includes(event.key)) {
        event.preventDefault()
        event.stopPropagation()
      }
      switch (event.key) {
        case 'ArrowDown':
          activeSuggestionIndex = (activeSuggestionIndex + 1) % items.length
          updateActiveSuggestion()
          break
        case 'ArrowUp':
          activeSuggestionIndex = (activeSuggestionIndex - 1 + items.length) % items.length
          updateActiveSuggestion()
          break
        case 'Tab':
        case 'Enter': {
          const selectedKey = items[activeSuggestionIndex]?.dataset.key
          if (selectedKey) {
            const focused = document.activeElement
            if (focused) {
              if (focused instanceof HTMLTextAreaElement) applyCompletion(focused, selectedKey)
              else if (focused.classList?.contains('ProseMirror'))
                applyCompletion(focused, selectedKey)
            }
          }
          hideSuggestionBox()
          break
        }
        case 'Escape':
          hideSuggestionBox()
          break
      }
    }
    function initCalloutSuggestionsUserscript() {
      try {
        createSuggestionBox()
        da('input', handleInput, true)
        da('keydown', handleKeydown, true)
        da('click', e => {
          if (e.target?.tagName !== 'TEXTAREA' && !suggestionBox?.contains(e.target))
            hideSuggestionBox()
        })
      } catch (e) {
        console.error('initCalloutSuggestionsUserscript failed', e)
      }
    }
    function getBuildPlatform() {
      try {
        return 'original'
      } catch {
        return 'original'
      }
    }
    function detectRuntimePlatform() {
      try {
        const isMobileSize = window.innerWidth <= 768
        const userAgent = navigator.userAgent.toLowerCase()
        const isMobileUserAgent =
          /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent)
        const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0
        if (isMobileSize && (isMobileUserAgent || isTouchDevice)) return 'mobile'
        else if (!isMobileSize && !isMobileUserAgent) return 'pc'
        return 'original'
      } catch {
        return 'original'
      }
    }
    function getEffectivePlatform() {
      const buildPlatform = getBuildPlatform()
      if (buildPlatform === 'original') return detectRuntimePlatform()
      return buildPlatform
    }
    function getPlatformUIConfig() {
      switch (getEffectivePlatform()) {
        case 'mobile':
          return {
            emojiPickerMaxHeight: '60vh',
            emojiPickerColumns: 4,
            emojiSize: 32,
            isModal: true,
            useCompactLayout: true,
            showSearchBar: true,
            floatingButtonSize: 48
          }
        case 'pc':
          return {
            emojiPickerMaxHeight: '400px',
            emojiPickerColumns: 6,
            emojiSize: 24,
            isModal: false,
            useCompactLayout: false,
            showSearchBar: true,
            floatingButtonSize: 40
          }
        default:
          return {
            emojiPickerMaxHeight: '350px',
            emojiPickerColumns: 5,
            emojiSize: 28,
            isModal: false,
            useCompactLayout: false,
            showSearchBar: true,
            floatingButtonSize: 44
          }
      }
    }
    function getPlatformToolbarSelectors() {
      const platform = getEffectivePlatform()
      const baseSelectors = [
        '.d-editor-button-bar[role="toolbar"]',
        '.chat-composer__inner-container',
        '.d-editor-button-bar'
      ]
      switch (platform) {
        case 'mobile':
          return [
            ...baseSelectors,
            '.mobile-composer .d-editor-button-bar',
            '.discourse-mobile .d-editor-button-bar',
            '[data-mobile-toolbar]'
          ]
        case 'pc':
          return [
            ...baseSelectors,
            '.desktop-composer .d-editor-button-bar',
            '.discourse-desktop .d-editor-button-bar',
            '[data-desktop-toolbar]'
          ]
        default:
          return baseSelectors
      }
    }
    function logPlatformInfo() {
      const buildPlatform = getBuildPlatform()
      const runtimePlatform = detectRuntimePlatform()
      const effectivePlatform = getEffectivePlatform()
      const config = getPlatformUIConfig()
      console.log('[Platform] Build target:', buildPlatform)
      console.log('[Platform] Runtime detected:', runtimePlatform)
      console.log('[Platform] Effective platform:', effectivePlatform)
      console.log('[Platform] UI config:', config)
      console.log('[Platform] Screen size:', `${window.innerWidth}x${window.innerHeight}`)
      console.log(
        '[Platform] User agent mobile:',
        /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
          navigator.userAgent.toLowerCase()
        )
      )
      console.log(
        '[Platform] Touch device:',
        'ontouchstart' in window || navigator.maxTouchPoints > 0
      )
    }
    function ensureHoverPreview() {
      if (_sharedPreview && document.body.contains(_sharedPreview)) return _sharedPreview
      _sharedPreview = createEl('div', {
        className: 'emoji-picker-hover-preview',
        style:
          'position:fixed;pointer-events:none;display:none;z-index:1000002;max-width:300px;max-height:300px;overflow:hidden;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,0.25);background:transparent;padding:6px;'
      })
      const img = createEl('img', {
        className: 'emoji-picker-hover-img',
        style: 'display:block;max-width:100%;max-height:220px;object-fit:contain;'
      })
      const label = createEl('div', {
        className: 'emoji-picker-hover-label',
        style: 'font-size:12px;color:var(--primary);margin-top:6px;text-align:center;'
      })
      _sharedPreview.appendChild(img)
      _sharedPreview.appendChild(label)
      document.body.appendChild(_sharedPreview)
      return _sharedPreview
    }
    let _sharedPreview
    const init_hoverPreview = __esmMin(() => {
      init_createEl()
      _sharedPreview = null
    })
    function injectGlobalThemeStyles() {
      if (themeStylesInjected || typeof document === 'undefined') return
      themeStylesInjected = true
      document.head.appendChild(
        createEl('style', {
          id: 'emoji-extension-theme-globals',
          text: `
    /* Global CSS variables for emoji extension theme support */
    :root {
      /* Light theme (default) */
      --emoji-modal-bg: #ffffff;
      --emoji-modal-text: #333333;
      --emoji-modal-border: #dddddd;
      --emoji-modal-input-bg: #ffffff;
      --emoji-modal-label: #555555;
      --emoji-modal-button-bg: #f5f5f5;
      --emoji-modal-primary-bg: #1890ff;
      
      --emoji-preview-bg: #ffffff;
      --emoji-preview-text: #222222;
      --emoji-preview-border: rgba(0,0,0,0.08);
      
      --emoji-button-gradient-start: #667eea;
      --emoji-button-gradient-end: #764ba2;
      --emoji-button-shadow: rgba(0, 0, 0, 0.15);
      --emoji-button-hover-shadow: rgba(0, 0, 0, 0.2);
    }
    
    /* Dark theme */
    @media (prefers-color-scheme: dark) {
      :root {
        --emoji-modal-bg: #2d2d2d;
        --emoji-modal-text: #e6e6e6;
        --emoji-modal-border: #444444;
        --emoji-modal-input-bg: #3a3a3a;
        --emoji-modal-label: #cccccc;
        --emoji-modal-button-bg: #444444;
        --emoji-modal-primary-bg: #1677ff;
        
        --emoji-preview-bg: rgba(32,33,36,0.94);
        --emoji-preview-text: #e6e6e6;
        --emoji-preview-border: rgba(255,255,255,0.12);
        
        --emoji-button-gradient-start: #4a5568;
        --emoji-button-gradient-end: #2d3748;
        --emoji-button-shadow: rgba(0, 0, 0, 0.3);
        --emoji-button-hover-shadow: rgba(0, 0, 0, 0.4);
      }
    }
  `
        })
      )
    }
    let themeStylesInjected
    const init_themeSupport = __esmMin(() => {
      init_createEl()
      themeStylesInjected = false
    })
    init_hoverPreview()
    init_themeSupport()
    init_injectStyles()
    function injectEmojiPickerStyles() {
      if (typeof document === 'undefined') return
      if (document.getElementById('emoji-picker-styles')) return
      injectGlobalThemeStyles()
      ensureStyleInjected(
        'emoji-picker-styles',
        `
.emoji-picker-hover-preview{
  position:fixed;
  pointer-events:none;
  display:none;
  z-index:1000002;
  max-width:320px;
  max-height:320px;
  overflow:hidden;
  border-radius:8px;
  box-shadow:0 6px 20px rgba(0,0,0,0.32);
  background:var(--emoji-preview-bg);
  padding:8px;
  transition:opacity .3s ease, transform .12s ease;
  border: 1px solid var(--emoji-preview-border);
  backdrop-filter: blur(10px);
}
.emoji-picker-hover-preview img.emoji-picker-hover-img{
  display:block;
  max-width:100%;
  max-height:220px;
  object-fit:contain;
}
.emoji-picker-hover-preview .emoji-picker-hover-label{
  font-size:12px;
  color:var(--emoji-preview-text);
  margin-top:8px;
  text-align:center;
  word-break:break-word;
  font-weight: 500;
}
`
      )
    }
    function isImageUrl(value) {
      if (!value) return false
      let v = value.trim()
      if (/^url\(/i.test(v)) {
        const inner = v
          .replace(/^url\(/i, '')
          .replace(/\)$/, '')
          .trim()
        if (
          (inner.startsWith('"') && inner.endsWith('"')) ||
          (inner.startsWith("'") && inner.endsWith("'"))
        )
          v = inner.slice(1, -1).trim()
        else v = inner
      }
      if (v.startsWith('data:image/')) return true
      if (v.startsWith('blob:')) return true
      if (v.startsWith('//')) v = 'https:' + v
      if (/\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)(\?.*)?$/i.test(v)) return true
      try {
        const url = new URL(v)
        const protocol = url.protocol
        if (protocol === 'http:' || protocol === 'https:' || protocol.endsWith(':')) {
          if (/\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)(\?.*)?$/i.test(url.pathname)) return true
          if (/format=|ext=|type=image|image_type=/i.test(url.search)) return true
        }
      } catch {}
      return false
    }
    const __vitePreload = function preload(baseModule, deps, importerUrl) {
      const promise = Promise.resolve()
      function handlePreloadError(err$2) {
        const e$1 = new Event('vite:preloadError', { cancelable: true })
        e$1.payload = err$2
        window.dispatchEvent(e$1)
        if (!e$1.defaultPrevented) throw err$2
      }
      return promise.then(res => {
        for (const item of res || []) {
          if (item.status !== 'rejected') continue
          handlePreloadError(item.reason)
        }
        return baseModule().catch(handlePreloadError)
      })
    }
    function injectManagerStyles() {
      if (__managerStylesInjected) return
      __managerStylesInjected = true
      ensureStyleInjected(
        'emoji-manager-styles',
        `
    /* Modal backdrop */
    .emoji-manager-wrapper { 
      position: fixed; 
      top: 0; 
      left: 0; 
      right: 0; 
      bottom: 0; 
      z-index: 999999; 
      display: flex; 
      align-items: center; 
      justify-content: center; 
    }
    
    /* Main modal panel */
    .emoji-manager-panel { 
      border-radius: 8px; 
      width: 90%; 
      height: 95%; 
      display: grid; 
      grid-template-columns: 300px 1fr; 
      grid-template-rows: 1fr auto;
      overflow: hidden; 
      box-shadow: 0 10px 40px rgba(0,0,0,0.3); 
    }
    
    /* Left panel - groups list */
    .emoji-manager-left { 
      background: var(--primary-very-low) 
      border-right: 1px solid #e9ecef; 
      display: flex; 
      flex-direction: column; 
      overflow: hidden; 
    }
    
    .emoji-manager-left-header { 
      display: flex; 
      align-items: center; 
      padding: 16px; 
      background: var(--primary-low); 
    }
    
    .emoji-manager-addgroup-row { 
      display: flex; 
      padding: 12px; 
    }
    
    .emoji-manager-groups-list { 
      background: var(--primary-very-low);
      flex: 1; 
      overflow-y: auto; 
      padding: 8px; 
    }
    
    .emoji-manager-groups-list > div { 
      margin-bottom: 4px; 
      transition: background-color 0.2s; 
    }
    
    .emoji-manager-groups-list > div:hover { 
      background: var(--primary); 
    }
    
    .emoji-manager-groups-list > div:focus { 
      outline: none; 
      box-shadow: inset 0 0 0 2px #007bff; 
      background: var(--primary); 
    }
    
    /* Right panel - emoji display and editing */
    .emoji-manager-right { 
      background: var(--primary-low); 
      display: flex; 
      flex-direction: column; 
      overflow: hidden; 
    }
    
    .emoji-manager-right-header { 
      display: flex; 
      align-items: center; 
      justify-content: space-between; 
      padding: 16px; 
    }
    
    .emoji-manager-right-main { 
      flex: 1; 
      overflow-y: auto; 
    }
    
    .emoji-manager-emojis { 
      display: grid; 
      grid-template-columns: repeat(auto-fill, minmax(25%, 1fr)); 
      gap: 12px; 
    }
    
    .emoji-manager-card { 
      display: flex; 
      flex-direction: column; 
      align-items: center; 
      padding: 12px; 
      background: var(--primary-medium); 
    }
    
    .emoji-manager-card:hover { 
      transform: translateY(-2px); 
      box-shadow: 0 4px 12px rgba(0,0,0,0.1); 
    }
    
    .emoji-manager-card-img { 
      max-width: 90%;
      max-height: 100%; /* allow tall images but cap at viewport height */
      object-fit: contain; 
      border-radius: 6px; 
      background: white; 
    }
    
    .emoji-manager-card-name { 
      font-size: 12px; 
      color: var(--primary); 
      text-align: center; 
      width: 100%; 
      overflow: hidden; 
      white-space: nowrap; 
      text-overflow: ellipsis; 
      font-weight: 500; 
    }
    
    .emoji-manager-card-actions { 
      display: flex; 
      gap: 6px; 
    }
    
    /* Add emoji form */
    .emoji-manager-add-emoji-form { 
      padding: 16px; 
      background: var(--primary-very-low) 
      border-top: 1px solid #e9ecef; 
      display: flex; 
      gap: 8px; 
      align-items: center; 
    }
    
    /* Footer */
    .emoji-manager-footer { 
      grid-column: 1 / -1;
      display: flex; 
      justify-content: space-between; 
      padding: 16px; 
      background: var(--primary-very-low); 
    }
    
    /* Editor panel - popup modal */
    .emoji-manager-editor-panel { 
      position: fixed; 
      top: 50%; 
      left: 50%; 
      transform: translate(-50%, -50%); 
      background: var( --primary-medium ); 
      padding: 2%; 
      z-index: 1000000; 
    }
    
    .emoji-manager-editor-preview { 
      max-width: 100%;
      max-height: 40vh;
    }

    /* Hover preview (moved from inline styles) */
    .emoji-manager-hover-preview {
      position: fixed;
      pointer-events: none;
      z-index: 1000002;
      display: none;
      max-width: 60%;
      max-height: 60%;
      border: 1px solid rgba(0,0,0,0.1);
      object-fit: contain;
      background: var(--primary);
      padding: 4px;
      border-radius: 6px;
      box-shadow: 0 6px 18px rgba(0,0,0,0.12);
    }
    
    /* Form styling */
    .form-control { 
      width: 100%; 
      display: flex;
    }
    
    .btn { 
      padding: 8px 16px; 
      border: 1px solid transparent; 
      border-radius: 4px; 
      font-size: 14px; 
      cursor: pointer; 
      transition: all 0.2s; 
    }
    
    .btn-primary { 
      background-color: var(--primary);
      color: white; 
    }
    
    .btn-primary:hover { 
      background-color: var(--primary-high);
    }
    
    .btn-sm { 
      padding: 4px 8px; 
      font-size: 12px; 
    }
  `
      )
    }
    let __managerStylesInjected
    const init_styles = __esmMin(() => {
      init_injectStyles()
      __managerStylesInjected = false
    })
    function showTemporaryMessage(message, duration = 2e3) {
      const messageEl = createEl('div', {
        style: `
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      background: var(--emoji-modal-primary-bg);
      color: white;
      padding: 12px 24px;
      border-radius: 6px;
      z-index: 9999999;
      font-size: 14px;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
      animation: fadeInOut 2s ease-in-out;
    `,
        text: message
      })
      if (!document.querySelector('#tempMessageStyles')) {
        const style = createEl('style', {
          id: 'tempMessageStyles',
          text: `
      @keyframes fadeInOut {
        0%, 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.9); }
        20%, 80% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
      }
    `
        })
        document.head.appendChild(style)
      }
      document.body.appendChild(messageEl)
      setTimeout(() => {
        try {
          messageEl.remove()
        } catch {}
      }, duration)
    }
    const init_tempMessage = __esmMin(() => {
      init_createEl()
    })
    function showImportExportModal(currentGroupId) {
      injectGlobalThemeStyles()
      const modal = createEl('div', {
        style: `
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      z-index: 999999;
      display: flex;
      align-items: center;
      justify-content: center;
      background: rgba(0, 0, 0, 0.5);
    `
      })
      const content = createEl('div', {
        style: `
      background: var(--secondary);
      color: var(--emoji-modal-text);
      border: 1px solid var(--emoji-modal-border);
      border-radius: 8px;
      padding: 24px;
      max-width: 90%;
      max-height: 90%;
      overflow-y: auto;
      position: relative;
      width: 600px;
    `
      })
      const currentGroup = currentGroupId
        ? userscriptState.emojiGroups.find(g => g.id === currentGroupId)
        : null
      content.innerHTML = `
    <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
      <h2 style="margin: 0; color: var(--emoji-modal-text);">分组表情导入/导出</h2>
      <button id="closeModal" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #999;">×</button>
    </div>

    ${
      currentGroup
        ? `
    <div style="margin-bottom: 24px; padding: 16px; background: var(--emoji-modal-button-bg); border-radius: 8px;">
      <h3 style="margin: 0 0 12px 0; color: var(--emoji-modal-label);">当前分组信息</h3>
      <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
        ${currentGroup.icon?.startsWith('http') ? `<img src="${currentGroup.icon}" alt="图标" style="width: 24px; height: 24px; object-fit: contain;">` : `<span style="font-size: 20px;">${currentGroup.icon || '📁'}</span>`}
        <span style="font-weight: bold; color: var(--emoji-modal-text);">${currentGroup.name || currentGroup.id}</span>
      </div>
      <div style="color: var(--emoji-modal-text); font-size: 14px;">
        分组 ID: ${currentGroup.id} | 表情数量:${currentGroup.emojis?.length || 0}
      </div>
    </div>
    `
        : ''
    }

    <div style="margin-bottom: 24px;">
      <h3 style="margin: 0 0 12px 0; color: var(--emoji-modal-label);">导出分组表情</h3>
      
      <div style="margin-bottom: 16px;">
        <label style="display: block; margin-bottom: 8px; color: var(--emoji-modal-label);">选择要导出的分组:</label>
        <select id="exportGroupSelect" style="
          width: 100%;
          padding: 8px;
          background: var(--emoji-modal-button-bg);
          color: var(--emoji-modal-text);
          border: 1px solid var(--emoji-modal-border);
          border-radius: 4px;
          margin-bottom: 8px;
        ">
          ${currentGroup ? `<option value="${currentGroup.id}" selected>${currentGroup.name || currentGroup.id} (${currentGroup.emojis?.length || 0} 表情)</option>` : ''}
          ${userscriptState.emojiGroups
            .filter(g => g.id !== currentGroupId)
            .map(
              group =>
                `<option value="${group.id}">${group.name || group.id} (${group.emojis?.length || 0} 表情)</option>`
            )
            .join('')}
        </select>
      </div>

      <div style="display: flex; gap: 8px;">
        <button id="exportGroup" style="padding: 8px 16px; background: var(--emoji-modal-primary-bg); color: white; border: none; border-radius: 4px; cursor: pointer;">导出选中分组</button>
      </div>
    </div>

    <div>
      <h3 style="margin: 0 0 12px 0; color: var(--emoji-modal-label);">导入分组表情</h3>
      
      <div style="margin-bottom: 16px;">
        <label style="display: block; margin-bottom: 8px; color: var(--emoji-modal-label);">导入目标分组:</label>
        <select id="importTargetGroupSelect" style="
          width: 100%;
          padding: 8px;
          background: var(--emoji-modal-button-bg);
          color: var(--emoji-modal-text);
          border: 1px solid var(--emoji-modal-border);
          border-radius: 4px;
          margin-bottom: 8px;
        ">
          ${currentGroup ? `<option value="${currentGroup.id}" selected>${currentGroup.name || currentGroup.id}</option>` : ''}
          ${userscriptState.emojiGroups
            .filter(g => g.id !== currentGroupId)
            .map(group => `<option value="${group.id}">${group.name || group.id}</option>`)
            .join('')}
          <option value="__new__">创建新分组...</option>
        </select>
      </div>

      <div style="margin-bottom: 16px;">
        <label style="display: block; margin-bottom: 8px; color: var(--emoji-modal-label);">上传分组文件:</label>
        <input type="file" id="importFile" accept=".json" style="margin-bottom: 8px; color: var(--emoji-modal-text);">
        <div style="font-size: 12px; color: var(--emoji-modal-text); opacity: 0.7;">
          支持 JSON 格式的分组文件
        </div>
      </div>

      <div style="margin-bottom: 16px;">
        <label style="display: block; margin-bottom: 8px; color: var(--emoji-modal-label);">或粘贴分组 JSON:</label>
        <textarea id="importText" placeholder="在此粘贴分组表情 JSON..." style="
          width: 100%;
          height: 120px;
          padding: 8px;
          background: var(--emoji-modal-button-bg);
          color: var(--emoji-modal-text);
          border: 1px solid var(--emoji-modal-border);
          border-radius: 4px;
          resize: vertical;
          font-family: monospace;
          font-size: 12px;
        "></textarea>
      </div>

      <div style="margin-bottom: 16px;">
        <label style="display: block; margin-bottom: 8px; color: var(--emoji-modal-label);">导入选项:</label>
        <div style="display: flex; flex-direction: column; gap: 8px;">
          <label style="display: flex; align-items: center; color: var(--emoji-modal-text);">
            <input type="radio" name="importMode" value="replace" checked style="margin-right: 8px;">
            替换现有表情 (清空目标分组后导入)
          </label>
          <label style="display: flex; align-items: center; color: var(--emoji-modal-text);">
            <input type="radio" name="importMode" value="merge" style="margin-right: 8px;">
            合并表情 (添加到现有表情中,跳过重复的)
          </label>
          <label style="display: flex; align-items: center; color: var(--emoji-modal-text);">
            <input type="radio" name="importMode" value="append" style="margin-right: 8px;">
            追加表情 (直接添加到现有表情后面)
          </label>
        </div>
      </div>

      <div style="display: flex; gap: 8px;">
        <button id="importGroup" style="padding: 8px 16px; background: var(--emoji-modal-primary-bg); color: white; border: none; border-radius: 4px; cursor: pointer;">导入到分组</button>
        <button id="previewImport" style="padding: 8px 16px; background: var(--emoji-modal-button-bg); color: var(--emoji-modal-text); border: 1px solid var(--emoji-modal-border); border-radius: 4px; cursor: pointer;">预览导入</button>
      </div>
    </div>
  `
      modal.appendChild(content)
      document.body.appendChild(modal)
      function createDownload(data, filename) {
        const jsonString = JSON.stringify(data, null, 2)
        const blob = new Blob([jsonString], { type: 'application/json' })
        const url = URL.createObjectURL(blob)
        const a = document.createElement('a')
        a.href = url
        a.download = filename
        document.body.appendChild(a)
        a.click()
        document.body.removeChild(a)
        URL.revokeObjectURL(url)
      }
      function parseImportData(jsonData) {
        try {
          const data = JSON.parse(jsonData)
          if (!data || typeof data !== 'object') throw new Error('无效的 JSON 格式')
          return data
        } catch (error) {
          throw new Error(
            'JSON 解析失败:' + (error instanceof Error ? error.message : String(error))
          )
        }
      }
      content.querySelector('#closeModal')?.addEventListener('click', () => {
        modal.remove()
      })
      modal.addEventListener('click', e => {
        if (e.target === modal) modal.remove()
      })
      content.querySelector('#exportGroup')?.addEventListener('click', () => {
        try {
          const selectedGroupId = content.querySelector('#exportGroupSelect').value
          if (!selectedGroupId) {
            alert('请选择要导出的分组')
            return
          }
          const group = userscriptState.emojiGroups.find(g => g.id === selectedGroupId)
          if (!group) {
            alert('找不到指定的分组')
            return
          }
          const exportData = {
            type: 'emoji_group',
            exportDate: /* @__PURE__ */ new Date().toISOString(),
            group: {
              id: group.id,
              name: group.name,
              icon: group.icon,
              emojis: group.emojis || [],
              order: group.order
            }
          }
          const timestamp = /* @__PURE__ */ new Date().toISOString().slice(0, 19).replace(/:/g, '-')
          createDownload(exportData, `emoji-group-${group.name || group.id}-${timestamp}.json`)
          showTemporaryMessage(
            `已导出分组 "${group.name || group.id}" (${group.emojis?.length || 0} 个表情)`
          )
        } catch (error) {
          console.error('Export group failed:', error)
          alert('导出分组失败:' + (error instanceof Error ? error.message : String(error)))
        }
      })
      content.querySelector('#importFile')?.addEventListener('change', e => {
        const file = e.target.files?.[0]
        if (file) {
          const reader = new FileReader()
          reader.onload = event => {
            const text = event.target?.result
            const importTextarea = content.querySelector('#importText')
            if (importTextarea) importTextarea.value = text
          }
          reader.onerror = () => {
            alert('文件读取失败')
          }
          reader.readAsText(file)
        }
      })
      content.querySelector('#previewImport')?.addEventListener('click', () => {
        try {
          const importText = content.querySelector('#importText').value.trim()
          if (!importText) {
            alert('请输入或选择要导入的内容')
            return
          }
          const data = parseImportData(importText)
          let preview = '导入预览:\\n\\n'
          if (data.type === 'emoji_group' && data.group) {
            const group = data.group
            preview += `分组类型:单个表情分组\\n`
            preview += `分组名称:${group.name || group.id || 'Unnamed'}\\n`
            preview += `分组 ID: ${group.id || 'N/A'}\\n`
            preview += `图标:${group.icon || '无'}\\n`
            preview += `表情数量:${group.emojis?.length || 0}\\n\\n`
            if (group.emojis && group.emojis.length > 0) {
              preview += `表情列表 (前 5 个):\\n`
              group.emojis.slice(0, 5).forEach((emoji, index) => {
                preview += `  ${index + 1}. ${emoji.name || 'Unnamed'} - ${emoji.url || 'No URL'}\\n`
              })
              if (group.emojis.length > 5)
                preview += `  ... 还有 ${group.emojis.length - 5} 个表情\\n`
            }
          } else if (data.emojiGroups && Array.isArray(data.emojiGroups)) {
            preview += `分组类型:多个表情分组\\n`
            preview += `分组数量:${data.emojiGroups.length}\\n\\n`
            data.emojiGroups.slice(0, 3).forEach((group, index) => {
              preview += `${index + 1}. ${group.name || group.id || 'Unnamed'} (${group.emojis?.length || 0} 表情)\\n`
            })
            if (data.emojiGroups.length > 3)
              preview += `... 还有 ${data.emojiGroups.length - 3} 个分组\\n`
          } else if (Array.isArray(data) && data.length > 0 && data[0].id && data[0].url) {
            preview += `分组类型:表情数组 (带扩展字段)\\n`
            preview += `表情数量:${data.length}\\n\\n`
            const groupIds = [...new Set(data.map(emoji => emoji.groupId).filter(Boolean))]
            if (groupIds.length > 0) preview += `包含的原始分组 ID: ${groupIds.join(', ')}\\n\\n`
            if (data.length > 0) {
              preview += `表情列表 (前 5 个):\\n`
              data.slice(0, 5).forEach((emoji, index) => {
                preview += `  ${index + 1}. ${emoji.name || emoji.id} - ${emoji.url}\\n`
                if (emoji.groupId) preview += `     原分组:${emoji.groupId}\\n`
              })
              if (data.length > 5) preview += `  ... 还有 ${data.length - 5} 个表情\\n`
            }
          } else preview += '无法识别的格式,可能不是有效的分组导出文件'
          alert(preview)
        } catch (error) {
          alert('预览失败:' + (error instanceof Error ? error.message : String(error)))
        }
      })
      content.querySelector('#importGroup')?.addEventListener('click', () => {
        try {
          const importText = content.querySelector('#importText').value.trim()
          if (!importText) {
            alert('请输入或选择要导入的内容')
            return
          }
          let targetGroupId = content.querySelector('#importTargetGroupSelect').value
          if (targetGroupId === '__new__') {
            const newGroupName = prompt('请输入新分组的名称:')
            if (!newGroupName || !newGroupName.trim()) return
            const newGroupId = 'imported_' + Date.now()
            const newGroup = {
              id: newGroupId,
              name: newGroupName.trim(),
              icon: '📁',
              emojis: [],
              order: userscriptState.emojiGroups.length
            }
            userscriptState.emojiGroups.push(newGroup)
            targetGroupId = newGroupId
          }
          if (!targetGroupId) {
            alert('请选择目标分组')
            return
          }
          const targetGroup = userscriptState.emojiGroups.find(g => g.id === targetGroupId)
          if (!targetGroup) {
            alert('找不到目标分组')
            return
          }
          const data = parseImportData(importText)
          const importModeInputs = content.querySelectorAll('input[name="importMode"]')
          const importMode =
            Array.from(importModeInputs).find(input => input.checked)?.value || 'replace'
          let importedEmojis = []
          if (data.type === 'emoji_group' && data.group && data.group.emojis)
            importedEmojis = data.group.emojis
          else if (data.emojiGroups && Array.isArray(data.emojiGroups))
            importedEmojis = data.emojiGroups.reduce((acc, group) => {
              return acc.concat(group.emojis || [])
            }, [])
          else if (Array.isArray(data.emojis)) importedEmojis = data.emojis
          else if (Array.isArray(data) && data.length > 0 && data[0].id && data[0].url)
            importedEmojis = data.map(emoji => ({
              name: emoji.name || emoji.id || 'unnamed',
              url: emoji.url,
              width: emoji.width,
              height: emoji.height,
              originalId: emoji.id,
              packet: emoji.packet,
              originalGroupId: emoji.groupId
            }))
          else {
            alert('无法识别的导入格式')
            return
          }
          if (importedEmojis.length === 0) {
            alert('导入文件中没有找到表情数据')
            return
          }
          let finalEmojis = []
          switch (importMode) {
            case 'replace':
              finalEmojis = importedEmojis
              break
            case 'merge':
              const existingUrls = new Set((targetGroup.emojis || []).map(e => e.url))
              const existingIds = new Set(
                (targetGroup.emojis || []).map(e => e.originalId || e.id).filter(Boolean)
              )
              const newEmojis = importedEmojis.filter(e => {
                if (existingUrls.has(e.url)) return false
                if (e.originalId && existingIds.has(e.originalId)) return false
                return true
              })
              finalEmojis = [...(targetGroup.emojis || []), ...newEmojis]
              break
            case 'append':
              finalEmojis = [...(targetGroup.emojis || []), ...importedEmojis]
              break
            default:
              finalEmojis = importedEmojis
          }
          targetGroup.emojis = finalEmojis
          saveDataToLocalStorage({ emojiGroups: userscriptState.emojiGroups })
          const message = `成功导入 ${importedEmojis.length} 个表情到分组 "${targetGroup.name || targetGroup.id}"`
          showTemporaryMessage(message)
          alert(message + '\\n\\n修改已保存,分组现在共有 ' + finalEmojis.length + ' 个表情')
          modal.remove()
        } catch (error) {
          console.error('Import group failed:', error)
          alert('导入分组失败:' + (error instanceof Error ? error.message : String(error)))
        }
      })
    }
    const init_importExport = __esmMin(() => {
      init_userscript_storage()
      init_createEl()
      init_themeSupport()
      init_tempMessage()
    })
    const manager_exports = /* @__PURE__ */ __export({
      openManagementInterface: () => openManagementInterface
    })
    function createEditorPopup(groupId, index, renderGroups, renderSelectedGroup) {
      const group = userscriptState.emojiGroups.find(g => g.id === groupId)
      if (!group) return
      const emo = group.emojis[index]
      if (!emo) return
      const backdrop = createEl('div', {
        style: `
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0,0,0,0.5);
    z-index: 1000000;
    display: flex;
    align-items: center;
    justify-content: center;
  `
      })
      const editorPanel = createEl('div', { className: 'emoji-manager-editor-panel' })
      const editorTitle = createEl('h3', {
        text: '编辑表情',
        className: 'emoji-manager-editor-title',
        style: 'margin: 0 0 16px 0; text-align: center;'
      })
      const editorPreview = createEl('img', { className: 'emoji-manager-editor-preview' })
      editorPreview.src = emo.url
      const editorWidthInput = createEl('input', {
        className: 'form-control',
        placeholder: '宽度 (px) 可选',
        value: emo.width ? String(emo.width) : ''
      })
      const editorHeightInput = createEl('input', {
        className: 'form-control',
        placeholder: '高度 (px) 可选',
        value: emo.height ? String(emo.height) : ''
      })
      const editorNameInput = createEl('input', {
        className: 'form-control',
        placeholder: '名称 (alias)',
        value: emo.name || ''
      })
      const editorUrlInput = createEl('input', {
        className: 'form-control',
        placeholder: '表情图片 URL',
        value: emo.url || ''
      })
      const buttonContainer = createEl('div', {
        style: 'display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px;'
      })
      const editorSaveBtn = createEl('button', {
        text: '保存修改',
        className: 'btn btn-primary'
      })
      const editorCancelBtn = createEl('button', {
        text: '取消',
        className: 'btn'
      })
      buttonContainer.appendChild(editorCancelBtn)
      buttonContainer.appendChild(editorSaveBtn)
      editorPanel.appendChild(editorTitle)
      editorPanel.appendChild(editorPreview)
      editorPanel.appendChild(editorWidthInput)
      editorPanel.appendChild(editorHeightInput)
      editorPanel.appendChild(editorNameInput)
      editorPanel.appendChild(editorUrlInput)
      editorPanel.appendChild(buttonContainer)
      backdrop.appendChild(editorPanel)
      document.body.appendChild(backdrop)
      editorUrlInput.addEventListener('input', () => {
        editorPreview.src = editorUrlInput.value
      })
      editorSaveBtn.addEventListener('click', () => {
        const newName = (editorNameInput.value || '').trim()
        const newUrl = (editorUrlInput.value || '').trim()
        const newWidth = parseInt((editorWidthInput.value || '').trim(), 10)
        const newHeight = parseInt((editorHeightInput.value || '').trim(), 10)
        if (!newName || !newUrl) {
          alert('名称和 URL 均不能为空')
          return
        }
        emo.name = newName
        emo.url = newUrl
        if (!isNaN(newWidth) && newWidth > 0) emo.width = newWidth
        else delete emo.width
        if (!isNaN(newHeight) && newHeight > 0) emo.height = newHeight
        else delete emo.height
        renderGroups()
        renderSelectedGroup()
        backdrop.remove()
      })
      editorCancelBtn.addEventListener('click', () => {
        backdrop.remove()
      })
      backdrop.addEventListener('click', e => {
        if (e.target === backdrop) backdrop.remove()
      })
    }
    function openManagementInterface() {
      injectManagerStyles()
      const modal = createEl('div', {
        className: 'emoji-manager-wrapper',
        attrs: {
          role: 'dialog',
          'aria-modal': 'true'
        }
      })
      const panel = createEl('div', { className: 'emoji-manager-panel' })
      const left = createEl('div', { className: 'emoji-manager-left' })
      const leftHeader = createEl('div', { className: 'emoji-manager-left-header' })
      const title = createEl('h3', { text: '表情管理器' })
      const closeBtn = createEl('button', {
        text: '×',
        className: 'btn',
        style: 'font-size:20px; background:none; border:none; cursor:pointer;'
      })
      leftHeader.appendChild(title)
      leftHeader.appendChild(closeBtn)
      left.appendChild(leftHeader)
      const addGroupRow = createEl('div', { className: 'emoji-manager-addgroup-row' })
      const addGroupInput = createEl('input', {
        placeholder: '新分组 id',
        className: 'form-control'
      })
      const addGroupBtn = createEl('button', {
        text: '添加',
        className: 'btn'
      })
      addGroupRow.appendChild(addGroupInput)
      addGroupRow.appendChild(addGroupBtn)
      left.appendChild(addGroupRow)
      const groupsList = createEl('div', { className: 'emoji-manager-groups-list' })
      left.appendChild(groupsList)
      const right = createEl('div', { className: 'emoji-manager-right' })
      const rightHeader = createEl('div', { className: 'emoji-manager-right-header' })
      const groupTitle = createEl('h4')
      groupTitle.textContent = ''
      const deleteGroupBtn = createEl('button', {
        text: '删除分组',
        className: 'btn',
        style: 'background:#ef4444; color:#fff;'
      })
      rightHeader.appendChild(groupTitle)
      rightHeader.appendChild(deleteGroupBtn)
      right.appendChild(rightHeader)
      const managerRightMain = createEl('div', { className: 'emoji-manager-right-main' })
      const emojisContainer = createEl('div', { className: 'emoji-manager-emojis' })
      managerRightMain.appendChild(emojisContainer)
      const addEmojiForm = createEl('div', { className: 'emoji-manager-add-emoji-form' })
      const emojiUrlInput = createEl('input', {
        placeholder: '表情图片 URL',
        className: 'form-control'
      })
      const emojiNameInput = createEl('input', {
        placeholder: '名称 (alias)',
        className: 'form-control'
      })
      const emojiWidthInput = createEl('input', {
        placeholder: '宽度 (px) 可选',
        className: 'form-control'
      })
      const emojiHeightInput = createEl('input', {
        placeholder: '高度 (px) 可选',
        className: 'form-control'
      })
      const addEmojiBtn = createEl('button', {
        text: '添加表情',
        className: 'btn btn-primary',
        attrs: {
          'data-action': 'add-emoji',
          'aria-label': '添加表情到当前分组'
        }
      })
      addEmojiForm.appendChild(emojiUrlInput)
      addEmojiForm.appendChild(emojiNameInput)
      addEmojiForm.appendChild(emojiWidthInput)
      addEmojiForm.appendChild(emojiHeightInput)
      addEmojiForm.appendChild(addEmojiBtn)
      managerRightMain.appendChild(addEmojiForm)
      right.appendChild(managerRightMain)
      const footer = createEl('div', { className: 'emoji-manager-footer' })
      const exportBtn = createEl('button', {
        text: '分组导出',
        className: 'btn'
      })
      const importBtn = createEl('button', {
        text: '分组导入',
        className: 'btn'
      })
      const exitBtn = createEl('button', {
        text: '退出',
        className: 'btn'
      })
      exitBtn.addEventListener('click', () => modal.remove())
      const saveBtn = createEl('button', {
        text: '保存',
        className: 'btn btn-primary'
      })
      const syncBtn = createEl('button', {
        text: '同步管理器',
        className: 'btn'
      })
      footer.appendChild(syncBtn)
      footer.appendChild(exportBtn)
      footer.appendChild(importBtn)
      footer.appendChild(exitBtn)
      footer.appendChild(saveBtn)
      panel.appendChild(left)
      panel.appendChild(right)
      panel.appendChild(footer)
      modal.appendChild(panel)
      document.body.appendChild(modal)
      let selectedGroupId = null
      function renderGroups() {
        groupsList.innerHTML = ''
        if (!selectedGroupId && userscriptState.emojiGroups.length > 0)
          selectedGroupId = userscriptState.emojiGroups[0].id
        userscriptState.emojiGroups.forEach(g => {
          const row = createEl('div', {
            style:
              'display:flex; justify-content:space-between; align-items:center; padding:6px; border-radius:4px; cursor:pointer;',
            text: `${g.name || g.id} (${(g.emojis || []).length})`,
            attrs: {
              tabindex: '0',
              'data-group-id': g.id
            }
          })
          const selectGroup = () => {
            selectedGroupId = g.id
            renderGroups()
            renderSelectedGroup()
          }
          row.addEventListener('click', selectGroup)
          row.addEventListener('keydown', e => {
            if (e.key === 'Enter' || e.key === ' ') {
              e.preventDefault()
              selectGroup()
            }
          })
          if (selectedGroupId === g.id) row.style.background = '#f0f8ff'
          groupsList.appendChild(row)
        })
      }
      function showEditorFor(groupId, index) {
        createEditorPopup(groupId, index, renderGroups, renderSelectedGroup)
      }
      function renderSelectedGroup() {
        const group = userscriptState.emojiGroups.find(g => g.id === selectedGroupId) || null
        groupTitle.textContent = group ? group.name || group.id : ''
        emojisContainer.innerHTML = ''
        if (!group) return
        ;(Array.isArray(group.emojis) ? group.emojis : []).forEach((emo, idx) => {
          const card = createEl('div', { className: 'emoji-manager-card' })
          const img = createEl('img', {
            src: emo.url,
            alt: emo.name,
            className: 'emoji-manager-card-img'
          })
          const name = createEl('div', {
            text: emo.name,
            className: 'emoji-manager-card-name'
          })
          const actions = createEl('div', { className: 'emoji-manager-card-actions' })
          const edit = createEl('button', {
            text: '编辑',
            className: 'btn btn-sm',
            attrs: {
              'data-action': 'edit-emoji',
              'aria-label': `编辑表情 ${emo.name}`
            }
          })
          edit.addEventListener('click', () => {
            showEditorFor(group.id, idx)
          })
          const del = createEl('button', {
            text: '删除',
            className: 'btn btn-sm',
            attrs: {
              'data-action': 'delete-emoji',
              'aria-label': `删除表情 ${emo.name}`
            }
          })
          del.addEventListener('click', () => {
            group.emojis.splice(idx, 1)
            renderGroups()
            renderSelectedGroup()
          })
          emojiManagerConfig.injectionPoints.addButton(actions, edit)
          emojiManagerConfig.injectionPoints.addButton(actions, del)
          card.appendChild(img)
          card.appendChild(name)
          card.appendChild(actions)
          emojiManagerConfig.injectionPoints.insertCard(emojisContainer, card)
          bindHoverPreview(img, emo)
        })
      }
      function bindHoverPreview(targetImg, emo) {
        const preview = ensureHoverPreview()
        const previewImg = preview.querySelector('img')
        const previewLabel = preview.querySelector('.emoji-picker-hover-label')
        function onEnter(e) {
          if (previewImg) previewImg.src = emo.url
          if (previewImg) {
            if (emo.width)
              previewImg.style.width = typeof emo.width === 'number' ? emo.width + 'px' : emo.width
            else previewImg.style.width = ''
            if (emo.height)
              previewImg.style.height =
                typeof emo.height === 'number' ? emo.height + 'px' : emo.height
            else previewImg.style.height = ''
          }
          if (previewLabel) previewLabel.textContent = emo.name || ''
          preview.style.display = 'block'
          movePreview(e)
        }
        function movePreview(e) {
          const pad = 12
          const vw = window.innerWidth
          const vh = window.innerHeight
          const rect = preview.getBoundingClientRect()
          let left$1 = e.clientX + pad
          let top = e.clientY + pad
          if (left$1 + rect.width > vw) left$1 = e.clientX - rect.width - pad
          if (top + rect.height > vh) top = e.clientY - rect.height - pad
          preview.style.left = left$1 + 'px'
          preview.style.top = top + 'px'
        }
        function onLeave() {
          preview.style.display = 'none'
        }
        targetImg.addEventListener('mouseenter', onEnter)
        targetImg.addEventListener('mousemove', movePreview)
        targetImg.addEventListener('mouseleave', onLeave)
      }
      addGroupBtn.addEventListener('click', () => {
        const id = (addGroupInput.value || '').trim()
        if (!id) return alert('请输入分组 id')
        if (userscriptState.emojiGroups.find(g => g.id === id)) return alert('分组已存在')
        userscriptState.emojiGroups.push({
          id,
          name: id,
          emojis: []
        })
        addGroupInput.value = ''
        const newIdx = userscriptState.emojiGroups.findIndex(g => g.id === id)
        if (newIdx >= 0) selectedGroupId = userscriptState.emojiGroups[newIdx].id
        renderGroups()
        renderSelectedGroup()
      })
      addEmojiBtn.addEventListener('click', () => {
        if (!selectedGroupId) return alert('请先选择分组')
        const url = emojiManagerConfig.parsers.getUrl({ urlInput: emojiUrlInput })
        const name = emojiManagerConfig.parsers.getName({
          nameInput: emojiNameInput,
          urlInput: emojiUrlInput
        })
        const width = emojiManagerConfig.parsers.getWidth({ widthInput: emojiWidthInput })
        const height = emojiManagerConfig.parsers.getHeight({ heightInput: emojiHeightInput })
        if (!url || !name) return alert('请输入 url 和 名称')
        const group = userscriptState.emojiGroups.find(g => g.id === selectedGroupId)
        if (!group) return
        group.emojis = group.emojis || []
        const newEmo = {
          url,
          name
        }
        if (width !== void 0) newEmo.width = width
        if (height !== void 0) newEmo.height = height
        group.emojis.push(newEmo)
        emojiUrlInput.value = ''
        emojiNameInput.value = ''
        emojiWidthInput.value = ''
        emojiHeightInput.value = ''
        renderGroups()
        renderSelectedGroup()
      })
      deleteGroupBtn.addEventListener('click', () => {
        if (!selectedGroupId) return alert('请先选择分组')
        const idx = userscriptState.emojiGroups.findIndex(g => g.id === selectedGroupId)
        if (idx >= 0) {
          if (!confirm('确认删除该分组?该操作不可撤销')) return
          userscriptState.emojiGroups.splice(idx, 1)
          if (userscriptState.emojiGroups.length > 0)
            selectedGroupId =
              userscriptState.emojiGroups[Math.min(idx, userscriptState.emojiGroups.length - 1)].id
          else selectedGroupId = null
          renderGroups()
          renderSelectedGroup()
        }
      })
      exportBtn.addEventListener('click', () => {
        showImportExportModal(selectedGroupId || void 0)
      })
      importBtn.addEventListener('click', () => {
        showImportExportModal(selectedGroupId || void 0)
      })
      saveBtn.addEventListener('click', () => {
        try {
          saveDataToLocalStorage({ emojiGroups: userscriptState.emojiGroups })
          alert('已保存')
        } catch (e) {
          alert('保存失败:' + e)
        }
      })
      syncBtn.addEventListener('click', () => {
        try {
          if (syncFromManager()) {
            const data = loadDataFromLocalStorage()
            userscriptState.emojiGroups = data.emojiGroups || []
            userscriptState.settings = data.settings || userscriptState.settings
            alert('同步成功,已导入管理器数据')
            renderGroups()
            renderSelectedGroup()
          } else alert('同步未成功,未检测到管理器数据')
        } catch (e) {
          alert('同步异常:' + e)
        }
      })
      closeBtn.addEventListener('click', () => modal.remove())
      modal.addEventListener('click', e => {
        if (e.target === modal) modal.remove()
      })
      renderGroups()
      if (userscriptState.emojiGroups.length > 0) {
        selectedGroupId = userscriptState.emojiGroups[0].id
        const first = groupsList.firstChild
        if (first) first.style.background = '#f0f8ff'
        renderSelectedGroup()
      }
    }
    let emojiManagerConfig
    const init_manager = __esmMin(() => {
      init_styles()
      init_createEl()
      init_state()
      init_userscript_storage()
      init_importExport()
      emojiManagerConfig = {
        selectors: {
          container: '.emoji-manager-emojis',
          card: '.emoji-manager-card',
          actionRow: '.emoji-manager-card-actions',
          editButton: '.btn.btn-sm:first-child',
          deleteButton: '.btn.btn-sm:last-child'
        },
        parsers: {
          getUrl: ({ urlInput }) => (urlInput.value || '').trim(),
          getName: ({ nameInput, urlInput }) => {
            const name = (nameInput.value || '').trim()
            if (!name && urlInput.value)
              return (
                (urlInput.value.trim().split('/').pop() || '').replace(/\.[^.]+$/, '') || '表情'
              )
            return name || '表情'
          },
          getWidth: ({ widthInput }) => {
            const val = (widthInput.value || '').trim()
            const parsed = parseInt(val, 10)
            return !isNaN(parsed) && parsed > 0 ? parsed : void 0
          },
          getHeight: ({ heightInput }) => {
            const val = (heightInput.value || '').trim()
            const parsed = parseInt(val, 10)
            return !isNaN(parsed) && parsed > 0 ? parsed : void 0
          }
        },
        injectionPoints: {
          addButton: (parent, button) => {
            parent.appendChild(button)
          },
          insertCard: (container, card) => {
            container.appendChild(card)
          }
        }
      }
    })
    function showGroupEditorModal() {
      injectGlobalThemeStyles()
      const modal = createEl('div', {
        style: `
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      z-index: 999999;
      display: flex;
      align-items: center;
      justify-content: center;
    `
      })
      const content = createEl('div', {
        style: `
      color: var(--emoji-modal-text);
      padding: 20px;
      width: min(900px, 95%);
      max-width: 95%;
      max-height: calc(100vh - 40px);
      overflow: auto;
      position: relative;
      box-sizing: border-box;
      border-radius: 10px;
      background: var(--emoji-modal-bg, #fff);
    `
      })
      content.innerHTML = `
    <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
      <h2 style="margin: 0; color: var(--emoji-modal-text);backdrop-filter: blur(10px);">表情分组编辑器</h2>
      <button id="closeModal" style="border:none;font-size: 24px; cursor: pointer;">×</button>
    </div>
    
    <div style="margin-bottom: 20px; padding: 16px; background: var(--emoji-modal-button-bg);">
      <div>编辑说明</div>
      <div>
        • 点击分组名称或图标进行编辑<br>
        • 图标支持 emoji 字符或单个字符<br>
        • 修改会立即保存到本地存储<br>
        • 可以调整分组的显示顺序
      </div>
    </div>
    
    <div id="groupsList" style="display: flex; flex-direction: column; gap: 12px;">
      ${userscriptState.emojiGroups
        .map(
          (group, index) =>
            `
        <div class="group-item" data-group-id="${group.id}" data-index="${index}" style="
          display: flex;
          align-items: center;
          gap: 12px;
          padding: 16px;
          background: var(--emoji-modal-button-bg);
        ">
          <div class="drag-handle" style="
            cursor: grab;
            color: var(--emoji-modal-text);
            opacity: 0.5;
            font-size: 16px;
            user-select: none;
          " title="拖拽调整顺序">⋮⋮</div>` +
            (group.icon?.startsWith('https://')
              ? `<img class="group-icon-editor" src="${group.icon}" alt="图标" style="
            max-width: 100px;
          " data-group-id="${group.id}" title="点击编辑图标">`
              : `
          <div class="group-icon-editor" style="
            display: flex;
            align-items: center;
            justify-content: center;
            background: var(--secondary);
            font-size: 18px;
            user-select: none;
          " data-group-id="${group.id}" title="点击编辑图标">
            ${group.icon || '📁'}
          </div>`) +
            `<div style="flex: 1; display: flex; flex-direction: column; gap: 4px;">
            <input class="group-name-editor" 
                   type="text" 
                   value="${group.name || 'Unnamed Group'}" 
                   data-group-id="${group.id}"
                   style="
                     background: var(--secondary);
                     color: var(--emoji-modal-text);
                     border: 1px solid var(--emoji-modal-border);
                     border-radius: 4px;
                     padding: 8px 12px;
                     font-size: 14px;
                     font-weight: 500;
                   " 
                   placeholder="分组名称">
            <div style="font-size: 12px; color: var(--emoji-modal-text);">
              ID: ${group.id} | 表情数:${group.emojis ? group.emojis.length : 0}
            </div>
          </div>
          
          <div style="display: flex; flex-direction: column; gap: 4px; align-items: center;">
            <button class="move-up" data-index="${index}" style="
              background: var(--emoji-modal-button-bg);
              border: 1px solid var(--emoji-modal-border);
              border-radius: 3px;
              padding: 4px 8px;
              cursor: pointer;
              font-size: 12px;
              color: var(--emoji-modal-text);
            " ${index === 0 ? 'disabled' : ''}>↑</button>
            <button class="move-down" data-index="${index}" style="
              background: var(--emoji-modal-button-bg);
              border: 1px solid var(--emoji-modal-border);
              border-radius: 3px;
              padding: 4px 8px;
              cursor: pointer;
              font-size: 12px;
              color: var(--emoji-modal-text);
            " ${index === userscriptState.emojiGroups.length - 1 ? 'disabled' : ''}>↓</button>
          </div>
        </div>
      `
        )
        .join('')}
    </div>
    
    <div style="margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--emoji-modal-border); display: flex; gap: 8px; justify-content: space-between;">
      <button id="openImportExport" style="padding: 8px 16px; background: var(--emoji-modal-button-bg); color: var(--emoji-modal-text); border: 1px solid var(--emoji-modal-border); border-radius: 4px; cursor: pointer;">分组导入/导出</button>
      <div style="display: flex; gap: 8px;">
        <button id="addNewGroup" style="padding: 8px 16px; background: var(--emoji-modal-primary-bg); color: white; border: none; border-radius: 4px; cursor: pointer;">新建分组</button>
        <button id="saveAllChanges" style="padding: 8px 16px; background: var(--emoji-modal-primary-bg); color: white; border: none; border-radius: 4px; cursor: pointer;">保存所有更改</button>
      </div>
    </div>
  `
      modal.appendChild(content)
      document.body.appendChild(modal)
      ensureStyleInjected(
        'group-editor-styles',
        `
    .group-item:hover {
      border-color: var(--emoji-modal-primary-bg) !important;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    }
    .group-icon-editor:hover {
      background: var(--emoji-modal-primary-bg) !important;
      color: white;
    }
    .move-up:hover, .move-down:hover {
      background: var(--emoji-modal-primary-bg) !important;
      color: white;
    }
    .move-up:disabled, .move-down:disabled {
      opacity: 0.3;
      cursor: not-allowed !important;
    }
    /* responsive fixes to avoid modal/content overflow on small screens */
    .group-item {
      flex-wrap: wrap;
      align-items: flex-start;
    }
    .group-item .group-icon-editor {
      flex: 0 0 auto;
      width: 56px;
      height: 56px;
      min-width: 56px;
      min-height: 56px;
      display: flex;
      align-items: center;
      justify-content: center;
      border-radius: 6px;
      overflow: hidden;
    }
    .group-item .group-icon-editor img {
      max-width: 100%;
      max-height: 100%;
      display: block;
    }
    .group-item > div[style*="flex: 1"] {
      min-width: 160px;
    }
    @media (max-width: 480px) {
      /* on very small screens, make modal full height and align to top */
      div[style*="position: fixed;"] {
        align-items: flex-start !important;
      }
      /* content area adjustments */
      div[style*="width: min(900px, 95%)"] {
        width: 100% !important;
        max-width: 100% !important;
        height: 100vh !important;
        max-height: 100vh !important;
        border-radius: 0 !important;
      }
    }
  `
      )
      content.querySelector('#closeModal')?.addEventListener('click', () => {
        modal.remove()
      })
      modal.addEventListener('click', e => {
        if (e.target === modal) modal.remove()
      })
      content.querySelectorAll('.group-name-editor').forEach(input => {
        input.addEventListener('change', e => {
          const target = e.target
          const groupId = target.getAttribute('data-group-id')
          const newName = target.value.trim()
          if (groupId && newName) {
            const group = userscriptState.emojiGroups.find(g => g.id === groupId)
            if (group) {
              group.name = newName
              showTemporaryMessage(`分组 "${newName}" 名称已更新`)
            }
          }
        })
      })
      content.querySelectorAll('.group-icon-editor').forEach(iconEl => {
        iconEl.addEventListener('click', e => {
          const target = e.target
          const groupId = target.getAttribute('data-group-id')
          if (groupId) {
            const newIcon = prompt(
              '请输入新的图标字符 (emoji 或单个字符):',
              target.textContent || '📁'
            )
            if (newIcon && newIcon.trim()) {
              const group = userscriptState.emojiGroups.find(g => g.id === groupId)
              if (group) {
                group.icon = newIcon.trim()
                target.textContent = newIcon.trim()
                showTemporaryMessage(`分组图标已更新为: ${newIcon.trim()}`)
              }
            }
          }
        })
      })
      content.querySelectorAll('.move-up').forEach(btn => {
        btn.addEventListener('click', e => {
          const index = parseInt(e.target.getAttribute('data-index') || '0')
          if (index > 0) {
            const temp = userscriptState.emojiGroups[index]
            userscriptState.emojiGroups[index] = userscriptState.emojiGroups[index - 1]
            userscriptState.emojiGroups[index - 1] = temp
            modal.remove()
            showTemporaryMessage('分组顺序已调整')
            setTimeout(() => showGroupEditorModal(), 300)
          }
        })
      })
      content.querySelectorAll('.move-down').forEach(btn => {
        btn.addEventListener('click', e => {
          const index = parseInt(e.target.getAttribute('data-index') || '0')
          if (index < userscriptState.emojiGroups.length - 1) {
            const temp = userscriptState.emojiGroups[index]
            userscriptState.emojiGroups[index] = userscriptState.emojiGroups[index + 1]
            userscriptState.emojiGroups[index + 1] = temp
            modal.remove()
            showTemporaryMessage('分组顺序已调整')
            setTimeout(() => showGroupEditorModal(), 300)
          }
        })
      })
      content.querySelector('#addNewGroup')?.addEventListener('click', () => {
        const groupName = prompt('请输入新分组的名称:')
        if (groupName && groupName.trim()) {
          const newGroup = {
            id: 'custom_' + Date.now(),
            name: groupName.trim(),
            icon: '📁',
            order: userscriptState.emojiGroups.length,
            emojis: []
          }
          userscriptState.emojiGroups.push(newGroup)
          modal.remove()
          showTemporaryMessage(`新分组 "${groupName.trim()}" 已创建`)
          setTimeout(() => showGroupEditorModal(), 300)
        }
      })
      content.querySelector('#saveAllChanges')?.addEventListener('click', () => {
        saveDataToLocalStorage({ emojiGroups: userscriptState.emojiGroups })
        showTemporaryMessage('所有更改已保存到本地存储')
      })
      content.querySelector('#openImportExport')?.addEventListener('click', () => {
        modal.remove()
        showImportExportModal()
      })
    }
    const init_groupEditor = __esmMin(() => {
      init_state()
      init_userscript_storage()
      init_createEl()
      init_themeSupport()
      init_tempMessage()
      init_injectStyles()
      init_importExport()
    })
    function showPopularEmojisModal() {
      injectGlobalThemeStyles()
      const modal = createEl('div', {
        style: `
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      z-index: 999999;
      display: flex;
      align-items: center;
      justify-content: center;
    `
      })
      const popularEmojis = getPopularEmojis(50)
      const content = createEl('div', {
        style: `
      background: var(--secondary);
      color: var(--emoji-modal-text);
      border-radius: 8px;
      padding: 24px;
      max-height: 80vh;
      overflow-y: auto;
      position: relative;
    `,
        innerHTML: `
    <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
      <h2 style="margin: 0; color: var(--emoji-modal-text);">常用表情 (${popularEmojis.length})</h2>
      <div style="display: flex; gap: 8px; align-items: center;">
        <button id="clearStats" style="padding: 6px 12px; background: #ff4444; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">清空统计</button>
        <button id="closeModal" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #999;">×</button>
      </div>
    </div>
    
    <div style="margin-bottom: 16px; padding: 12px; background: var(--emoji-modal-button-bg); border-radius: 6px; border: 1px solid var(--emoji-modal-border);">
      <div style="display: flex; justify-content: space-between; align-items: center;">
        <span style="font-weight: 500; color: var(--emoji-modal-label);">表情按使用次数排序</span>
        <span style="font-size: 12px; color: var(--emoji-modal-text);">点击表情直接使用</span>
      </div>
      <div style="font-size: 12px; color: var(--emoji-modal-text);">
        总使用次数: ${popularEmojis.reduce((sum, emoji) => sum + emoji.count, 0)}
      </div>
    </div>
    
    <div id="popularEmojiGrid" style="
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
      gap: 8px;
      max-height: 400px;
      overflow-y: auto;
    ">
      ${
        popularEmojis.length === 0
          ? '<div style="grid-column: 1/-1; text-align: center; padding: 40px; color: var(--emoji-modal-text);">还没有使用过表情<br><small>开始使用表情后,这里会显示常用的表情</small></div>'
          : popularEmojis
              .map(
                emoji => `
          <div class="popular-emoji-item" data-name="${emoji.name}" data-url="${emoji.url}" style="
            display: flex;
            flex-direction: column;
            align-items: center;
            padding: 8px;
            border: 1px solid var(--emoji-modal-border);
            border-radius: 6px;
            cursor: pointer;
            transition: all 0.2s;
            background: var(--emoji-modal-button-bg);
          ">
            <img src="${emoji.url}" alt="${emoji.name}" style="
              width: 40px;
              height: 40px;
              object-fit: contain;
              margin-bottom: 4px;
            ">
            <div style="
              font-size: 11px;
              font-weight: 500;
              color: var(--emoji-modal-text);
              text-align: center;
              word-break: break-all;
              line-height: 1.2;
              margin-bottom: 2px;
            ">${emoji.name}</div>
            <div style="
              font-size: 10px;
              color: var(--emoji-modal-text);
              opacity: 0.6;
              text-align: center;
            ">使用${emoji.count}次</div>
          </div>
        `
              )
              .join('')
      }
    </div>
    
    ${
      popularEmojis.length > 0
        ? `
      <div style="margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--emoji-modal-border); font-size: 12px; color: var(--emoji-modal-text); opacity: 0.6; text-align: center;">
        统计数据保存在本地,清空统计将重置所有使用记录
      </div>
    `
        : ''
    }
  `
      })
      modal.appendChild(content)
      document.body.appendChild(modal)
      ensureStyleInjected(
        'popular-emojis-styles',
        `
    .popular-emoji-item:hover {
      transform: translateY(-2px);
      border-color: var(--emoji-modal-primary-bg) !important;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    }
  `
      )
      content.querySelector('#closeModal')?.addEventListener('click', () => {
        modal.remove()
      })
      content.querySelector('#clearStats')?.addEventListener('click', () => {
        if (confirm('确定要清空所有表情使用统计吗?此操作不可撤销。')) {
          clearEmojiUsageStats()
          modal.remove()
          showTemporaryMessage('表情使用统计已清空')
          setTimeout(() => showPopularEmojisModal(), 300)
        }
      })
      content.querySelectorAll('.popular-emoji-item').forEach(item => {
        item.addEventListener('click', () => {
          const name = item.getAttribute('data-name')
          const url = item.getAttribute('data-url')
          if (name && url) {
            trackEmojiUsage(name, url)
            useEmojiFromPopular(name, url)
            modal.remove()
            showTemporaryMessage(`已使用表情: ${name}`)
          }
        })
      })
      modal.addEventListener('click', e => {
        if (e.target === modal) modal.remove()
      })
    }
    function useEmojiFromPopular(name, url) {
      const activeElement = document.activeElement
      if (
        activeElement &&
        (activeElement.tagName === 'TEXTAREA' || activeElement.tagName === 'INPUT')
      ) {
        const textArea = activeElement
        const format = userscriptState.settings.outputFormat
        let emojiText = ''
        if (format === 'markdown') emojiText = `![${name}](${url})`
        else
          emojiText = `<img src="${url}" alt="${name}" style="width: ${userscriptState.settings.imageScale}px; height: ${userscriptState.settings.imageScale}px;">`
        const start = textArea.selectionStart || 0
        const end = textArea.selectionEnd || 0
        const currentValue = textArea.value
        textArea.value = currentValue.slice(0, start) + emojiText + currentValue.slice(end)
        const newPosition = start + emojiText.length
        textArea.setSelectionRange(newPosition, newPosition)
        textArea.dispatchEvent(new Event('input', { bubbles: true }))
        textArea.focus()
      } else {
        const textAreas = document.querySelectorAll(
          'textarea, input[type="text"], [contenteditable="true"]'
        )
        const lastTextArea = Array.from(textAreas).pop()
        if (lastTextArea) {
          lastTextArea.focus()
          if (lastTextArea.tagName === 'TEXTAREA' || lastTextArea.tagName === 'INPUT') {
            const format = userscriptState.settings.outputFormat
            let emojiText = ''
            if (format === 'markdown') emojiText = `![${name}](${url})`
            else
              emojiText = `<img src="${url}" alt="${name}" style="width: ${userscriptState.settings.imageScale}px; height: ${userscriptState.settings.imageScale}px;">`
            const textarea = lastTextArea
            textarea.value += emojiText
            textarea.dispatchEvent(new Event('input', { bubbles: true }))
          }
        }
      }
    }
    const init_popularEmojis = __esmMin(() => {
      init_state()
      init_userscript_storage()
      init_createEl()
      init_themeSupport()
      init_tempMessage()
      init_injectStyles()
    })
    const settings_exports = /* @__PURE__ */ __export({
      showSettingsModal: () => showSettingsModal
    })
    function showSettingsModal() {
      injectGlobalThemeStyles()
      const modal = createEl('div', {
        style: `
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 999999;
    display: flex;
    align-items: center;
    justify-content: center;
  `
      })
      const content = createEl('div', {
        style: `
    backdrop-filter: blur(10px);
    padding: 24px;
    overflow-y: auto;
    position: relative;
  `,
        innerHTML: `
    <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
      <h2 style="margin: 0; color: var(--emoji-modal-text);">设置</h2>
      <button id="closeModal" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #999;">×</button>
    </div>
    
    <div style="margin-bottom: 16px;">
      <label style="display: block; margin-bottom: 8px; color: var(--emoji-modal-label); font-weight: 500;">图片缩放比例:<span id="scaleValue">${userscriptState.settings.imageScale}%</span></label>
      <input type="range" id="scaleSlider" min="5" max="150" step="5" value="${userscriptState.settings.imageScale}" 
             style="width: 100%; margin-bottom: 8px;">
    </div>
    
    <div style="margin-bottom: 16px;">
      <label style="display: block; margin-bottom: 8px; color: var(--emoji-modal-label); font-weight: 500;">输出格式:</label>
      <div style="display: flex; gap: 16px;">
        <label style="display: flex; align-items: center; color: var(--emoji-modal-text);">
          <input type="radio" name="outputFormat" value="markdown" ${userscriptState.settings.outputFormat === 'markdown' ? 'checked' : ''} style="margin-right: 4px;">
          Markdown
        </label>
        <label style="display: flex; align-items: center; color: var(--emoji-modal-text);">
          <input type="radio" name="outputFormat" value="html" ${userscriptState.settings.outputFormat === 'html' ? 'checked' : ''} style="margin-right: 4px;">
          HTML
        </label>
      </div>
    </div>
    
    <div style="margin-bottom: 16px;">
      <label style="display: flex; align-items: center; color: var(--emoji-modal-label); font-weight: 500;">
        <input type="checkbox" id="showSearchBar" ${userscriptState.settings.showSearchBar ? 'checked' : ''} style="margin-right: 8px;">
        显示搜索栏
      </label>
    </div>
    
    <div style="margin-bottom: 16px;">
      <label style="display: flex; align-items: center; color: var(--emoji-modal-label); font-weight: 500;">
        <input type="checkbox" id="enableFloatingPreview" ${userscriptState.settings.enableFloatingPreview ? 'checked' : ''} style="margin-right: 8px;">
        启用悬浮预览功能
      </label>
    </div>

    <div style="margin-bottom: 16px;">
      <label style="display: flex; align-items: center; color: var(--emoji-modal-label); font-weight: 500;">
        <input type="checkbox" id="enableCalloutSuggestions" ${userscriptState.settings.enableCalloutSuggestions ? 'checked' : ''} style="margin-right: 8px;">
        在 textarea 中启用 Callout Suggestion(输入 [ 即可触发)
      </label>
    </div>

    <div style="margin-bottom: 16px;">
      <label style="display: flex; align-items: center; color: var(--emoji-modal-label); font-weight: 500;">
        <input type="checkbox" id="enableBatchParseImages" ${userscriptState.settings.enableBatchParseImages ? 'checked' : ''} style="margin-right: 8px;">
        注入“一键解析并添加所有图片”按钮
      </label>
    </div>
    
    <div style="margin-bottom: 16px;">
      <label style="display: flex; align-items: center; color: var(--emoji-modal-label); font-weight: 500;">
        <input type="checkbox" id="forceMobileMode" ${userscriptState.settings.forceMobileMode ? 'checked' : ''} style="margin-right: 8px;">
        强制移动模式 (在不兼容检测时也注入移动版布局)
      </label>
    </div>
    
    <div style="margin-bottom: 16px; padding: 12px; background: var(--emoji-modal-button-bg); border-radius: 6px; border: 1px solid var(--emoji-modal-border);">
      <div style="font-weight: 500; color: var(--emoji-modal-label); margin-bottom: 8px;">高级功能</div>
      <div style="display: flex; gap: 8px; flex-wrap: wrap;">
        <button id="openGroupEditor" style="
          padding: 6px 12px; 
          background: var(--emoji-modal-primary-bg); 
          color: white; 
          border: none; 
          font-size: 12px;
        ">编辑分组</button>
        <button id="openPopularEmojis" style="
          padding: 6px 12px; 
          background: var(--emoji-modal-primary-bg); 
          color: white; 
          border: none; 
          font-size: 12px;
        ">常用表情</button>
        <button id="openImportExport" style="
          padding: 6px 12px; 
          background: var(--emoji-modal-primary-bg); 
          color: white; 
          border: none; 
          font-size: 12px;
        ">导入导出</button>
      </div>
    </div>
    
    <div style="display: flex; gap: 8px; justify-content: flex-end;">
      <button id="resetSettings" style="padding: 8px 16px; background: var(--emoji-modal-button-bg); color: var(--emoji-modal-text); border: 1px solid var(--emoji-modal-border); border-radius: 4px; cursor: pointer;">重置</button>
      <button id="saveSettings" style="padding: 8px 16px; background: var(--emoji-modal-primary-bg); color: white; border: none; border-radius: 4px; cursor: pointer;">保存</button>
    </div>
  `
      })
      modal.appendChild(content)
      document.body.appendChild(modal)
      const scaleSlider = content.querySelector('#scaleSlider')
      const scaleValue = content.querySelector('#scaleValue')
      scaleSlider?.addEventListener('input', () => {
        if (scaleValue) scaleValue.textContent = scaleSlider.value + '%'
      })
      content.querySelector('#closeModal')?.addEventListener('click', () => {
        modal.remove()
      })
      content.querySelector('#resetSettings')?.addEventListener('click', async () => {
        if (confirm('确定要重置所有设置吗?')) {
          userscriptState.settings = { ...DEFAULT_USER_SETTINGS }
          modal.remove()
        }
      })
      content.querySelector('#saveSettings')?.addEventListener('click', () => {
        userscriptState.settings.imageScale = parseInt(scaleSlider?.value || '30')
        const outputFormat = content.querySelector('input[name="outputFormat"]:checked')
        if (outputFormat) userscriptState.settings.outputFormat = outputFormat.value
        const showSearchBar = content.querySelector('#showSearchBar')
        if (showSearchBar) userscriptState.settings.showSearchBar = showSearchBar.checked
        const enableFloatingPreview = content.querySelector('#enableFloatingPreview')
        if (enableFloatingPreview)
          userscriptState.settings.enableFloatingPreview = enableFloatingPreview.checked
        const enableCalloutEl = content.querySelector('#enableCalloutSuggestions')
        if (enableCalloutEl)
          userscriptState.settings.enableCalloutSuggestions = !!enableCalloutEl.checked
        const enableBatchEl = content.querySelector('#enableBatchParseImages')
        if (enableBatchEl) userscriptState.settings.enableBatchParseImages = !!enableBatchEl.checked
        const forceMobileEl = content.querySelector('#forceMobileMode')
        if (forceMobileEl) userscriptState.settings.forceMobileMode = !!forceMobileEl.checked
        saveDataToLocalStorage({ settings: userscriptState.settings })
        try {
          const remoteInput = content.querySelector('#remoteConfigUrl')
          if (remoteInput && remoteInput.value.trim())
            localStorage.setItem('emoji_extension_remote_config_url', remoteInput.value.trim())
        } catch (e) {}
        alert('设置已保存')
        modal.remove()
      })
      content.querySelector('#openGroupEditor')?.addEventListener('click', () => {
        modal.remove()
        showGroupEditorModal()
      })
      content.querySelector('#openPopularEmojis')?.addEventListener('click', () => {
        modal.remove()
        showPopularEmojisModal()
      })
      content.querySelector('#openImportExport')?.addEventListener('click', () => {
        modal.remove()
        showImportExportModal()
      })
      modal.addEventListener('click', e => {
        if (e.target === modal) modal.remove()
      })
    }
    const init_settings = __esmMin(() => {
      init_state()
      init_userscript_storage()
      init_createEl()
      init_themeSupport()
      init_groupEditor()
      init_popularEmojis()
      init_importExport()
    })
    init_state()
    init_userscript_storage()
    init_createEl()
    init_hoverPreview()
    function isMobileView() {
      try {
        return (
          getEffectivePlatform() === 'mobile' ||
          !!(
            userscriptState &&
            userscriptState.settings &&
            userscriptState.settings.forceMobileMode
          )
        )
      } catch (e) {
        return false
      }
    }
    function insertEmojiIntoEditor(emoji) {
      console.log('[Emoji Extension Userscript] Inserting emoji:', emoji)
      if (emoji.name && emoji.url) trackEmojiUsage(emoji.name, emoji.url)
      const selectors = [
        'textarea.d-editor-input',
        'textarea.ember-text-area',
        '#channel-composer',
        '.chat-composer__input',
        'textarea.chat-composer__input'
      ]
      const proseMirror = document.querySelector('.ProseMirror.d-editor-input')
      let textarea = null
      for (const s of selectors) {
        const el = document.querySelector(s)
        if (el) {
          textarea = el
          break
        }
      }
      const contentEditable = document.querySelector('[contenteditable="true"]')
      if (!textarea && !proseMirror && !contentEditable) {
        console.error('找不到输入框')
        return
      }
      const dimensionMatch = emoji.url?.match(/_(\d{3,})x(\d{3,})\./)
      let width = '500'
      let height = '500'
      if (dimensionMatch) {
        width = dimensionMatch[1]
        height = dimensionMatch[2]
      } else if (emoji.width && emoji.height) {
        width = emoji.width.toString()
        height = emoji.height.toString()
      }
      const scale = userscriptState.settings?.imageScale || 30
      const outputFormat = userscriptState.settings?.outputFormat || 'markdown'
      if (textarea) {
        let insertText = ''
        if (outputFormat === 'html') {
          const scaledWidth = Math.max(1, Math.round(Number(width) * (scale / 100)))
          const scaledHeight = Math.max(1, Math.round(Number(height) * (scale / 100)))
          insertText = `<img src="${emoji.url}" title=":${emoji.name}:" class="emoji only-emoji" alt=":${emoji.name}:" loading="lazy" width="${scaledWidth}" height="${scaledHeight}" style="aspect-ratio: ${scaledWidth} / ${scaledHeight};"> `
        } else insertText = `![${emoji.name}|${width}x${height},${scale}%](${emoji.url}) `
        const selectionStart = textarea.selectionStart
        const selectionEnd = textarea.selectionEnd
        textarea.value =
          textarea.value.substring(0, selectionStart) +
          insertText +
          textarea.value.substring(selectionEnd, textarea.value.length)
        textarea.selectionStart = textarea.selectionEnd = selectionStart + insertText.length
        textarea.focus()
        const inputEvent = new Event('input', {
          bubbles: true,
          cancelable: true
        })
        textarea.dispatchEvent(inputEvent)
      } else if (proseMirror) {
        const imgWidth = Number(width) || 500
        const scaledWidth = Math.max(1, Math.round(imgWidth * (scale / 100)))
        const htmlContent = `<img src="${emoji.url}" alt="${emoji.name}" width="${width}" height="${height}" data-scale="${scale}" style="width: ${scaledWidth}px">`
        try {
          const dataTransfer = new DataTransfer()
          dataTransfer.setData('text/html', htmlContent)
          const pasteEvent = new ClipboardEvent('paste', {
            clipboardData: dataTransfer,
            bubbles: true
          })
          proseMirror.dispatchEvent(pasteEvent)
        } catch (error) {
          try {
            document.execCommand('insertHTML', false, htmlContent)
          } catch (fallbackError) {
            console.error('无法向富文本编辑器中插入表情', fallbackError)
          }
        }
      } else if (contentEditable)
        try {
          if (outputFormat === 'html') {
            const imgWidth = Number(width) || 500
            const scaledWidth = Math.max(1, Math.round(imgWidth * (scale / 100)))
            const htmlContent = `<img src="${emoji.url}" alt="${emoji.name}" width="${width}" height="${height}" data-scale="${scale}" style="width: ${scaledWidth}px"> `
            const sel = window.getSelection()
            if (sel && sel.rangeCount > 0) {
              const range = sel.getRangeAt(0)
              const frag = document.createRange().createContextualFragment(htmlContent)
              range.deleteContents()
              range.insertNode(frag)
              range.collapse(false)
              sel.removeAllRanges()
              sel.addRange(range)
            } else contentEditable.insertAdjacentHTML('beforeend', htmlContent)
          } else {
            const insertText = `![${emoji.name}|${width}x${height},${scale}%](${emoji.url}) `
            const textNode = document.createTextNode(insertText)
            const sel = window.getSelection()
            if (sel && sel.rangeCount > 0) {
              const range = sel.getRangeAt(0)
              range.deleteContents()
              range.insertNode(textNode)
              range.setStartAfter(textNode)
              range.collapse(true)
              sel.removeAllRanges()
              sel.addRange(range)
            } else contentEditable.appendChild(textNode)
          }
          const inputEvent = new Event('input', {
            bubbles: true,
            cancelable: true
          })
          contentEditable.dispatchEvent(inputEvent)
        } catch (e) {
          console.error('无法向 contenteditable 插入表情', e)
        }
    }
    function createMobileEmojiPicker(groups) {
      const modal = createEl('div', {
        className: 'modal d-modal fk-d-menu-modal emoji-picker-content',
        attrs: {
          'data-identifier': 'emoji-picker',
          'data-keyboard': 'false',
          'aria-modal': 'true',
          role: 'dialog'
        }
      })
      const modalContainerDiv = createEl('div', { className: 'd-modal__container' })
      const modalBody = createEl('div', { className: 'd-modal__body' })
      modalBody.tabIndex = -1
      const emojiPickerDiv = createEl('div', { className: 'emoji-picker' })
      const filterContainer = createEl('div', { className: 'emoji-picker__filter-container' })
      const filterInputContainer = createEl('div', {
        className: 'emoji-picker__filter filter-input-container'
      })
      const filterInput = createEl('input', {
        className: 'filter-input',
        placeholder: '按表情符号名称搜索…',
        type: 'text'
      })
      filterInputContainer.appendChild(filterInput)
      const closeButton = createEl('button', {
        className: 'btn no-text btn-icon btn-transparent emoji-picker__close-btn',
        type: 'button',
        innerHTML: `<svg class="fa d-icon d-icon-xmark svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#xmark"></use></svg>`
      })
      closeButton.addEventListener('click', () => {
        const container = modal.closest('.modal-container') || modal
        if (container) container.remove()
      })
      filterContainer.appendChild(filterInputContainer)
      filterContainer.appendChild(closeButton)
      const content = createEl('div', { className: 'emoji-picker__content' })
      const sectionsNav = createEl('div', { className: 'emoji-picker__sections-nav' })
      const managementButton = createEl('button', {
        className: 'btn no-text btn-flat emoji-picker__section-btn management-btn',
        attrs: {
          tabindex: '-1',
          style: 'border-right: 1px solid #ddd;'
        },
        innerHTML: '⚙️',
        title: '管理表情 - 点击打开完整管理界面',
        type: 'button'
      })
      managementButton.addEventListener('click', () => {
        __vitePreload(
          async () => {
            const { openManagementInterface: openManagementInterface$1 } =
              await Promise.resolve().then(() => (init_manager(), manager_exports))
            return { openManagementInterface: openManagementInterface$1 }
          },
          void 0
        ).then(({ openManagementInterface: openManagementInterface$1 }) => {
          openManagementInterface$1()
        })
      })
      sectionsNav.appendChild(managementButton)
      const settingsButton = createEl('button', {
        className: 'btn no-text btn-flat emoji-picker__section-btn settings-btn',
        innerHTML: '🔧',
        title: '设置',
        attrs: {
          tabindex: '-1',
          style: 'border-right: 1px solid #ddd;'
        },
        type: 'button'
      })
      settingsButton.addEventListener('click', async () => {
        try {
          const { showSettingsModal: showSettingsModal$1 } = await __vitePreload(
            async () => {
              const { showSettingsModal: showSettingsModal$2 } = await Promise.resolve().then(
                () => (init_settings(), settings_exports)
              )
              return { showSettingsModal: showSettingsModal$2 }
            },
            void 0
          )
          showSettingsModal$1()
        } catch (e) {
          console.error('[Userscript] Failed to load settings module:', e)
        }
      })
      sectionsNav.appendChild(settingsButton)
      const scrollableContent = createEl('div', { className: 'emoji-picker__scrollable-content' })
      const sections = createEl('div', {
        className: 'emoji-picker__sections',
        attrs: { role: 'button' }
      })
      groups.forEach((group, index) => {
        if (!group?.emojis?.length) return
        const navButton = createEl('button', {
          className: `btn no-text btn-flat emoji-picker__section-btn ${index === 0 ? 'active' : ''}`,
          attrs: {
            tabindex: '-1',
            'data-section': group.id,
            type: 'button'
          }
        })
        const iconVal = group.icon || '📁'
        if (isImageUrl(iconVal)) {
          const img = createEl('img', {
            src: iconVal,
            alt: group.name || '',
            className: 'emoji',
            style: 'width: 18px; height: 18px; object-fit: contain;'
          })
          navButton.appendChild(img)
        } else navButton.textContent = String(iconVal)
        navButton.title = group.name
        navButton.addEventListener('click', () => {
          sectionsNav
            .querySelectorAll('.emoji-picker__section-btn')
            .forEach(btn => btn.classList.remove('active'))
          navButton.classList.add('active')
          const target = sections.querySelector(`[data-section="${group.id}"]`)
          if (target)
            target.scrollIntoView({
              behavior: 'smooth',
              block: 'start'
            })
        })
        sectionsNav.appendChild(navButton)
        const section = createEl('div', {
          className: 'emoji-picker__section',
          attrs: {
            'data-section': group.id,
            role: 'region',
            'aria-label': group.name
          }
        })
        const titleContainer = createEl('div', {
          className: 'emoji-picker__section-title-container'
        })
        const title = createEl('h2', {
          className: 'emoji-picker__section-title',
          text: group.name
        })
        titleContainer.appendChild(title)
        const sectionEmojis = createEl('div', { className: 'emoji-picker__section-emojis' })
        group.emojis.forEach(emoji => {
          if (!emoji || typeof emoji !== 'object' || !emoji.url || !emoji.name) return
          const img = createEl('img', {
            src: emoji.url,
            alt: emoji.name,
            className: 'emoji',
            title: `:${emoji.name}:`,
            style: 'width: 32px; height: 32px; object-fit: contain;',
            attrs: {
              'data-emoji': emoji.name,
              tabindex: '0',
              loading: 'lazy'
            }
          })
          ;(function bindHover(imgEl, emo) {
            if (!userscriptState.settings?.enableFloatingPreview) return
            const preview = ensureHoverPreview()
            const previewImg = preview.querySelector('img')
            const previewLabel = preview.querySelector('.emoji-picker-hover-label')
            let fadeTimer = null
            function onEnter(e) {
              previewImg.src = emo.url
              previewLabel.textContent = emo.name || ''
              preview.style.display = 'block'
              preview.style.opacity = '1'
              preview.style.transition = 'opacity 0.12s ease, transform 0.12s ease'
              if (fadeTimer) {
                clearTimeout(fadeTimer)
                fadeTimer = null
              }
              fadeTimer = window.setTimeout(() => {
                preview.style.opacity = '0'
                setTimeout(() => {
                  if (preview.style.opacity === '0') preview.style.display = 'none'
                }, 300)
              }, 5e3)
              move(e)
            }
            function move(e) {
              const pad = 12
              const vw = window.innerWidth
              const vh = window.innerHeight
              const rect = preview.getBoundingClientRect()
              let left = e.clientX + pad
              let top = e.clientY + pad
              if (left + rect.width > vw) left = e.clientX - rect.width - pad
              if (top + rect.height > vh) top = e.clientY - rect.height - pad
              preview.style.left = left + 'px'
              preview.style.top = top + 'px'
            }
            function onLeave() {
              if (fadeTimer) {
                clearTimeout(fadeTimer)
                fadeTimer = null
              }
              preview.style.display = 'none'
            }
            imgEl.addEventListener('mouseenter', onEnter)
            imgEl.addEventListener('mousemove', move)
            imgEl.addEventListener('mouseleave', onLeave)
          })(img, emoji)
          img.addEventListener('click', () => {
            insertEmojiIntoEditor(emoji)
            if (modal.parentElement) modal.parentElement.removeChild(modal)
          })
          img.addEventListener('keydown', e => {
            if (e.key === 'Enter' || e.key === ' ') {
              e.preventDefault()
              insertEmojiIntoEditor(emoji)
              if (modal.parentElement) modal.parentElement.removeChild(modal)
            }
          })
          sectionEmojis.appendChild(img)
        })
        section.appendChild(titleContainer)
        section.appendChild(sectionEmojis)
        sections.appendChild(section)
      })
      filterInput.addEventListener('input', e => {
        const q = (e.target.value || '').toLowerCase()
        sections.querySelectorAll('img').forEach(img => {
          const emojiName = (img.dataset.emoji || '').toLowerCase()
          img.style.display = q === '' || emojiName.includes(q) ? '' : 'none'
        })
        sections.querySelectorAll('.emoji-picker__section').forEach(section => {
          const visibleEmojis = section.querySelectorAll('img:not([style*="display: none"])')
          section.style.display = visibleEmojis.length > 0 ? '' : 'none'
        })
      })
      scrollableContent.appendChild(sections)
      content.appendChild(sectionsNav)
      content.appendChild(scrollableContent)
      emojiPickerDiv.appendChild(filterContainer)
      emojiPickerDiv.appendChild(content)
      modalBody.appendChild(emojiPickerDiv)
      modalContainerDiv.appendChild(modalBody)
      modal.appendChild(modalContainerDiv)
      return modal
    }
    function createDesktopEmojiPicker(groups) {
      const picker = createEl('div', {
        className: 'fk-d-menu -animated -expanded',
        style: 'max-width: 400px; visibility: visible; z-index: 999999;',
        attrs: {
          'data-identifier': 'emoji-picker',
          role: 'dialog'
        }
      })
      const innerContent = createEl('div', { className: 'fk-d-menu__inner-content' })
      const emojiPickerDiv = createEl('div', { className: 'emoji-picker' })
      const filterContainer = createEl('div', { className: 'emoji-picker__filter-container' })
      const filterDiv = createEl('div', {
        className: 'emoji-picker__filter filter-input-container'
      })
      const searchInput = createEl('input', {
        className: 'filter-input',
        placeholder: '按表情符号名称搜索…',
        type: 'text'
      })
      filterDiv.appendChild(searchInput)
      filterContainer.appendChild(filterDiv)
      const content = createEl('div', { className: 'emoji-picker__content' })
      const sectionsNav = createEl('div', { className: 'emoji-picker__sections-nav' })
      const managementButton = createEl('button', {
        className: 'btn no-text btn-flat emoji-picker__section-btn management-btn',
        attrs: {
          tabindex: '-1',
          style: 'border-right: 1px solid #ddd;'
        },
        type: 'button',
        innerHTML: '⚙️',
        title: '管理表情 - 点击打开完整管理界面'
      })
      managementButton.addEventListener('click', () => {
        __vitePreload(
          async () => {
            const { openManagementInterface: openManagementInterface$1 } =
              await Promise.resolve().then(() => (init_manager(), manager_exports))
            return { openManagementInterface: openManagementInterface$1 }
          },
          void 0
        ).then(({ openManagementInterface: openManagementInterface$1 }) => {
          openManagementInterface$1()
        })
      })
      sectionsNav.appendChild(managementButton)
      const settingsButton = createEl('button', {
        className: 'btn no-text btn-flat emoji-picker__section-btn settings-btn',
        attrs: {
          tabindex: '-1',
          style: 'border-right: 1px solid #ddd;'
        },
        type: 'button',
        innerHTML: '🔧',
        title: '设置'
      })
      settingsButton.addEventListener('click', async () => {
        try {
          const { showSettingsModal: showSettingsModal$1 } = await __vitePreload(
            async () => {
              const { showSettingsModal: showSettingsModal$2 } = await Promise.resolve().then(
                () => (init_settings(), settings_exports)
              )
              return { showSettingsModal: showSettingsModal$2 }
            },
            void 0
          )
          showSettingsModal$1()
        } catch (e) {
          console.error('[Userscript] Failed to load settings module:', e)
        }
      })
      sectionsNav.appendChild(settingsButton)
      const scrollableContent = createEl('div', { className: 'emoji-picker__scrollable-content' })
      const sections = createEl('div', {
        className: 'emoji-picker__sections',
        attrs: { role: 'button' }
      })
      groups.forEach((group, index) => {
        if (!group?.emojis?.length) return
        const navButton = createEl('button', {
          className: `btn no-text btn-flat emoji-picker__section-btn ${index === 0 ? 'active' : ''}`,
          attrs: {
            tabindex: '-1',
            'data-section': group.id
          },
          type: 'button'
        })
        const iconVal = group.icon || '📁'
        if (isImageUrl(iconVal)) {
          const img = createEl('img', {
            src: iconVal,
            alt: group.name || '',
            className: 'emoji-group-icon',
            style: 'width: 18px; height: 18px; object-fit: contain;'
          })
          navButton.appendChild(img)
        } else navButton.textContent = String(iconVal)
        navButton.title = group.name
        navButton.addEventListener('click', () => {
          sectionsNav
            .querySelectorAll('.emoji-picker__section-btn')
            .forEach(btn => btn.classList.remove('active'))
          navButton.classList.add('active')
          const target = sections.querySelector(`[data-section="${group.id}"]`)
          if (target)
            target.scrollIntoView({
              behavior: 'smooth',
              block: 'start'
            })
        })
        sectionsNav.appendChild(navButton)
        const section = createEl('div', {
          className: 'emoji-picker__section',
          attrs: {
            'data-section': group.id,
            role: 'region',
            'aria-label': group.name
          }
        })
        const titleContainer = createEl('div', {
          className: 'emoji-picker__section-title-container'
        })
        const title = createEl('h2', {
          className: 'emoji-picker__section-title',
          text: group.name
        })
        titleContainer.appendChild(title)
        const sectionEmojis = createEl('div', { className: 'emoji-picker__section-emojis' })
        let added = 0
        group.emojis.forEach(emoji => {
          if (!emoji || typeof emoji !== 'object' || !emoji.url || !emoji.name) return
          const img = createEl('img', {
            width: '32px',
            height: '32px',
            className: 'emoji',
            src: emoji.url,
            alt: emoji.name,
            title: `:${emoji.name}:`,
            attrs: {
              'data-emoji': emoji.name,
              tabindex: '0',
              loading: 'lazy'
            }
          })
          ;(function bindHover(imgEl, emo) {
            if (!userscriptState.settings?.enableFloatingPreview) return
            const preview = ensureHoverPreview()
            const previewImg = preview.querySelector('img')
            const previewLabel = preview.querySelector('.emoji-picker-hover-label')
            let fadeTimer = null
            function onEnter(e) {
              previewImg.src = emo.url
              previewLabel.textContent = emo.name || ''
              preview.style.display = 'block'
              preview.style.opacity = '1'
              preview.style.transition = 'opacity 0.12s ease, transform 0.12s ease'
              if (fadeTimer) {
                clearTimeout(fadeTimer)
                fadeTimer = null
              }
              fadeTimer = window.setTimeout(() => {
                preview.style.opacity = '0'
                setTimeout(() => {
                  if (preview.style.opacity === '0') preview.style.display = 'none'
                }, 300)
              }, 5e3)
              move(e)
            }
            function move(e) {
              const pad = 12
              const vw = window.innerWidth
              const vh = window.innerHeight
              const rect = preview.getBoundingClientRect()
              let left = e.clientX + pad
              let top = e.clientY + pad
              if (left + rect.width > vw) left = e.clientX - rect.width - pad
              if (top + rect.height > vh) top = e.clientY - rect.height - pad
              preview.style.left = left + 'px'
              preview.style.top = top + 'px'
            }
            function onLeave() {
              if (fadeTimer) {
                clearTimeout(fadeTimer)
                fadeTimer = null
              }
              preview.style.display = 'none'
            }
            imgEl.addEventListener('mouseenter', onEnter)
            imgEl.addEventListener('mousemove', move)
            imgEl.addEventListener('mouseleave', onLeave)
          })(img, emoji)
          img.addEventListener('click', () => {
            insertEmojiIntoEditor(emoji)
            picker.remove()
          })
          img.addEventListener('keydown', e => {
            if (e.key === 'Enter' || e.key === ' ') {
              e.preventDefault()
              insertEmojiIntoEditor(emoji)
              picker.remove()
            }
          })
          sectionEmojis.appendChild(img)
          added++
        })
        if (added === 0) {
          const msg = createEl('div', {
            text: `${group.name} 组暂无有效表情`,
            style: 'padding: 20px; text-align: center; color: #999;'
          })
          sectionEmojis.appendChild(msg)
        }
        section.appendChild(titleContainer)
        section.appendChild(sectionEmojis)
        sections.appendChild(section)
      })
      searchInput.addEventListener('input', e => {
        const q = (e.target.value || '').toLowerCase()
        sections.querySelectorAll('img').forEach(img => {
          const emojiName = img.getAttribute('data-emoji')?.toLowerCase() || ''
          img.style.display = q === '' || emojiName.includes(q) ? '' : 'none'
        })
        sections.querySelectorAll('.emoji-picker__section').forEach(section => {
          const visibleEmojis = section.querySelectorAll('img:not([style*="none"])')
          const titleContainer = section.querySelector('.emoji-picker__section-title-container')
          if (titleContainer) titleContainer.style.display = visibleEmojis.length > 0 ? '' : 'none'
        })
      })
      scrollableContent.appendChild(sections)
      content.appendChild(sectionsNav)
      content.appendChild(scrollableContent)
      emojiPickerDiv.appendChild(filterContainer)
      emojiPickerDiv.appendChild(content)
      innerContent.appendChild(emojiPickerDiv)
      picker.appendChild(innerContent)
      return picker
    }
    async function createEmojiPicker() {
      const groups = userscriptState.emojiGroups
      const mobile = isMobileView()
      try {
        injectEmojiPickerStyles()
      } catch (e) {
        console.warn('injectEmojiPickerStyles failed', e)
      }
      if (mobile) return createMobileEmojiPicker(groups)
      else return createDesktopEmojiPicker(groups)
    }
    init_createEl()
    init_state()
    init_popularEmojis()
    const QUICK_INSERTS = [
      'info',
      'tip',
      'faq',
      'question',
      'note',
      'abstract',
      'todo',
      'success',
      'warning',
      'failure',
      'danger',
      'bug',
      'example',
      'quote'
    ]
    const ICONS = {
      info: {
        icon: 'ℹ️',
        color: 'rgba(2, 122, 255, 0.1)',
        svg: '<svg class="fa d-icon d-icon-far-lightbulb svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#far-lightbulb"></use></svg>'
      },
      tip: {
        icon: '💡',
        color: 'rgba(0, 191, 188, 0.1);',
        svg: '<svg class="fa d-icon d-icon-fire-flame-curved svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#fire-flame-curved"></use></svg>'
      },
      faq: {
        icon: '❓',
        color: 'rgba(236, 117, 0, 0.1);',
        svg: '<svg class="fa d-icon d-icon-far-circle-question svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#far-circle-question"></use></svg>'
      },
      question: {
        icon: '🤔',
        color: 'rgba(236, 117, 0, 0.1);',
        svg: '<svg class="fa d-icon d-icon-far-circle-question svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#far-circle-question"></use></svg>'
      },
      note: {
        icon: '📝',
        color: 'rgba(8, 109, 221, 0.1);',
        svg: '<svg class="fa d-icon d-icon-far-pen-to-square svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#far-pen-to-square"></use></svg>'
      },
      abstract: {
        icon: '📋',
        color: 'rgba(0, 191, 188, 0.1);',
        svg: '<svg class="fa d-icon d-icon-far-clipboard svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#far-clipboard"></use></svg>'
      },
      todo: {
        icon: '☑️',
        color: 'rgba(2, 122, 255, 0.1);',
        svg: '<svg class="fa d-icon d-icon-far-circle-check svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#far-circle-check"></use></svg>'
      },
      success: {
        icon: '🎉',
        color: 'rgba(68, 207, 110, 0.1);',
        svg: '<svg class="fa d-icon d-icon-check svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#check"></use></svg>'
      },
      warning: {
        icon: '⚠️',
        color: 'rgba(236, 117, 0, 0.1);',
        svg: '<svg class="fa d-icon d-icon-triangle-exclamation svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#triangle-exclamation"></use></svg>'
      },
      failure: {
        icon: '❌',
        color: 'rgba(233, 49, 71, 0.1);',
        svg: '<svg class="fa d-icon d-icon-xmark svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#xmark"></use></svg>'
      },
      danger: {
        icon: '☠️',
        color: 'rgba(233, 49, 71, 0.1);',
        svg: '<svg class="fa d-icon d-icon-bolt svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#bolt"></use></svg>'
      },
      bug: {
        icon: '🐛',
        color: 'rgba(233, 49, 71, 0.1);',
        svg: '<svg class="fa d-icon d-icon-bug svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#bug"></use></svg>'
      },
      example: {
        icon: '🔎',
        color: 'rgba(120, 82, 238, 0.1);',
        svg: '<svg class="fa d-icon d-icon-list svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#list"></use></svg>'
      },
      quote: {
        icon: '💬',
        color: 'rgba(158, 158, 158, 0.1);',
        svg: '<svg class="fa d-icon d-icon-quote-left svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#quote-left"></use></svg>'
      }
    }
    function insertIntoEditor(text) {
      const chatComposer = document.querySelector('textarea#channel-composer.chat-composer__input')
      if (chatComposer) {
        const start = chatComposer.selectionStart ?? 0
        const end = chatComposer.selectionEnd ?? start
        const value = chatComposer.value
        chatComposer.value = value.slice(0, start) + text + value.slice(end)
        const pos = start + text.length
        if ('setSelectionRange' in chatComposer)
          try {
            chatComposer.setSelectionRange(pos, pos)
          } catch (e) {}
        chatComposer.dispatchEvent(new Event('input', { bubbles: true }))
        return
      }
      const active = document.activeElement
      const isTextarea = el => !!el && el.tagName === 'TEXTAREA'
      if (isTextarea(active)) {
        const textarea = active
        const start = textarea.selectionStart ?? 0
        const end = textarea.selectionEnd ?? start
        const value = textarea.value
        textarea.value = value.slice(0, start) + text + value.slice(end)
        const pos = start + text.length
        if ('setSelectionRange' in textarea)
          try {
            textarea.setSelectionRange(pos, pos)
          } catch (e) {}
        textarea.dispatchEvent(new Event('input', { bubbles: true }))
        return
      }
      if (active && active.isContentEditable) {
        const sel = window.getSelection()
        if (!sel) return
        const range = sel.getRangeAt(0)
        range.deleteContents()
        const node = document.createTextNode(text)
        range.insertNode(node)
        range.setStartAfter(node)
        range.setEndAfter(node)
        sel.removeAllRanges()
        sel.addRange(range)
        active.dispatchEvent(new Event('input', { bubbles: true }))
        return
      }
      const fallback = document.querySelector('textarea')
      if (fallback) {
        fallback.focus()
        const start = fallback.selectionStart ?? fallback.value.length
        const end = fallback.selectionEnd ?? start
        const value = fallback.value
        fallback.value = value.slice(0, start) + text + value.slice(end)
        const pos = start + text.length
        if ('setSelectionRange' in fallback)
          try {
            fallback.setSelectionRange(pos, pos)
          } catch (e) {}
        fallback.dispatchEvent(new Event('input', { bubbles: true }))
      }
    }
    function createQuickInsertMenu() {
      const menu = createEl('div', {
        className:
          'fk-d-menu toolbar-menu__options-content toolbar-popup-menu-options -animated -expanded'
      })
      const inner = createEl('div', { className: 'fk-d-menu__inner-content' })
      const list = createEl('ul', { className: 'dropdown-menu' })
      QUICK_INSERTS.forEach(key => {
        const li = createEl('li', { className: 'dropdown-menu__item' })
        const btn = createEl('button', {
          className: 'btn btn-icon-text',
          type: 'button',
          title: key.charAt(0).toUpperCase() + key.slice(1),
          style: 'background: ' + (ICONS[key]?.color || 'auto')
        })
        btn.addEventListener('click', () => {
          if (menu.parentElement) menu.parentElement.removeChild(menu)
          insertIntoEditor(`>[!${key}]+\n`)
        })
        const emojiSpan = createEl('span', {
          className: 'd-button-emoji',
          text: ICONS[key]?.icon || '✳️',
          style: 'margin-right: 6px;'
        })
        const labelWrap = createEl('span', { className: 'd-button-label' })
        const labelText = createEl('span', {
          className: 'd-button-label__text',
          text: key.charAt(0).toUpperCase() + key.slice(1)
        })
        labelWrap.appendChild(labelText)
        const svgHtml = ICONS[key]?.svg || ''
        if (svgHtml) {
          const svgSpan = createEl('span', {
            className: 'd-button-label__svg',
            innerHTML: svgHtml,
            style: 'margin-left: 6px; display: inline-flex; align-items: center;'
          })
          labelWrap.appendChild(svgSpan)
        }
        btn.appendChild(emojiSpan)
        btn.appendChild(labelWrap)
        li.appendChild(btn)
        list.appendChild(li)
      })
      inner.appendChild(list)
      menu.appendChild(inner)
      return menu
    }
    function shouldSkipToolbarInjection() {
      if (!(userscriptState.settings?.forceMobileMode || false)) return false
      return !!document.querySelector('#d-menu-portals')
    }
    function findAllToolbars() {
      if (shouldSkipToolbarInjection()) {
        console.log(
          '[Emoji Extension Userscript] Force mobile mode with #d-menu-portals detected, skipping toolbar injection'
        )
        return []
      }
      const toolbars = []
      const selectors = getPlatformToolbarSelectors()
      for (const selector of selectors) {
        const elements = document.querySelectorAll(selector)
        toolbars.push(...Array.from(elements))
      }
      return toolbars
    }
    let menuTriggersInitialized = false
    function setupForceMobileMenuTriggers() {
      if (menuTriggersInitialized) return
      if (!(userscriptState.settings?.forceMobileMode || false)) return
      const portalContainer = document.querySelector('#d-menu-portals')
      if (!portalContainer) {
        console.log(
          '[Emoji Extension Userscript] #d-menu-portals not found, skipping force mobile menu triggers'
        )
        return
      }
      console.log(
        '[Emoji Extension Userscript] Force mobile mode enabled, setting up menu triggers'
      )
      const toolbarOptionsTrigger = document.querySelector(
        'button.toolbar-menu__options-trigger[data-identifier="toolbar-menu__options"]'
      )
      const chatComposerTrigger = document.querySelector(
        'button.chat-composer-dropdown__trigger-btn[data-identifier="chat-composer-dropdown__menu"], button.chat-composer-dropdown__menu-trigger[data-identifier="chat-composer-dropdown__menu"]'
      )
      new MutationObserver(mutations => {
        mutations.forEach(mutation => {
          mutation.addedNodes.forEach(node => {
            if (node.nodeType === Node.ELEMENT_NODE) {
              const element = node
              if (
                element.classList.contains('toolbar-menu__options-content') ||
                element.classList.contains('chat-composer-dropdown__content') ||
                element.classList.contains('chat-composer-dropdown__menu-content')
              ) {
                console.log('[Emoji Extension Userscript] Menu expanded, injecting custom buttons')
                injectCustomMenuButtons(element)
              }
            }
          })
        })
      }).observe(portalContainer, {
        childList: true,
        subtree: true
      })
      if (toolbarOptionsTrigger) {
        toolbarOptionsTrigger.addEventListener('click', () => {
          setTimeout(() => {
            const menu = document.querySelector(
              '.toolbar-menu__options-content[data-identifier="toolbar-menu__options"]'
            )
            if (menu) injectCustomMenuButtons(menu)
          }, 100)
        })
        console.log('[Emoji Extension Userscript] Toolbar options trigger listener added')
      }
      if (chatComposerTrigger) {
        chatComposerTrigger.addEventListener('click', () => {
          setTimeout(() => {
            const menu = document.querySelector(
              '.chat-composer-dropdown__content[data-identifier="chat-composer-dropdown__menu"], .chat-composer-dropdown__menu-content[data-identifier="chat-composer-dropdown__menu"]'
            )
            if (menu) injectCustomMenuButtons(menu)
          }, 100)
        })
        console.log('[Emoji Extension Userscript] Chat composer trigger listener added')
      }
      menuTriggersInitialized = true
    }
    function injectCustomMenuButtons(menu) {
      if (menu.querySelector('.emoji-extension-menu-item')) return
      let dropdownMenu = menu.querySelector('ul.dropdown-menu')
      if (!dropdownMenu) dropdownMenu = menu.querySelector('ul.chat-composer-dropdown__list')
      if (!dropdownMenu) {
        console.warn(
          '[Emoji Extension Userscript] No dropdown-menu or chat-composer-dropdown__list found in expanded menu'
        )
        return
      }
      const isChatComposerMenu = dropdownMenu.classList.contains('chat-composer-dropdown__list')
      const itemClassName = isChatComposerMenu
        ? 'chat-composer-dropdown__item emoji-extension-menu-item'
        : 'dropdown-menu__item emoji-extension-menu-item'
      const btnClassName = isChatComposerMenu
        ? 'btn btn-icon-text chat-composer-dropdown__action-btn btn-transparent'
        : 'btn btn-icon-text'
      const emojiPickerItem = createEl('li', { className: itemClassName })
      const emojiPickerBtn = createEl('button', {
        className: btnClassName,
        type: 'button',
        title: '表情包选择器',
        innerHTML: `
      <svg class="fa d-icon d-icon-smile svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#far-smile"></use></svg>
      <span class="d-button-label">表情包选择器</span>
    `
      })
      emojiPickerBtn.addEventListener('click', async e => {
        e.stopPropagation()
        if (menu.parentElement) menu.remove()
        if (currentPicker) {
          closeCurrentPicker()
          return
        }
        currentPicker = await createEmojiPicker()
        if (!currentPicker) return
        document.body.appendChild(currentPicker)
        currentPicker.style.position = 'fixed'
        currentPicker.style.top = '0'
        currentPicker.style.left = '0'
        currentPicker.style.right = '0'
        currentPicker.style.bottom = '0'
        currentPicker.style.zIndex = '999999'
        setTimeout(() => {
          const handleClick = e$1 => {
            if (currentPicker && !currentPicker.contains(e$1.target)) {
              closeCurrentPicker()
              document.removeEventListener('click', handleClick)
            }
          }
          document.addEventListener('click', handleClick)
        }, 100)
      })
      emojiPickerItem.appendChild(emojiPickerBtn)
      dropdownMenu.appendChild(emojiPickerItem)
      const quickInsertItem = createEl('li', { className: itemClassName })
      const quickInsertBtn = createEl('button', {
        className: btnClassName,
        type: 'button',
        title: '快捷输入',
        innerHTML: `
      <svg class="fa d-icon d-icon-list svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#list"></use></svg>
      <span class="d-button-label">快捷输入</span>
    `
      })
      quickInsertBtn.addEventListener('click', e => {
        e.stopPropagation()
        if (menu.parentElement) menu.remove()
        const quickMenu = createQuickInsertMenu()
        ;(document.querySelector('#d-menu-portals') || document.body).appendChild(quickMenu)
        const rect = quickInsertBtn.getBoundingClientRect()
        quickMenu.style.position = 'fixed'
        quickMenu.style.zIndex = '10000'
        quickMenu.style.top = `${rect.bottom + 5}px`
        quickMenu.style.left = `${Math.max(8, Math.min(rect.left + rect.width / 2 - 150, window.innerWidth - 300))}px`
        const removeMenu = ev => {
          if (!quickMenu.contains(ev.target)) {
            if (quickMenu.parentElement) quickMenu.parentElement.removeChild(quickMenu)
            document.removeEventListener('click', removeMenu)
          }
        }
        setTimeout(() => document.addEventListener('click', removeMenu), 100)
      })
      quickInsertItem.appendChild(quickInsertBtn)
      dropdownMenu.appendChild(quickInsertItem)
      console.log('[Emoji Extension Userscript] Custom menu buttons injected')
    }
    var currentPicker = null
    function closeCurrentPicker() {
      if (currentPicker) {
        currentPicker.remove()
        currentPicker = null
      }
    }
    function injectEmojiButton(toolbar) {
      if (toolbar.querySelector('.emoji-extension-button')) return
      const isChatComposer = toolbar.classList.contains('chat-composer__inner-container')
      const button = createEl('button', {
        className:
          'btn no-text btn-icon toolbar__button nacho-emoji-picker-button emoji-extension-button',
        title: '表情包',
        type: 'button',
        innerHTML: '🐈‍⬛'
      })
      const popularButton = createEl('button', {
        className:
          'btn no-text btn-icon toolbar__button nacho-emoji-popular-button emoji-extension-button',
        title: '常用表情',
        type: 'button',
        innerHTML: '⭐'
      })
      if (isChatComposer) {
        button.classList.add(
          'fk-d-menu__trigger',
          'emoji-picker-trigger',
          'chat-composer-button',
          'btn-transparent',
          '-emoji'
        )
        button.setAttribute('aria-expanded', 'false')
        button.setAttribute('data-identifier', 'emoji-picker')
        button.setAttribute('data-trigger', '')
        popularButton.classList.add(
          'fk-d-menu__trigger',
          'popular-emoji-trigger',
          'chat-composer-button',
          'btn-transparent',
          '-popular'
        )
        popularButton.setAttribute('aria-expanded', 'false')
        popularButton.setAttribute('data-identifier', 'popular-emoji')
        popularButton.setAttribute('data-trigger', '')
      }
      button.addEventListener('click', async e => {
        e.stopPropagation()
        if (currentPicker) {
          closeCurrentPicker()
          return
        }
        currentPicker = await createEmojiPicker()
        if (!currentPicker) return
        document.body.appendChild(currentPicker)
        const buttonRect = button.getBoundingClientRect()
        if (
          currentPicker.classList.contains('modal') ||
          currentPicker.className.includes('d-modal')
        ) {
          currentPicker.style.position = 'fixed'
          currentPicker.style.top = '0'
          currentPicker.style.left = '0'
          currentPicker.style.right = '0'
          currentPicker.style.bottom = '0'
          currentPicker.style.zIndex = '999999'
        } else {
          currentPicker.style.position = 'fixed'
          const margin = 8
          const vpWidth = window.innerWidth
          const vpHeight = window.innerHeight
          currentPicker.style.top = buttonRect.bottom + margin + 'px'
          currentPicker.style.left = buttonRect.left + 'px'
          const pickerRect = currentPicker.getBoundingClientRect()
          const spaceBelow = vpHeight - buttonRect.bottom
          const neededHeight = pickerRect.height + margin
          let top = buttonRect.bottom + margin
          if (spaceBelow < neededHeight)
            top = Math.max(margin, buttonRect.top - pickerRect.height - margin)
          let left = buttonRect.left
          if (left + pickerRect.width + margin > vpWidth)
            left = Math.max(margin, vpWidth - pickerRect.width - margin)
          if (left < margin) left = margin
          currentPicker.style.top = top + 'px'
          currentPicker.style.left = left + 'px'
        }
        setTimeout(() => {
          const handleClick = e$1 => {
            if (currentPicker && !currentPicker.contains(e$1.target) && e$1.target !== button) {
              closeCurrentPicker()
              document.removeEventListener('click', handleClick)
            }
          }
          document.addEventListener('click', handleClick)
        }, 100)
      })
      popularButton.addEventListener('click', e => {
        e.stopPropagation()
        closeCurrentPicker()
        showPopularEmojisModal()
      })
      const quickInsertButton = createEl('button', {
        className: 'btn no-text btn-icon toolbar__button quick-insert-button',
        title: '快捷输入',
        type: 'button',
        innerHTML: '⎘'
      })
      if (isChatComposer) {
        quickInsertButton.classList.add(
          'fk-d-menu__trigger',
          'chat-composer-button',
          'btn-transparent'
        )
        quickInsertButton.setAttribute('aria-expanded', 'false')
        quickInsertButton.setAttribute('data-trigger', '')
      }
      quickInsertButton.addEventListener('click', e => {
        e.stopPropagation()
        const menu = createQuickInsertMenu()
        ;(document.querySelector('#d-menu-portals') || document.body).appendChild(menu)
        const rect = quickInsertButton.getBoundingClientRect()
        menu.style.position = 'fixed'
        menu.style.zIndex = '10000'
        menu.style.top = `${rect.bottom + 5}px`
        menu.style.left = `${Math.max(8, Math.min(rect.left + rect.width / 2 - 150, window.innerWidth - 300))}px`
        const removeMenu = ev => {
          if (!menu.contains(ev.target)) {
            if (menu.parentElement) menu.parentElement.removeChild(menu)
            document.removeEventListener('click', removeMenu)
          }
        }
        setTimeout(() => document.addEventListener('click', removeMenu), 100)
      })
      try {
        if (isChatComposer) {
          const existingEmojiTrigger = toolbar.querySelector(
            '.emoji-picker-trigger:not(.emoji-extension-button)'
          )
          if (existingEmojiTrigger) {
            toolbar.insertBefore(button, existingEmojiTrigger)
            toolbar.insertBefore(quickInsertButton, existingEmojiTrigger)
            toolbar.insertBefore(popularButton, existingEmojiTrigger)
          } else {
            toolbar.appendChild(button)
            toolbar.appendChild(quickInsertButton)
            toolbar.appendChild(popularButton)
          }
        } else {
          toolbar.appendChild(button)
          toolbar.appendChild(quickInsertButton)
          toolbar.appendChild(popularButton)
        }
      } catch (error) {
        console.error('[Emoji Extension Userscript] Failed to inject button:', error)
      }
    }
    function attemptInjection() {
      const toolbars = findAllToolbars()
      let injectedCount = 0
      toolbars.forEach(toolbar => {
        if (!toolbar.querySelector('.emoji-extension-button')) {
          console.log('[Emoji Extension Userscript] Toolbar found, injecting button.')
          injectEmojiButton(toolbar)
          injectedCount++
        }
      })
      setupForceMobileMenuTriggers()
      return {
        injectedCount,
        totalToolbars: toolbars.length
      }
    }
    function startPeriodicInjection() {
      setInterval(() => {
        findAllToolbars().forEach(toolbar => {
          if (!toolbar.querySelector('.emoji-extension-button')) {
            console.log('[Emoji Extension Userscript] New toolbar found, injecting button.')
            injectEmojiButton(toolbar)
          }
        })
        setupForceMobileMenuTriggers()
      }, 3e4)
    }
    init_createEl()
    function userscriptNotify(message, type = 'info', timeout = 4e3) {
      try {
        let container = document.getElementById('emoji-ext-userscript-toast')
        if (!container) {
          container = createEl('div', {
            attrs: {
              id: 'emoji-ext-userscript-toast',
              'aria-live': 'polite'
            },
            style: `
        position: fixed;
        right: 12px; 
        bottom: 12px; 
        z-index: 2147483646; 
        display:flex; 
        flex-direction:column; 
        gap:8px;
        `
          })
          try {
            if (document.body) document.body.appendChild(container)
            else document.documentElement.appendChild(container)
          } catch (e) {
            document.documentElement.appendChild(container)
          }
          container.style.position = 'fixed'
          container.style.right = '12px'
          container.style.bottom = '12px'
          container.style.zIndex = String(2147483646)
          try {
            container.style.setProperty('z-index', String(2147483646), 'important')
          } catch (_e) {}
          container.style.display = 'flex'
          container.style.flexDirection = 'column'
          container.style.gap = '8px'
          container.style.pointerEvents = 'auto'
        }
        const el = createEl('div', {
          text: message,
          style: `padding:8px 12px; border-radius:6px; color:#fff; font-size:13px; max-width:320px; word-break:break-word; opacity:0; transform: translateY(8px); transition: all 220ms ease;`
        })
        if (type === 'success') el.style.setProperty('background', '#16a34a', 'important')
        else if (type === 'error') el.style.setProperty('background', '#dc2626', 'important')
        else el.style.setProperty('background', '#0369a1', 'important')
        container.appendChild(el)
        el.offsetHeight
        el.style.opacity = '1'
        el.style.transform = 'translateY(0)'
        const id = setTimeout(() => {
          try {
            el.style.opacity = '0'
            el.style.transform = 'translateY(8px)'
            setTimeout(() => el.remove(), 250)
          } catch (_e) {}
          clearTimeout(id)
        }, timeout)
        try {
          console.log('[UserscriptNotify] shown:', message, 'type=', type)
        } catch (_e) {}
        return () => {
          try {
            el.remove()
          } catch (_e) {}
          clearTimeout(id)
        }
      } catch (_e) {
        return () => {}
      }
    }
    init_createEl()
    init_themeSupport()
    init_state()
    init_injectStyles()
    let floatingButton = null
    let isButtonVisible = false
    const FLOATING_BUTTON_STYLES = `
.emoji-extension-floating-container {
  position: fixed !important;
  bottom: 20px !important;
  right: 20px !important;
  display: flex !important;
  flex-direction: column !important;
  gap: 10px !important;
  z-index: 999999 !important;
}

.emoji-extension-floating-button {
  width: 56px !important;
  height: 56px !important;
  border-radius: 50% !important;
  background: transparent;
  border: none !important;
  box-shadow: 0 4px 12px var(--emoji-button-shadow) !important;
  cursor: pointer !important;
  color: white !important;
  display: flex !important;
  align-items: center !important;
  justify-content: center !important;
  transition: all 0.2s ease !important;
  opacity: 0.95 !important;
}

.emoji-extension-floating-button:hover { 
  transform: scale(1.05) !important;
 }
.emoji-extension-floating-button:active { transform: scale(0.95) !important; }

.emoji-extension-floating-button.secondary {
  background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%) !important;
}

.emoji-extension-floating-button.hidden {
  opacity: 0 !important;
  pointer-events: none !important;
  transform: translateY(20px) !important;
}

@media (max-width: 768px) {
  .emoji-extension-floating-button { 
  width: 48px !important; 
  height: 48px !important; 
  font-size: 20px !important; }
  .emoji-extension-floating-container { bottom: 15px !important; right: 15px !important; }
}
`
    function injectStyles() {
      injectGlobalThemeStyles()
      ensureStyleInjected('emoji-extension-floating-button-styles', FLOATING_BUTTON_STYLES)
    }
    function createManualButton() {
      const button = createEl('button', {
        className: 'emoji-extension-floating-button',
        title: '手动注入表情按钮 (Manual Emoji Injection)',
        innerHTML: '🐈‍⬛'
      })
      button.addEventListener('click', async e => {
        e.stopPropagation()
        e.preventDefault()
        button.style.transform = 'scale(0.9)'
        button.innerHTML = '⏳'
        try {
          if (attemptInjection().injectedCount > 0) {
            button.innerHTML = '✅'
            setTimeout(() => {
              button.innerHTML = '🐈‍⬛'
              button.style.transform = 'scale(1)'
            }, 1500)
          } else {
            button.innerHTML = '❌'
            setTimeout(() => {
              button.innerHTML = '🐈‍⬛'
              button.style.transform = 'scale(1)'
            }, 1500)
          }
        } catch (error) {
          button.innerHTML = '⚠️'
          setTimeout(() => {
            button.innerHTML = '🐈‍⬛'
            button.style.transform = 'scale(1)'
          }, 1500)
          console.error('[Emoji Extension Userscript] Manual injection error:', error)
        }
      })
      return button
    }
    async function invokeAutoRead(showNotify = false) {
      try {
        const fn = window.callAutoReadRepliesV2 || window.autoReadAllRepliesV2
        console.log(
          '[Emoji Extension] invokeAutoRead: found fn=',
          !!fn,
          ' typeof=',
          typeof fn,
          ' showNotify=',
          showNotify
        )
        if (fn && typeof fn === 'function') {
          const res = await fn()
          console.log('[Emoji Extension] invokeAutoRead: fn returned', res)
          if (showNotify) userscriptNotify('自动阅读已触发', 'success')
        } else {
          console.warn('[Emoji Extension] autoRead function not available on window')
          console.log('[Emoji Extension] invokeAutoRead: attempting userscriptNotify fallback')
          userscriptNotify('自动阅读功能当前不可用', 'error')
        }
      } catch (err) {
        console.error('[Emoji Extension] auto-read menu invocation failed', err)
        if (showNotify)
          userscriptNotify(
            '自动阅读调用失败: ' + (err && err.message ? err.message : String(err)),
            'error'
          )
      }
    }
    function createAutoReadMenuItem(variant = 'dropdown') {
      if (variant === 'dropdown') {
        const li$1 = createEl('li', { className: 'submenu-item emoji-extension-auto-read' })
        const a = createEl('a', {
          className: 'submenu-link',
          attrs: {
            href: '#',
            title: '像插件一样自动阅读话题 (Auto-read topics)'
          },
          innerHTML: `
      <svg class="fa d-icon d-icon-book svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#book"></use></svg>
      自动阅读
    `
        })
        a.addEventListener('click', async e => {
          e.preventDefault()
          e.stopPropagation()
          await invokeAutoRead(true)
        })
        li$1.appendChild(a)
        return li$1
      }
      const li = createEl('li', {
        className: 'submenu-item emoji-extension-auto-read sidebar-section-link-wrapper',
        style: 'list-style: none; padding-left: 0;'
      })
      const btn = createEl('button', {
        className:
          'fk-d-menu__trigger sidebar-more-section-trigger sidebar-section-link sidebar-more-section-links-details-summary sidebar-row --link-button sidebar-section-link sidebar-row',
        attrs: {
          type: 'button',
          title: '像插件一样自动阅读话题 (Auto-read topics)',
          'aria-expanded': 'false',
          'data-identifier': 'emoji-ext-auto-read',
          'data-trigger': ''
        },
        innerHTML: `
      <span class="sidebar-section-link-prefix icon">
        <svg class="fa d-icon d-icon-book svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#book"></use></svg>
      </span>
      <span class="sidebar-section-link-content-text">自动阅读</span>
    `,
        style: `
    background: transparent; 
    border: none;
    `
      })
      btn.addEventListener('click', async e => {
        e.preventDefault()
        e.stopPropagation()
        await invokeAutoRead(true)
      })
      li.appendChild(btn)
      return li
    }
    function showFloatingButton() {
      if (userscriptState.settings?.forceMobileMode || false) {
        if (document.querySelector('#d-menu-portals')) {
          console.log(
            '[Emoji Extension Userscript] Force mobile mode with #d-menu-portals detected, skipping floating button'
          )
          return
        }
      }
      if (floatingButton) return
      injectStyles()
      const manual = createManualButton()
      const wrapper = createEl('div', { className: 'emoji-extension-floating-container' })
      wrapper.appendChild(manual)
      document.body.appendChild(wrapper)
      floatingButton = wrapper
      isButtonVisible = true
      console.log(
        '[Emoji Extension Userscript] Floating manual injection button shown (bottom-right)'
      )
    }
    async function injectIntoUserMenu() {
      const SELECTOR_SIDEBAR = '#sidebar-section-content-community'
      const SELECTOR_OTHER_ANCHOR =
        'a.menu-item[title="其他服务"], a.menu-item.vdm[title="其他服务"]'
      const SELECTOR_OTHER_DROPDOWN = '.d-header-dropdown .d-dropdown-menu'
      for (;;) {
        const otherAnchor = document.querySelector(SELECTOR_OTHER_ANCHOR)
        if (otherAnchor) {
          const dropdown = otherAnchor.querySelector(SELECTOR_OTHER_DROPDOWN)
          if (dropdown) {
            dropdown.appendChild(createAutoReadMenuItem('dropdown'))
            isButtonVisible = true
            console.log('[Emoji Extension Userscript] Auto-read injected into 其他服务 dropdown')
            return
          }
        }
        const sidebar = document.querySelector(SELECTOR_SIDEBAR)
        if (sidebar) {
          sidebar.appendChild(createAutoReadMenuItem('sidebar'))
          isButtonVisible = true
          console.log(
            '[Emoji Extension Userscript] Auto-read injected into sidebar #sidebar-section-content-community'
          )
          return
        }
        await new Promise(resolve => setTimeout(resolve, 500))
      }
    }
    async function showAutoReadInMenu() {
      injectStyles()
      try {
        await injectIntoUserMenu()
        return
      } catch (e) {
        console.warn('[Emoji Extension Userscript] injecting menu item failed', e)
      }
    }
    function hideFloatingButton() {
      if (floatingButton) {
        floatingButton.classList.add('hidden')
        setTimeout(() => {
          if (floatingButton) {
            floatingButton.remove()
            floatingButton = null
            isButtonVisible = false
          }
        }, 300)
        console.log('[Emoji Extension Userscript] Floating manual injection button hidden')
      }
    }
    function autoShowFloatingButton() {
      if (!isButtonVisible) {
        console.log(
          '[Emoji Extension Userscript] Auto-showing floating button due to injection difficulties'
        )
        showFloatingButton()
      }
    }
    function checkAndShowFloatingButton() {
      if (userscriptState.settings?.forceMobileMode || false) {
        if (document.querySelector('#d-menu-portals')) {
          if (isButtonVisible) hideFloatingButton()
          return
        }
      }
      const existingButtons = document.querySelectorAll('.emoji-extension-button')
      if (existingButtons.length === 0 && !isButtonVisible)
        setTimeout(() => {
          autoShowFloatingButton()
        }, 2e3)
      else if (existingButtons.length > 0 && isButtonVisible) hideFloatingButton()
    }
    init_userscript_storage()
    init_state()
    async function initializeUserscriptData() {
      const data = await loadDataFromLocalStorageAsync().catch(err => {
        console.warn(
          '[Userscript] loadDataFromLocalStorageAsync failed, falling back to sync loader',
          err
        )
        return loadDataFromLocalStorage()
      })
      userscriptState.emojiGroups = data.emojiGroups || []
      userscriptState.settings = data.settings || userscriptState.settings
    }
    function isDiscoursePage() {
      if (
        document.querySelectorAll(
          'meta[name*="discourse"], meta[content*="discourse"], meta[property*="discourse"]'
        ).length > 0
      ) {
        console.log('[Emoji Extension Userscript] Discourse detected via meta tags')
        return true
      }
      const generatorMeta = document.querySelector('meta[name="generator"]')
      if (generatorMeta) {
        if ((generatorMeta.getAttribute('content')?.toLowerCase() || '').includes('discourse')) {
          console.log('[Emoji Extension Userscript] Discourse detected via generator meta')
          return true
        }
      }
      if (
        document.querySelectorAll(
          '#main-outlet, .ember-application, textarea.d-editor-input, .ProseMirror.d-editor-input'
        ).length > 0
      ) {
        console.log('[Emoji Extension Userscript] Discourse elements detected')
        return true
      }
      console.log('[Emoji Extension Userscript] Not a Discourse site')
      return false
    }
    async function initializeEmojiFeature(maxAttempts = 10, delay = 1e3) {
      console.log('[Emoji Extension Userscript] Initializing...')
      logPlatformInfo()
      await initializeUserscriptData()
      try {
        if (userscriptState.settings?.enableBatchParseImages !== false) {
          initOneClickAdd()
          initPhotoSwipeTopbarUserscript()
          console.log('[Userscript] One-click batch parse images enabled')
        } else console.log('[Userscript] One-click batch parse images disabled by setting')
      } catch (e) {
        console.warn('[Userscript] initOneClickAdd failed', e)
      }
      try {
        if (userscriptState.settings?.enableCalloutSuggestions !== false) {
          initCalloutSuggestionsUserscript()
          console.log('[Userscript] Callout suggestions enabled')
        } else console.log('[Userscript] Callout suggestions disabled by user setting')
      } catch (e) {
        console.warn('[Userscript] initCalloutSuggestionsUserscript failed', e)
      }
      try {
        showAutoReadInMenu()
      } catch (e) {
        console.warn('[Userscript] showAutoReadInMenu failed', e)
      }
      function exposeAutoReadWrapper() {
        try {
          const existing = window.autoReadAllRepliesV2
          if (existing && typeof existing === 'function') {
            window.callAutoReadRepliesV2 = topicId => {
              try {
                return existing(topicId)
              } catch (e) {
                console.warn('[Userscript] callAutoReadRepliesV2 invocation failed', e)
              }
            }
            console.log('[Userscript] callAutoReadRepliesV2 is exposed')
            return
          }
          window.callAutoReadRepliesV2 = topicId => {
            try {
              const fn = window.autoReadAllRepliesV2
              if (fn && typeof fn === 'function') return fn(topicId)
            } catch (e) {
              console.warn('[Userscript] callAutoReadRepliesV2 invocation failed', e)
            }
            console.warn('[Userscript] autoReadAllRepliesV2 not available on this page yet')
          }
        } catch (e) {
          console.warn('[Userscript] exposeAutoReadWrapper failed', e)
        }
      }
      exposeAutoReadWrapper()
      let attempts = 0
      function attemptToolbarInjection() {
        attempts++
        const result = attemptInjection()
        if (result.injectedCount > 0 || result.totalToolbars > 0) {
          console.log(
            `[Emoji Extension Userscript] Injection successful: ${result.injectedCount} buttons injected into ${result.totalToolbars} toolbars`
          )
          return
        }
        if (attempts < maxAttempts) {
          console.log(
            `[Emoji Extension Userscript] Toolbar not found, attempt ${attempts}/${maxAttempts}. Retrying in ${delay / 1e3}s.`
          )
          setTimeout(attemptToolbarInjection, delay)
        } else {
          console.error(
            '[Emoji Extension Userscript] Failed to find toolbar after multiple attempts.'
          )
          console.log('[Emoji Extension Userscript] Showing floating button as fallback')
          showFloatingButton()
        }
      }
      if (document.readyState === 'loading')
        document.addEventListener('DOMContentLoaded', attemptToolbarInjection)
      else attemptToolbarInjection()
      startPeriodicInjection()
      setInterval(() => {
        checkAndShowFloatingButton()
      }, 5e3)
    }
    if (isDiscoursePage()) {
      console.log('[Emoji Extension Userscript] Discourse detected, initializing emoji feature')
      initializeEmojiFeature()
    } else console.log('[Emoji Extension Userscript] Not a Discourse site, skipping injection')
  })()
})()

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址