Linux do 表情扩展 (Emoji Extension) lite

为论坛网站添加表情选择器功能 (Add emoji picker functionality to forum websites)

当前为 2025-09-16 提交的版本,查看 最新版本

// ==UserScript==
// @name         Linux do 表情扩展 (Emoji Extension) lite
// @namespace    https://github.com/stevessr/bug-v3
// @version      1.0.5
// @description  为论坛网站添加表情选择器功能 (Add emoji picker functionality to forum websites)
// @author       stevessr
// @match        https://linux.do/*
// @match        https://meta.discourse.org/*
// @match        https://*.discourse.org/*
// @match        http://localhost:5173/*
// @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 = {
          imageScale: 30,
          gridColumns: 4,
          outputFormat: 'markdown',
          forceMobileMode: false,
          defaultGroup: 'nachoneko',
          showSearchBar: true,
          enableFloatingPreview: true
        }
        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)
        console.error('[Userscript] Failed to load from localStorage:', error)
        return {
          emojiGroups: [],
          settings: {
            imageScale: 30,
            gridColumns: 4,
            outputFormat: 'markdown',
            forceMobileMode: false,
            defaultGroup: 'nachoneko',
            showSearchBar: true,
            enableFloatingPreview: true
          }
        }
      }
    }
    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: {
            imageScale: 30,
            gridColumns: 4,
            outputFormat: 'markdown',
            forceMobileMode: false,
            defaultGroup: 'nachoneko',
            showSearchBar: true,
            enableFloatingPreview: true
          }
        }
      }
    }
    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 exportUserscriptData() {
      try {
        const data = loadDataFromLocalStorage()
        return JSON.stringify(data, null, 2)
      } catch (error) {
        console.error('[Userscript] Failed to export data:', error)
        return ''
      }
    }
    function importUserscriptData(jsonData) {
      try {
        const data = JSON.parse(jsonData)
        if (data.emojiGroups && Array.isArray(data.emojiGroups))
          saveDataToLocalStorage({ emojiGroups: data.emojiGroups })
        if (data.settings && typeof data.settings === 'object')
          saveDataToLocalStorage({ settings: data.settings })
        console.log('[Userscript] Data imported successfully')
        return true
      } catch (error) {
        console.error('[Userscript] Failed to import data:', error)
        return false
      }
    }
    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
    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'
    })
    let userscriptState
    const init_state = __esmMin(() => {
      userscriptState = {
        emojiGroups: [],
        settings: {
          imageScale: 30,
          gridColumns: 4,
          outputFormat: 'markdown',
          forceMobileMode: false,
          defaultGroup: 'nachoneko',
          showSearchBar: true,
          enableFloatingPreview: true
        },
        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
      }
      return el
    }
    const init_createEl = __esmMin(() => {})
    init_createEl()
    init_state()
    init_userscript_storage()
    function notify(message, type = 'info', timeout = 4e3) {
      try {
        let container = document.getElementById('emoji-ext-toast-container')
        if (!container) {
          container = document.createElement('div')
          container.id = 'emoji-ext-toast-container'
          container.style.position = 'fixed'
          container.style.right = '12px'
          container.style.bottom = '12px'
          container.style.zIndex = '2147483647'
          container.style.display = 'flex'
          container.style.flexDirection = 'column'
          container.style.gap = '8px'
          document.body.appendChild(container)
        }
        const el = document.createElement('div')
        el.textContent = message
        el.style.padding = '8px 12px'
        el.style.borderRadius = '6px'
        el.style.boxShadow = '0 2px 8px rgba(0,0,0,0.12)'
        el.style.color = '#ffffff'
        el.style.fontSize = '13px'
        el.style.maxWidth = '320px'
        el.style.wordBreak = 'break-word'
        if (type === 'success') el.style.background = '#16a34a'
        else if (type === 'error') el.style.background = '#dc2626'
        else el.style.background = '#0369a1'
        container.appendChild(el)
        const id = setTimeout(() => {
          el.remove()
          clearTimeout(id)
        }, timeout)
        return () => {
          el.remove()
          clearTimeout(id)
        }
      } catch (e) {
        try {
          alert(message)
        } catch (_e) {}
        return () => {}
      }
    }
    async function postTimings(topicId, timings) {
      function readCsrfToken() {
        try {
          const meta = document.querySelector('meta[name="csrf-token"]')
          if (meta && meta.content) return meta.content
          const input = document.querySelector('input[name="authenticity_token"]')
          if (input && input.value) return input.value
          const match = document.cookie.match(/csrf_token=([^;]+)/)
          if (match) return decodeURIComponent(match[1])
        } catch (e) {
          console.warn('[timingsBinder] failed to read csrf token', e)
        }
        return null
      }
      const csrf = readCsrfToken() || ''
      const map = {}
      if (Array.isArray(timings)) for (let i = 0; i < timings.length; i++) map[i] = timings[i]
      else
        for (const k of Object.keys(timings)) {
          const key = Number(k)
          if (!Number.isNaN(key)) map[key] = timings[key]
        }
      const params = new URLSearchParams()
      let maxTime = 0
      for (const idxStr of Object.keys(map)) {
        const idx = Number(idxStr)
        const val = String(map[idx])
        params.append(`timings[${idx}]`, val)
        const num = Number(val)
        if (!Number.isNaN(num) && num > maxTime) maxTime = num
      }
      params.append('topic_time', String(maxTime))
      params.append('topic_id', String(topicId))
      const url = 'https://linux.do/topics/timings'
      const headers = {
        'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
        'x-requested-with': 'XMLHttpRequest'
      }
      if (csrf) headers['x-csrf-token'] = csrf
      return await fetch(url, {
        method: 'POST',
        body: params.toString(),
        credentials: 'same-origin',
        headers
      })
    }
    function sleep(ms) {
      return new Promise(resolve => setTimeout(resolve, ms))
    }
    async function fetchPostsForTopic(topicId) {
      const url = `/t/${topicId}/posts.json`
      const resp = await fetch(url, { credentials: 'same-origin' })
      if (!resp.ok) throw new Error(`failed to fetch posts.json: ${resp.status}`)
      const data = await resp.json()
      let posts = []
      let totalCount = 0
      if (data && data.post_stream && Array.isArray(data.post_stream.posts)) {
        posts = data.post_stream.posts
        if (posts.length > 0 && typeof posts[0].posts_count === 'number')
          totalCount = posts[0].posts_count
      }
      if ((!posts || posts.length === 0) && data && Array.isArray(data.posts)) posts = data.posts
      if (!totalCount) {
        if (data && typeof data.highest_post_number === 'number')
          totalCount = data.highest_post_number
        else if (data && typeof data.posts_count === 'number') totalCount = data.posts_count
        else if (posts && posts.length > 0) totalCount = posts.length
      }
      return {
        posts,
        totalCount
      }
    }
    async function autoReadAll(topicId) {
      try {
        let tid = topicId || 0
        if (!tid) {
          const m1 = window.location.pathname.match(/t\/topic\/(\d+)/)
          const m2 = window.location.pathname.match(/t\/(\d+)/)
          if (m1 && m1[1]) tid = Number(m1[1])
          else if (m2 && m2[1]) tid = Number(m2[1])
          else {
            const el = document.querySelector('[data-topic-id]')
            if (el) tid = Number(el.getAttribute('data-topic-id')) || 0
          }
        }
        if (!tid) {
          notify('无法推断 topic_id,自动阅读取消', 'error')
          return
        }
        notify(`开始自动阅读话题 ${tid} 的所有帖子...`, 'info')
        const { posts, totalCount } = await fetchPostsForTopic(tid)
        if ((!posts || posts.length === 0) && !totalCount) {
          notify('未获取到任何帖子或总数信息', 'error')
          return
        }
        const total = totalCount || posts.length
        const postNumbers = []
        for (let n = 1; n <= total; n++) postNumbers.push(n)
        const BATCH_SIZE = 7
        for (let i = 0; i < postNumbers.length; i += BATCH_SIZE) {
          const batch = postNumbers.slice(i, i + BATCH_SIZE)
          const timings = {}
          for (const pn of batch) timings[pn] = 1e3
          try {
            await postTimings(tid, timings)
            notify(`已标记 ${Object.keys(timings).length} 个帖子为已读(发送)`, 'success')
          } catch (e) {
            notify('发送阅读标记失败: ' + (e && e.message ? e.message : String(e)), 'error')
          }
          const delay = 500 + Math.floor(Math.random() * 1e3)
          await sleep(delay)
        }
        notify('自动阅读完成', 'success')
      } catch (e) {
        notify('自动阅读异常: ' + (e && e.message ? e.message : String(e)), 'error')
      }
    }
    window.autoReadAllReplies = autoReadAll
    function insertIntoEditor(text) {
      const textArea = document.querySelector('textarea.d-editor-input')
      const richEle = document.querySelector('.ProseMirror.d-editor-input')
      if (!textArea && !richEle) {
        console.error('找不到输入框')
        return
      }
      if (textArea) {
        const start = textArea.selectionStart
        const end = textArea.selectionEnd
        const value = textArea.value
        textArea.value = value.substring(0, start) + text + value.substring(end)
        textArea.setSelectionRange(start + text.length, start + text.length)
        textArea.focus()
        const event = new Event('input', { bubbles: true })
        textArea.dispatchEvent(event)
      } else if (richEle) {
        const selection = window.getSelection()
        if (selection && selection.rangeCount > 0) {
          const range = selection.getRangeAt(0)
          const textNode = document.createTextNode(text)
          range.insertNode(textNode)
          range.setStartAfter(textNode)
          range.setEndAfter(textNode)
          selection.removeAllRanges()
          selection.addRange(range)
        }
        richEle.focus()
      }
    }
    const ImageUploader = class {
      waitingQueue = []
      uploadingQueue = []
      failedQueue = []
      successQueue = []
      isProcessing = false
      maxRetries = 2
      progressDialog = null
      async uploadImage(file) {
        return new Promise((resolve, reject) => {
          const item = {
            id: `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
            file,
            resolve,
            reject,
            retryCount: 0,
            status: 'waiting',
            timestamp: Date.now()
          }
          this.waitingQueue.push(item)
          this.updateProgressDialog()
          this.processQueue()
        })
      }
      moveToQueue(item, targetStatus) {
        this.waitingQueue = this.waitingQueue.filter(i => i.id !== item.id)
        this.uploadingQueue = this.uploadingQueue.filter(i => i.id !== item.id)
        this.failedQueue = this.failedQueue.filter(i => i.id !== item.id)
        this.successQueue = this.successQueue.filter(i => i.id !== item.id)
        item.status = targetStatus
        switch (targetStatus) {
          case 'waiting':
            this.waitingQueue.push(item)
            break
          case 'uploading':
            this.uploadingQueue.push(item)
            break
          case 'failed':
            this.failedQueue.push(item)
            break
          case 'success':
            this.successQueue.push(item)
            break
        }
        this.updateProgressDialog()
      }
      async processQueue() {
        if (this.isProcessing || this.waitingQueue.length === 0) return
        this.isProcessing = true
        while (this.waitingQueue.length > 0) {
          const item = this.waitingQueue.shift()
          if (!item) continue
          this.moveToQueue(item, 'uploading')
          try {
            const result = await this.performUpload(item.file)
            item.result = result
            this.moveToQueue(item, 'success')
            item.resolve(result)
            const markdown = `![${result.original_filename}](${result.url})`
            insertIntoEditor(markdown)
          } catch (error) {
            item.error = error
            if (this.shouldRetry(error, item)) {
              item.retryCount++
              if (error.error_type === 'rate_limit' && error.extras?.wait_seconds)
                await this.sleep(error.extras.wait_seconds * 1e3)
              else await this.sleep(Math.pow(2, item.retryCount) * 1e3)
              this.moveToQueue(item, 'waiting')
            } else {
              this.moveToQueue(item, 'failed')
              item.reject(error)
            }
          }
        }
        this.isProcessing = false
      }
      shouldRetry(error, item) {
        if (item.retryCount >= this.maxRetries) return false
        return error.error_type === 'rate_limit'
      }
      retryFailedItem(itemId) {
        const item = this.failedQueue.find(i => i.id === itemId)
        if (item && item.retryCount < this.maxRetries) {
          item.retryCount++
          this.moveToQueue(item, 'waiting')
          this.processQueue()
        }
      }
      showProgressDialog() {
        if (this.progressDialog) return
        this.progressDialog = this.createProgressDialog()
        document.body.appendChild(this.progressDialog)
      }
      hideProgressDialog() {
        if (this.progressDialog) {
          this.progressDialog.remove()
          this.progressDialog = null
        }
      }
      updateProgressDialog() {
        if (!this.progressDialog) return
        const allItems = [
          ...this.waitingQueue,
          ...this.uploadingQueue,
          ...this.failedQueue,
          ...this.successQueue
        ]
        this.renderQueueItems(this.progressDialog, allItems)
      }
      async sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms))
      }
      createProgressDialog() {
        const dialog = document.createElement('div')
        dialog.style.cssText = `
      position: fixed;
      top: 20px;
      right: 20px;
      width: 350px;
      max-height: 400px;
      background: white;
      border-radius: 8px;
      box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
      z-index: 10000;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      border: 1px solid #e5e7eb;
      overflow: hidden;
    `
        const header = document.createElement('div')
        header.style.cssText = `
      padding: 16px 20px;
      background: #f9fafb;
      border-bottom: 1px solid #e5e7eb;
      font-weight: 600;
      font-size: 14px;
      color: #374151;
      display: flex;
      justify-content: space-between;
      align-items: center;
    `
        header.textContent = '图片上传队列'
        const closeButton = document.createElement('button')
        closeButton.innerHTML = '✕'
        closeButton.style.cssText = `
      background: none;
      border: none;
      font-size: 16px;
      cursor: pointer;
      color: #6b7280;
      padding: 4px;
      border-radius: 4px;
      transition: background-color 0.2s;
    `
        closeButton.addEventListener('click', () => {
          this.hideProgressDialog()
        })
        closeButton.addEventListener('mouseenter', () => {
          closeButton.style.backgroundColor = '#e5e7eb'
        })
        closeButton.addEventListener('mouseleave', () => {
          closeButton.style.backgroundColor = 'transparent'
        })
        header.appendChild(closeButton)
        const content = document.createElement('div')
        content.className = 'upload-queue-content'
        content.style.cssText = `
      max-height: 320px;
      overflow-y: auto;
      padding: 12px;
    `
        dialog.appendChild(header)
        dialog.appendChild(content)
        return dialog
      }
      renderQueueItems(dialog, allItems) {
        const content = dialog.querySelector('.upload-queue-content')
        if (!content) return
        content.innerHTML = ''
        if (allItems.length === 0) {
          const emptyState = document.createElement('div')
          emptyState.style.cssText = `
        text-align: center;
        color: #6b7280;
        font-size: 14px;
        padding: 20px;
      `
          emptyState.textContent = '暂无上传任务'
          content.appendChild(emptyState)
          return
        }
        allItems.forEach(item => {
          const itemEl = document.createElement('div')
          itemEl.style.cssText = `
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 8px 12px;
        margin-bottom: 8px;
        background: #f9fafb;
        border-radius: 6px;
        border-left: 4px solid ${this.getStatusColor(item.status)};
      `
          const leftSide = document.createElement('div')
          leftSide.style.cssText = `
        flex: 1;
        min-width: 0;
      `
          const fileName = document.createElement('div')
          fileName.style.cssText = `
        font-size: 13px;
        font-weight: 500;
        color: #374151;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      `
          fileName.textContent = item.file.name
          const status = document.createElement('div')
          status.style.cssText = `
        font-size: 12px;
        color: #6b7280;
        margin-top: 2px;
      `
          status.textContent = this.getStatusText(item)
          leftSide.appendChild(fileName)
          leftSide.appendChild(status)
          const rightSide = document.createElement('div')
          rightSide.style.cssText = `
        display: flex;
        align-items: center;
        gap: 8px;
      `
          if (item.status === 'failed' && item.retryCount < this.maxRetries) {
            const retryButton = document.createElement('button')
            retryButton.innerHTML = '🔄'
            retryButton.style.cssText = `
          background: none;
          border: none;
          cursor: pointer;
          font-size: 14px;
          padding: 4px;
          border-radius: 4px;
          transition: background-color 0.2s;
        `
            retryButton.title = '重试上传'
            retryButton.addEventListener('click', () => {
              this.retryFailedItem(item.id)
            })
            retryButton.addEventListener('mouseenter', () => {
              retryButton.style.backgroundColor = '#e5e7eb'
            })
            retryButton.addEventListener('mouseleave', () => {
              retryButton.style.backgroundColor = 'transparent'
            })
            rightSide.appendChild(retryButton)
          }
          const statusIcon = document.createElement('div')
          statusIcon.style.cssText = `
        font-size: 16px;
      `
          statusIcon.textContent = this.getStatusIcon(item.status)
          rightSide.appendChild(statusIcon)
          itemEl.appendChild(leftSide)
          itemEl.appendChild(rightSide)
          content.appendChild(itemEl)
        })
      }
      getStatusColor(status) {
        switch (status) {
          case 'waiting':
            return '#f59e0b'
          case 'uploading':
            return '#3b82f6'
          case 'success':
            return '#10b981'
          case 'failed':
            return '#ef4444'
          default:
            return '#6b7280'
        }
      }
      getStatusText(item) {
        switch (item.status) {
          case 'waiting':
            return '等待上传'
          case 'uploading':
            return '正在上传...'
          case 'success':
            return '上传成功'
          case 'failed':
            if (item.error?.error_type === 'rate_limit')
              return `上传失败 - 请求过于频繁 (重试 ${item.retryCount}/${this.maxRetries})`
            return `上传失败 (重试 ${item.retryCount}/${this.maxRetries})`
          default:
            return '未知状态'
        }
      }
      getStatusIcon(status) {
        switch (status) {
          case 'waiting':
            return '⏳'
          case 'uploading':
            return '📤'
          case 'success':
            return '✅'
          case 'failed':
            return '❌'
          default:
            return '❓'
        }
      }
      async performUpload(file) {
        const sha1 = await this.calculateSHA1(file)
        const formData = new FormData()
        formData.append('upload_type', 'composer')
        formData.append('relativePath', 'null')
        formData.append('name', file.name)
        formData.append('type', file.type)
        formData.append('sha1_checksum', sha1)
        formData.append('file', file, file.name)
        const csrfToken = this.getCSRFToken()
        const headers = { 'X-Csrf-Token': csrfToken }
        if (document.cookie) headers['Cookie'] = document.cookie
        const response = await fetch(
          `https://linux.do/uploads.json?client_id=f06cb5577ba9410d94b9faf94e48c2d8`,
          {
            method: 'POST',
            headers,
            body: formData
          }
        )
        if (!response.ok) throw await response.json()
        return await response.json()
      }
      getCSRFToken() {
        const metaToken = document.querySelector('meta[name="csrf-token"]')
        if (metaToken) return metaToken.content
        const match = document.cookie.match(/csrf_token=([^;]+)/)
        if (match) return decodeURIComponent(match[1])
        const hiddenInput = document.querySelector('input[name="authenticity_token"]')
        if (hiddenInput) return hiddenInput.value
        console.warn('[Image Uploader] No CSRF token found')
        return ''
      }
      async calculateSHA1(file) {
        const text = `${file.name}-${file.size}-${file.lastModified}`
        const data = new TextEncoder().encode(text)
        if (crypto.subtle)
          try {
            const hashBuffer = await crypto.subtle.digest('SHA-1', data)
            return Array.from(new Uint8Array(hashBuffer))
              .map(b => b.toString(16).padStart(2, '0'))
              .join('')
          } catch (e) {
            console.warn('[Image Uploader] Could not calculate SHA1, using fallback')
          }
        let hash = 0
        for (let i = 0; i < text.length; i++) {
          const char = text.charCodeAt(i)
          hash = (hash << 5) - hash + char
          hash = hash & hash
        }
        return Math.abs(hash).toString(16).padStart(40, '0')
      }
    }
    const uploader = new ImageUploader()
    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(url)
      name = name.trim()
      if (name.length === 0) name = '表情'
      return {
        name,
        url
      }
    }
    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(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)
          try {
            uploader.showProgressDialog()
          } catch (e$1) {
            console.warn('[Userscript] uploader.showProgressDialog failed:', e$1)
          }
          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(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)
      })
    }
    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'
      ]
      switch (platform) {
        case 'mobile':
          return [
            ...baseSelectors,
            '.mobile-composer-toolbar',
            '.chat-composer-mobile',
            '[data-mobile-toolbar]',
            '.discourse-mobile .d-editor-button-bar'
          ]
        case 'pc':
          return [
            ...baseSelectors,
            '.desktop-composer-toolbar',
            '.chat-composer-desktop',
            '[data-desktop-toolbar]',
            '.discourse-desktop .d-editor-button-bar'
          ]
        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 injectGlobalThemeStyles() {
      if (themeStylesInjected || typeof document === 'undefined') return
      themeStylesInjected = true
      const style = document.createElement('style')
      style.id = 'emoji-extension-theme-globals'
      style.textContent = `
    /* 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);
      }
    }
  `
      document.head.appendChild(style)
    }
    let themeStylesInjected
    const init_themeSupport = __esmMin(() => {
      themeStylesInjected = false
    })
    init_themeSupport()
    function injectEmojiPickerStyles() {
      if (typeof document === 'undefined') return
      if (document.getElementById('emoji-picker-styles')) return
      injectGlobalThemeStyles()
      const css = `
.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;
}
`
      const style = document.createElement('style')
      style.id = 'emoji-picker-styles'
      style.textContent = css
      document.head.appendChild(style)
    }
    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
      document.head.appendChild(
        createEl('style', {
          attrs: { 'data-emoji-manager-styles': '1' },
          text: `
    /* Modal backdrop */
    .emoji-manager-wrapper {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background: rgba(0,0,0,0.8);
      z-index: 999999;
      display: flex;
      align-items: center;
      justify-content: center;
    }

    /* Main modal panel */
    .emoji-manager-panel {
      background: white;
      border-radius: 8px;
      max-width: 90vw;
      max-height: 90vh;
      width: 1000px;
      height: 600px;
      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: #f8f9fa;
      border-right: 1px solid #e9ecef;
      display: flex;
      flex-direction: column;
      overflow: hidden;
    }

    .emoji-manager-left-header {
      display: flex;
      align-items: center;
      padding: 16px;
      border-bottom: 1px solid #e9ecef;
      background: white;
    }

    .emoji-manager-addgroup-row {
      display: flex;
      gap: 8px;
      padding: 12px;
      border-bottom: 1px solid #e9ecef;
    }

    .emoji-manager-groups-list {
      flex: 1;
      overflow-y: auto;
      padding: 8px;
    }

    .emoji-manager-groups-list > div {
      padding: 12px;
      border-radius: 6px;
      cursor: pointer;
      margin-bottom: 4px;
      transition: background-color 0.2s;
    }

    .emoji-manager-groups-list > div:hover {
      background: #e9ecef;
    }

    .emoji-manager-groups-list > div:focus {
      outline: none;
      box-shadow: inset 0 0 0 2px #007bff;
    }

    /* Right panel - emoji display and editing */
    .emoji-manager-right {
      background: white;
      display: flex;
      flex-direction: column;
      overflow: hidden;
    }

    .emoji-manager-right-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 16px;
      border-bottom: 1px solid #e9ecef;
    }

    .emoji-manager-right-main {
      flex: 1;
      overflow-y: auto;
      padding: 16px;
    }

    .emoji-manager-emojis {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
      gap: 12px;
      margin-bottom: 16px;
    }

    .emoji-manager-card {
      display: flex;
      flex-direction: column;
      gap: 8px;
      align-items: center;
      padding: 12px;
      background: #f8f9fa;
      border: 1px solid #e9ecef;
      border-radius: 8px;
      transition: transform 0.2s, box-shadow 0.2s;
    }

    .emoji-manager-card:hover {
      transform: translateY(-2px);
      box-shadow: 0 4px 12px rgba(0,0,0,0.1);
    }

    .emoji-manager-card-img {
      width: 80px;
      height: 80px;
      /* Prevent extremely large images from breaking the layout by limiting their
         rendered size relative to the card. Use both absolute and percentage-based
         constraints so user-provided pixel sizes (from edit form) still work but
         will not overflow the card or modal. */
      max-width: 90%;
      max-height: 60vh; /* allow tall images but cap at viewport height */
      object-fit: contain;
      border-radius: 6px;
      background: white;
    }

    .emoji-manager-card-name {
      font-size: 12px;
      color: #495057;
      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: #f8f9fa;
      border-top: 1px solid #e9ecef;
      display: flex;
      gap: 8px;
      align-items: center;
    }

    /* Footer */
    .emoji-manager-footer {
      grid-column: 1 / -1;
      display: flex;
      gap: 8px;
      justify-content: space-between;
      padding: 16px;
      background: #f8f9fa;
      border-top: 1px solid #e9ecef;
    }

    /* Editor panel - popup modal */
    .emoji-manager-editor-panel {
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      background: white;
      border: 1px solid #e9ecef;
      border-radius: 8px;
      padding: 24px;
      box-shadow: 0 10px 40px rgba(0,0,0,0.3);
      z-index: 1000000;
      min-width: 400px;
    }

    .emoji-manager-editor-preview {
      width: 100px;
      height: 100px;
      /* editor preview should be bounded to avoid huge remote images
         while still allowing percentage-based scaling */
      max-width: 100%;
      max-height: 40vh;
      object-fit: contain;
      border-radius: 8px;
      background: #f8f9fa;
      margin: 0 auto 16px;
      display: block;
    }

    /* Hover preview (moved from inline styles) */
    .emoji-manager-hover-preview {
      position: fixed;
      pointer-events: none;
      z-index: 1000002;
      display: none;
      /* For hover previews allow a generous but bounded size relative to viewport
         to avoid covering entire UI or pushing content off-screen. */
      max-width: 30vw;
      max-height: 40vh;
      width: auto;
      height: auto;
      border: 1px solid rgba(0,0,0,0.1);
      background: #fff;
      padding: 4px;
      border-radius: 6px;
      box-shadow: 0 6px 18px rgba(0,0,0,0.12);
    }

    /* Form styling */
    .form-control {
      width: 100%;
      padding: 8px 12px;
      border: 1px solid #ced4da;
      border-radius: 4px;
      font-size: 14px;
      margin-bottom: 8px;
    }

    .btn {
      padding: 8px 16px;
      border: 1px solid transparent;
      border-radius: 4px;
      font-size: 14px;
      cursor: pointer;
      transition: all 0.2s;
    }

    .btn-primary {
      background-color: #007bff;
      color: white;
    }

    .btn-primary:hover {
      background-color: #0056b3;
    }

    .btn-sm {
      padding: 4px 8px;
      font-size: 12px;
    }
  `
        })
      )
    }
    let __managerStylesInjected
    const init_styles = __esmMin(() => {
      init_createEl()
      __managerStylesInjected = false
    })
    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'
      })
      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'
          })
          if (emo.width)
            img.style.width = typeof emo.width === 'number' ? emo.width + 'px' : emo.width
          if (emo.height)
            img.style.height = typeof emo.height === 'number' ? emo.height + 'px' : emo.height
          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'
          })
          edit.addEventListener('click', () => {
            showEditorFor(group.id, idx)
          })
          const del = createEl('button', {
            text: '删除',
            className: 'btn btn-sm'
          })
          del.addEventListener('click', () => {
            group.emojis.splice(idx, 1)
            renderGroups()
            renderSelectedGroup()
          })
          actions.appendChild(edit)
          actions.appendChild(del)
          card.appendChild(img)
          card.appendChild(name)
          card.appendChild(actions)
          emojisContainer.appendChild(card)
          bindHoverPreview(img, emo)
        })
      }
      let hoverPreviewEl = null
      function ensureHoverPreview$1() {
        if (hoverPreviewEl && document.body.contains(hoverPreviewEl)) return hoverPreviewEl
        hoverPreviewEl = createEl('img', { className: 'emoji-manager-hover-preview' })
        document.body.appendChild(hoverPreviewEl)
        return hoverPreviewEl
      }
      function bindHoverPreview(targetImg, emo) {
        const preview = ensureHoverPreview$1()
        function onEnter(e) {
          preview.src = emo.url
          if (emo.width)
            preview.style.width = typeof emo.width === 'number' ? emo.width + 'px' : emo.width
          else preview.style.width = ''
          if (emo.height)
            preview.style.height = typeof emo.height === 'number' ? emo.height + 'px' : emo.height
          else preview.style.height = ''
          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() {
          if (preview) 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 = (emojiUrlInput.value || '').trim()
        const name = (emojiNameInput.value || '').trim()
        const widthVal = (emojiWidthInput.value || '').trim()
        const heightVal = (emojiHeightInput.value || '').trim()
        const width = widthVal ? parseInt(widthVal, 10) : NaN
        const height = heightVal ? parseInt(heightVal, 10) : NaN
        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 (!isNaN(width) && width > 0) newEmo.width = width
        if (!isNaN(height) && height > 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', () => {
        const data = exportUserscriptData()
        navigator.clipboard
          .writeText(data)
          .then(() => alert('已复制到剪贴板'))
          .catch(() => {
            const ta = createEl('textarea', { value: data })
            document.body.appendChild(ta)
            ta.select()
          })
      })
      importBtn.addEventListener('click', () => {
        const ta = createEl('textarea', {
          placeholder: '粘贴 JSON 后点击确认',
          style: 'width:100%;height:200px;margin-top:8px;'
        })
        const ok = createEl('button', {
          text: '确认导入',
          style: 'padding:6px 8px;margin-top:6px;'
        })
        const container = createEl('div')
        container.appendChild(ta)
        container.appendChild(ok)
        const importModal = createEl('div', {
          style:
            'position:fixed;left:0;top:0;right:0;bottom:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:1000001;'
        })
        const box = createEl('div', {
          style: 'background:#fff;padding:12px;border-radius:6px;width:90%;max-width:700px;'
        })
        box.appendChild(container)
        importModal.appendChild(box)
        document.body.appendChild(importModal)
        ok.addEventListener('click', () => {
          try {
            const json = ta.value.trim()
            if (!json) return
            if (importUserscriptData(json)) {
              alert('导入成功,请保存以持久化')
              loadDataFromLocalStorage$1()
              renderGroups()
              renderSelectedGroup()
            } else alert('导入失败:格式错误')
          } catch (e) {
            alert('导入异常:' + e)
          }
          importModal.remove()
        })
      })
      saveBtn.addEventListener('click', () => {
        try {
          saveDataToLocalStorage({ emojiGroups: userscriptState.emojiGroups })
          alert('已保存')
        } catch (e) {
          alert('保存失败:' + e)
        }
      })
      syncBtn.addEventListener('click', () => {
        try {
          if (syncFromManager()) {
            alert('同步成功,已导入管理器数据')
            loadDataFromLocalStorage$1()
            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()
      }
    }
    function loadDataFromLocalStorage$1() {
      console.log('Data reload requested')
    }
    const init_manager = __esmMin(() => {
      init_styles()
      init_createEl()
      init_userscript_storage()
    })
    function showGroupEditorModal() {
      injectGlobalThemeStyles()
      const modal = createEl('div', {
        style: `
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.8);
      z-index: 999999;
      display: flex;
      align-items: center;
      justify-content: center;
    `
      })
      const content = createEl('div', {
        style: `
      background: var(--emoji-modal-bg);
      color: var(--emoji-modal-text);
      border-radius: 8px;
      padding: 24px;
      max-width: 700px;
      max-height: 80vh;
      overflow-y: auto;
      position: relative;
      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
    `
      })
      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>

    <div style="margin-bottom: 20px; padding: 16px; 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="font-size: 14px; color: var(--emoji-modal-text); opacity: 0.8; line-height: 1.4;">
        • 点击分组名称或图标进行编辑<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);
          border: 1px solid var(--emoji-modal-border);
          border-radius: 6px;
          transition: all 0.2s;
        ">
          <div class="drag-handle" style="
            cursor: grab;
            color: var(--emoji-modal-text);
            opacity: 0.5;
            font-size: 16px;
            user-select: none;
          " title="拖拽调整顺序">⋮⋮</div>

          <div class="group-icon-editor" style="
            min-width: 40px;
            height: 40px;
            display: flex;
            align-items: center;
            justify-content: center;
            background: var(--emoji-modal-bg);
            border: 1px dashed var(--emoji-modal-border);
            border-radius: 4px;
            cursor: pointer;
            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(--emoji-modal-bg);
                     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); opacity: 0.6;">
              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: flex-end;">
      <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>
  `
      modal.appendChild(content)
      document.body.appendChild(modal)
      const style = document.createElement('style')
      style.textContent = `
    .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;
    }
  `
      document.head.appendChild(style)
      content.querySelector('#closeModal')?.addEventListener('click', () => {
        modal.remove()
        style.remove()
      })
      modal.addEventListener('click', e => {
        if (e.target === modal) {
          modal.remove()
          style.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$1(`分组 "${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$1(`分组图标已更新为: ${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()
            style.remove()
            showTemporaryMessage$1('分组顺序已调整')
            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()
            style.remove()
            showTemporaryMessage$1('分组顺序已调整')
            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()
          style.remove()
          showTemporaryMessage$1(`新分组 "${groupName.trim()}" 已创建`)
          setTimeout(() => showGroupEditorModal(), 300)
        }
      })
      content.querySelector('#saveAllChanges')?.addEventListener('click', () => {
        saveDataToLocalStorage({ emojiGroups: userscriptState.emojiGroups })
        showTemporaryMessage$1('所有更改已保存到本地存储')
      })
    }
    function showTemporaryMessage$1(message) {
      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 = document.createElement('style')
        style.id = 'tempMessageStyles'
        style.textContent = `
      @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(() => {
        messageEl.remove()
      }, 2e3)
    }
    const init_groupEditor = __esmMin(() => {
      init_state()
      init_userscript_storage()
      init_createEl()
      init_themeSupport()
    })
    function showPopularEmojisModal() {
      injectGlobalThemeStyles()
      const modal = createEl('div', {
        style: `
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.8);
      z-index: 999999;
      display: flex;
      align-items: center;
      justify-content: center;
    `
      })
      const content = createEl('div', {
        style: `
      background: var(--emoji-modal-bg);
      color: var(--emoji-modal-text);
      border-radius: 8px;
      padding: 24px;
      max-width: 600px;
      max-height: 80vh;
      overflow-y: auto;
      position: relative;
      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
    `
      })
      const popularEmojis = getPopularEmojis(50)
      content.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; margin-bottom: 8px;">
        <span style="font-weight: 500; color: var(--emoji-modal-label);">表情按使用次数排序</span>
        <span style="font-size: 12px; color: var(--emoji-modal-text); opacity: 0.7;">点击表情直接使用</span>
      </div>
      <div style="font-size: 12px; color: var(--emoji-modal-text); opacity: 0.6;">
        总使用次数: ${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); opacity: 0.7;">还没有使用过表情<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)
      const style = document.createElement('style')
      style.textContent = `
    .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);
    }
  `
      document.head.appendChild(style)
      content.querySelector('#closeModal')?.addEventListener('click', () => {
        modal.remove()
        style.remove()
      })
      content.querySelector('#clearStats')?.addEventListener('click', () => {
        if (confirm('确定要清空所有表情使用统计吗?此操作不可撤销。')) {
          clearEmojiUsageStats()
          modal.remove()
          style.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()
            style.remove()
            showTemporaryMessage(`已使用表情: ${name}`)
          }
        })
      })
      modal.addEventListener('click', e => {
        if (e.target === modal) {
          modal.remove()
          style.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 }))
          }
        }
      }
    }
    function showTemporaryMessage(message) {
      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 = document.createElement('style')
        style.id = 'tempMessageStyles'
        style.textContent = `
      @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(() => {
        messageEl.remove()
      }, 2e3)
    }
    const init_popularEmojis = __esmMin(() => {
      init_state()
      init_userscript_storage()
      init_createEl()
      init_themeSupport()
    })
    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%;
    background: rgba(0, 0, 0, 0.8);
    z-index: 999999;
    display: flex;
    align-items: center;
    justify-content: center;
  `
      })
      const content = createEl('div', {
        style: `
      background: var(--emoji-modal-bg);
      color: var(--emoji-modal-text);
      border-radius: 8px;
    padding: 24px;
    max-width: 500px;
    max-height: 80vh;
    overflow-y: auto;
    position: relative;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
  `,
        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="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;
          border-radius: 4px;
          cursor: pointer;
          font-size: 12px;
        ">编辑分组</button>
        <button id="openPopularEmojis" style="
          padding: 6px 12px;
          background: var(--emoji-modal-primary-bg);
          color: white;
          border: none;
          border-radius: 4px;
          cursor: pointer;
          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 = {
            imageScale: 30,
            gridColumns: 4,
            outputFormat: 'markdown',
            forceMobileMode: false,
            defaultGroup: 'nachoneko',
            showSearchBar: true,
            enableFloatingPreview: true
          }
          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 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()
      })
      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_state()
    init_userscript_storage()
    init_createEl()
    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 textarea = document.querySelector('textarea.d-editor-input')
      const proseMirror = document.querySelector('.ProseMirror.d-editor-input')
      if (!textarea && !proseMirror) {
        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)
          }
        }
      }
    }
    let _hoverPreviewEl = null
    function ensureHoverPreview() {
      if (_hoverPreviewEl && document.body.contains(_hoverPreviewEl)) return _hoverPreviewEl
      _hoverPreviewEl = 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:#fff;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:#333;margin-top:6px;text-align:center;'
      })
      _hoverPreviewEl.appendChild(img)
      _hoverPreviewEl.appendChild(label)
      document.body.appendChild(_hoverPreviewEl)
      return _hoverPreviewEl
    }
    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', () => {
        __vitePreload(
          async () => {
            const { showSettingsModal: showSettingsModal$1 } = await Promise.resolve().then(
              () => (init_settings(), settings_exports)
            )
            return { showSettingsModal: showSettingsModal$1 }
          },
          void 0
        ).then(({ showSettingsModal: showSettingsModal$1 }) => {
          showSettingsModal$1()
        })
      })
      sectionsNav.appendChild(settingsButton)
      const scrollableContent = createEl('div', { className: 'emoji-picker__scrollable-content' })
      const sections = createEl('div', {
        className: 'emoji-picker__sections',
        attrs: { role: 'button' }
      })
      let hoverPreviewEl = null
      function ensureHoverPreview$1() {
        if (hoverPreviewEl && document.body.contains(hoverPreviewEl)) return hoverPreviewEl
        hoverPreviewEl = 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:#fff;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:#333;margin-top:6px;text-align:center;'
        })
        hoverPreviewEl.appendChild(img)
        hoverPreviewEl.appendChild(label)
        document.body.appendChild(hoverPreviewEl)
        return hoverPreviewEl
      }
      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$1()
            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)
            const modalContainer = modal.closest('.modal-container')
            if (modalContainer) modalContainer.remove()
            else modal.remove()
          })
          img.addEventListener('keydown', e => {
            if (e.key === 'Enter' || e.key === ' ') {
              e.preventDefault()
              insertEmojiIntoEditor(emoji)
              const modalContainer = modal.closest('.modal-container')
              if (modalContainer) modalContainer.remove()
              else modal.remove()
            }
          })
          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', () => {
        __vitePreload(
          async () => {
            const { showSettingsModal: showSettingsModal$1 } = await Promise.resolve().then(
              () => (init_settings(), settings_exports)
            )
            return { showSettingsModal: showSettingsModal$1 }
          },
          void 0
        ).then(({ showSettingsModal: showSettingsModal$1 }) => {
          showSettingsModal$1()
        })
      })
      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_popularEmojis()
    function findAllToolbars() {
      const toolbars = []
      const selectors = getPlatformToolbarSelectors()
      for (const selector of selectors) {
        const elements = document.querySelectorAll(selector)
        toolbars.push(...Array.from(elements))
      }
      return toolbars
    }
    let 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()
      })
      try {
        if (isChatComposer) {
          const existingEmojiTrigger = toolbar.querySelector(
            '.emoji-picker-trigger:not(.emoji-extension-button)'
          )
          if (existingEmojiTrigger) {
            toolbar.insertBefore(button, existingEmojiTrigger)
            toolbar.insertBefore(popularButton, existingEmojiTrigger)
          } else {
            toolbar.appendChild(button)
            toolbar.appendChild(popularButton)
          }
        } else {
          toolbar.appendChild(button)
          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++
        }
      })
      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)
          }
        })
      }, 3e4)
    }
    init_createEl()
    init_themeSupport()
    let floatingButton = null
    let isButtonVisible = false
    const FLOATING_BUTTON_STYLES = `
.emoji-extension-floating-button {
  position: fixed !important;
  bottom: 20px !important;
  right: 20px !important;
  width: 56px !important;
  height: 56px !important;
  border-radius: 50% !important;
  background: linear-gradient(135deg, var(--emoji-button-gradient-start) 0%, var(--emoji-button-gradient-end) 100%) !important;
  border: none !important;
  box-shadow: 0 4px 12px var(--emoji-button-shadow) !important;
  cursor: pointer !important;
  z-index: 999999 !important;
  font-size: 24px !important;
  color: white !important;
  display: flex !important;
  align-items: center !important;
  justify-content: center !important;
  transition: all 0.3s ease !important;
  opacity: 0.9 !important;
  line-height: 1 !important;
}

.emoji-extension-floating-button:hover {
  transform: scale(1.1) !important;
  opacity: 1 !important;
  box-shadow: 0 6px 16px var(--emoji-button-hover-shadow) !important;
}

.emoji-extension-floating-button:active {
  transform: scale(0.95) !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 {
    bottom: 15px !important;
    right: 15px !important;
    width: 48px !important;
    height: 48px !important;
    font-size: 20px !important;
  }
}
`
    function injectStyles() {
      if (document.getElementById('emoji-extension-floating-button-styles')) return
      injectGlobalThemeStyles()
      const style = createEl('style', {
        attrs: { id: 'emoji-extension-floating-button-styles' },
        text: FLOATING_BUTTON_STYLES
      })
      document.head.appendChild(style)
    }
    function createFloatingButton() {
      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 {
          const result = attemptInjection()
          if (result.injectedCount > 0) {
            button.innerHTML = '✅'
            button.style.background = 'linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%)'
            setTimeout(() => {
              button.innerHTML = '🐈‍⬛'
              button.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
              button.style.transform = 'scale(1)'
            }, 1500)
            console.log(
              `[Emoji Extension Userscript] Manual injection successful: ${result.injectedCount} buttons injected into ${result.totalToolbars} toolbars`
            )
          } else {
            button.innerHTML = '❌'
            button.style.background = 'linear-gradient(135deg, #ff6b6b 0%, #ffa8a8 100%)'
            setTimeout(() => {
              button.innerHTML = '🐈‍⬛'
              button.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
              button.style.transform = 'scale(1)'
            }, 1500)
            console.log(
              '[Emoji Extension Userscript] Manual injection failed: No compatible toolbars found'
            )
          }
        } catch (error) {
          button.innerHTML = '⚠️'
          button.style.background = 'linear-gradient(135deg, #ff6b6b 0%, #ffa8a8 100%)'
          setTimeout(() => {
            button.innerHTML = '🐈‍⬛'
            button.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
            button.style.transform = 'scale(1)'
          }, 1500)
          console.error('[Emoji Extension Userscript] Manual injection error:', error)
        }
      })
      return button
    }
    function showFloatingButton() {
      if (floatingButton) return
      injectStyles()
      floatingButton = createFloatingButton()
      document.body.appendChild(floatingButton)
      isButtonVisible = true
      console.log('[Emoji Extension Userscript] Floating manual injection button shown')
    }
    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() {
      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 shouldInjectEmoji() {
      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) {
        const content = generatorMeta.getAttribute('content')?.toLowerCase() || ''
        if (
          content.includes('discourse') ||
          content.includes('flarum') ||
          content.includes('phpbb')
        ) {
          console.log('[Emoji Extension Userscript] Forum platform detected via generator meta')
          return true
        }
      }
      const hostname = window.location.hostname.toLowerCase()
      if (
        ['linux.do', 'meta.discourse.org', 'pixiv.net'].some(domain => hostname.includes(domain))
      ) {
        console.log('[Emoji Extension Userscript] Allowed domain detected:', hostname)
        return true
      }
      if (
        document.querySelectorAll(
          'textarea.d-editor-input, .ProseMirror.d-editor-input, .composer-input, .reply-area textarea'
        ).length > 0
      ) {
        console.log('[Emoji Extension Userscript] Discussion editor detected')
        return true
      }
      console.log('[Emoji Extension Userscript] No compatible platform detected')
      return false
    }
    async function initializeEmojiFeature(maxAttempts = 10, delay = 1e3) {
      console.log('[Emoji Extension Userscript] Initializing...')
      logPlatformInfo()
      await initializeUserscriptData()
      initOneClickAdd()
      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 (shouldInjectEmoji()) {
      console.log('[Emoji Extension Userscript] Initializing emoji feature')
      initializeEmojiFeature()
    } else console.log('[Emoji Extension Userscript] Skipping injection - incompatible platform')
  })()
})()

QingJ © 2025

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