// ==UserScript== // @name Discourse 表情管理器 (Emoji Manager) mgr lite // @namespace https://github.com/stevessr/bug-v3 // @version 1.2.2 // @description Discourse 论坛表情管理 - 设置、导入导出、分组编辑 (Emoji management for Discourse - Settings, import/export, group editor) // @author stevessr // @match https://linux.do/* // @match https://meta.discourse.org/* // @match https://*.discourse.org/* // @match http://localhost:5173/* // @exclude https://linux.do/a/* // @match https://idcflare.com/* // @grant none // @license MIT // @homepageURL https://github.com/stevessr/bug-v3 // @supportURL https://github.com/stevessr/bug-v3/issues // @run-at document-end // @downloadURL none // ==/UserScript== (function() { 'use strict'; (function() { var __defProp = Object.defineProperty; var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res); var __export = (all) => { let target = {}; for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); return target; }; async function fetchPackagedJSON(url) { try { if (typeof fetch === "undefined") return null; const res = await fetch(url || "/assets/defaultEmojiGroups.json", { cache: "no-cache" }); if (!res.ok) return null; return await res.json(); } catch (err) { return null; } } async function loadDefaultEmojiGroups(url) { const packaged = await fetchPackagedJSON(url); if (packaged && Array.isArray(packaged.groups)) return packaged.groups; return []; } var init_defaultEmojiGroups_loader = __esmMin((() => {})); function loadDataFromLocalStorage() { try { const groupsData = localStorage.getItem(STORAGE_KEY); let emojiGroups = []; if (groupsData) try { const parsed = JSON.parse(groupsData); if (Array.isArray(parsed) && parsed.length > 0) emojiGroups = parsed; } catch (e) { console.warn("[Userscript] Failed to parse stored emoji groups:", e); } if (emojiGroups.length === 0) emojiGroups = []; const settingsData = localStorage.getItem(SETTINGS_KEY); let settings = { ...DEFAULT_USER_SETTINGS }; if (settingsData) try { const parsed = JSON.parse(settingsData); if (parsed && typeof parsed === "object") settings = { ...settings, ...parsed }; } catch (e) { console.warn("[Userscript] Failed to parse stored settings:", e); } emojiGroups = emojiGroups.filter((g) => g.id !== "favorites"); console.log("[Userscript] Loaded data from localStorage:", { groupsCount: emojiGroups.length, emojisCount: emojiGroups.reduce((acc, g) => acc + (g.emojis?.length || 0), 0), settings }); return { emojiGroups, settings }; } catch (error) { console.error("[Userscript] Failed to load from localStorage:", error); return { emojiGroups: [], settings: { ...DEFAULT_USER_SETTINGS } }; } } async function loadDataFromLocalStorageAsync() { try { const local = loadDataFromLocalStorage(); if (local.emojiGroups && local.emojiGroups.length > 0) return local; const remoteUrl = localStorage.getItem("emoji_extension_remote_config_url"); if (remoteUrl && typeof remoteUrl === "string" && remoteUrl.trim().length > 0) try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5e3); const res = await fetch(remoteUrl, { signal: controller.signal }); clearTimeout(timeout); if (res && res.ok) { const json = await res.json(); const groups = Array.isArray(json.emojiGroups) ? json.emojiGroups : Array.isArray(json.groups) ? json.groups : null; const settings = json.settings && typeof json.settings === "object" ? json.settings : local.settings; if (groups && groups.length > 0) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(groups)); } catch (e) { console.warn("[Userscript] Failed to persist fetched remote groups to localStorage", e); } return { emojiGroups: groups.filter((g) => g.id !== "favorites"), settings }; } } } catch (err) { console.warn("[Userscript] Failed to fetch remote default config:", err); } try { const runtime = await loadDefaultEmojiGroups("https://video2gif-pages.pages.dev/assets/defaultEmojiGroups.json"); const source = runtime && runtime.length ? runtime : []; const filtered = JSON.parse(JSON.stringify(source)).filter((g) => g.id !== "favorites"); try { localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered)); } catch (e) {} return { emojiGroups: filtered, settings: local.settings }; } catch (e) { console.error("[Userscript] Failed to load default groups in async fallback:", e); return { emojiGroups: [], settings: local.settings }; } } catch (error) { console.error("[Userscript] loadDataFromLocalStorageAsync failed:", error); return { emojiGroups: [], settings: { ...DEFAULT_USER_SETTINGS } }; } } function saveDataToLocalStorage(data) { try { if (data.emojiGroups) localStorage.setItem(STORAGE_KEY, JSON.stringify(data.emojiGroups)); if (data.settings) localStorage.setItem(SETTINGS_KEY, JSON.stringify(data.settings)); } catch (error) { console.error("[Userscript] Failed to save to localStorage:", error); } } function 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); } } var STORAGE_KEY, SETTINGS_KEY, USAGE_STATS_KEY, DEFAULT_USER_SETTINGS; var init_userscript_storage = __esmMin((() => { init_defaultEmojiGroups_loader(); STORAGE_KEY = "emoji_extension_userscript_data"; SETTINGS_KEY = "emoji_extension_userscript_settings"; USAGE_STATS_KEY = "emoji_extension_userscript_usage_stats"; DEFAULT_USER_SETTINGS = { imageScale: 30, gridColumns: 4, outputFormat: "markdown", forceMobileMode: false, defaultGroup: "nachoneko", showSearchBar: true, enableFloatingPreview: true, enableCalloutSuggestions: true, enableBatchParseImages: true }; })); var userscriptState; var init_state = __esmMin((() => { init_userscript_storage(); userscriptState = { emojiGroups: [], settings: { ...DEFAULT_USER_SETTINGS }, emojiUsageStats: {} }; })); init_state(); 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() { return detectRuntimePlatform(); } 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 logPlatformInfo() { const buildPlatform = "original"; 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); } const __vitePreload = function preload(baseModule, deps, importerUrl) { let 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 ensureStyleInjected(id, css) { const style = document.createElement("style"); style.id = id; style.textContent = css; document.documentElement.appendChild(style); } var init_injectStyles = __esmMin((() => {})); function injectManagerStyles() { if (__managerStylesInjected) return; __managerStylesInjected = true; ensureStyleInjected("emoji-manager-styles", ` /* Modal backdrop */ .emoji-manager-wrapper { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 999999; display: flex; align-items: center; justify-content: center; } /* Main modal panel */ .emoji-manager-panel { border-radius: 8px; width: 90%; height: 95%; display: grid; grid-template-columns: 300px 1fr; grid-template-rows: 1fr auto; overflow: hidden; box-shadow: 0 10px 40px rgba(0,0,0,0.3); } /* Left panel - groups list */ .emoji-manager-left { background: var(--primary-very-low) border-right: 1px solid #e9ecef; display: flex; flex-direction: column; overflow: hidden; } .emoji-manager-left-header { display: flex; align-items: center; padding: 16px; background: var(--primary-low); } .emoji-manager-addgroup-row { display: flex; padding: 12px; } .emoji-manager-groups-list { background: var(--primary-very-low); flex: 1; overflow-y: auto; padding: 8px; } .emoji-manager-groups-list > div { margin-bottom: 4px; transition: background-color 0.2s; } .emoji-manager-groups-list > div:hover { background: var(--d-selected); } .emoji-manager-groups-list > div:focus { outline: none; box-shadow: inset 0 0 0 2px #007bff; background: var(--d-selected); } /* Right panel - emoji display and editing */ .emoji-manager-right { background: var(--primary-low); display: flex; flex-direction: column; overflow: hidden; } .emoji-manager-right-header { display: flex; align-items: center; justify-content: space-between; padding: 16px; } .emoji-manager-right-main { flex: 1; overflow-y: auto; } .emoji-manager-emojis { display: grid; grid-template-columns: repeat(auto-fill, minmax(25%, 1fr)); gap: 12px; } .emoji-manager-card { display: flex; flex-direction: column; align-items: center; padding: 12px; background: var(--primary-medium); } .emoji-manager-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); } .emoji-manager-card-img { max-width: 90%; max-height: 100%; /* allow tall images but cap at viewport height */ object-fit: contain; border-radius: 6px; background: white; } .emoji-manager-card-name { font-size: 12px; color: var(--primary); text-align: center; width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-weight: 500; } .emoji-manager-card-actions { display: flex; gap: 6px; } /* Add emoji form */ .emoji-manager-add-emoji-form { padding: 16px; background: var(--primary-very-low) border-top: 1px solid #e9ecef; display: flex; gap: 8px; align-items: center; } /* Footer */ .emoji-manager-footer { grid-column: 1 / -1; display: flex; justify-content: space-between; padding: 16px; background: var(--primary-very-low); } /* Editor panel - popup modal */ .emoji-manager-editor-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: var( --primary-medium ); padding: 2%; z-index: 1000000; } .emoji-manager-editor-preview { max-width: 100%; max-height: 40vh; } /* Hover preview (moved from inline styles) */ .emoji-manager-hover-preview { position: fixed; pointer-events: none; z-index: 1000002; display: none; max-width: 60%; max-height: 60%; border: 1px solid rgba(0,0,0,0.1); object-fit: contain; background: var(--primary); padding: 4px; border-radius: 6px; box-shadow: 0 6px 18px rgba(0,0,0,0.12); } /* Form styling */ .form-control { width: 100%; display: flex; } .btn { padding: 8px 16px; border: 1px solid transparent; border-radius: 4px; font-size: 14px; cursor: pointer; transition: all 0.2s; } .btn-sm { padding: 4px 8px; font-size: 12px; } `); } var __managerStylesInjected; var init_styles = __esmMin((() => { init_injectStyles(); __managerStylesInjected = false; })); function createEl(tag, opts) { const el = document.createElement(tag); if (opts) { if (opts.width) el.style.width = opts.width; if (opts.height) el.style.height = opts.height; if (opts.className) el.className = opts.className; if (opts.text) el.textContent = opts.text; if (opts.placeholder && "placeholder" in el) el.placeholder = opts.placeholder; if (opts.type && "type" in el) el.type = opts.type; if (opts.value !== void 0 && "value" in el) el.value = opts.value; if (opts.style) el.style.cssText = opts.style; if (opts.src && "src" in el) el.src = opts.src; if (opts.attrs) for (const k in opts.attrs) el.setAttribute(k, opts.attrs[k]); if (opts.dataset) for (const k in opts.dataset) el.dataset[k] = opts.dataset[k]; if (opts.innerHTML) el.innerHTML = opts.innerHTML; if (opts.title) el.title = opts.title; if (opts.alt && "alt" in el) el.alt = opts.alt; if (opts.id) el.id = opts.id; if (opts.on) for (const [evt, handler] of Object.entries(opts.on)) el.addEventListener(evt, handler); } return el; } var init_createEl = __esmMin((() => {})); function ensureHoverPreview() { if (_sharedPreview && document.body.contains(_sharedPreview)) return _sharedPreview; _sharedPreview = createEl("div", { className: "emoji-picker-hover-preview", style: "position:fixed;pointer-events:none;display:none;z-index:1000002;max-width:300px;max-height:300px;overflow:hidden;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,0.25);background:transparent;padding:6px;" }); const img = createEl("img", { className: "emoji-picker-hover-img", style: "display:block;max-width:100%;max-height:220px;object-fit:contain;" }); const label = createEl("div", { className: "emoji-picker-hover-label", style: "font-size:12px;color:var(--primary);margin-top:6px;text-align:center;" }); _sharedPreview.appendChild(img); _sharedPreview.appendChild(label); document.body.appendChild(_sharedPreview); return _sharedPreview; } var _sharedPreview; var init_hoverPreview = __esmMin((() => { init_createEl(); _sharedPreview = null; })); function injectGlobalThemeStyles() { if (themeStylesInjected || typeof document === "undefined") return; themeStylesInjected = true; document.head.appendChild(createEl("style", { id: "emoji-extension-theme-globals", text: ` /* Global CSS variables for emoji extension theme support */ :root { /* Light theme (default) */ --emoji-modal-bg: #ffffff; --emoji-modal-text: #333333; --emoji-modal-border: #dddddd; --emoji-modal-input-bg: #ffffff; --emoji-modal-label: #555555; --emoji-modal-button-bg: #f5f5f5; --emoji-modal-primary-bg: #1890ff; --emoji-preview-bg: #ffffff; --emoji-preview-text: #222222; --emoji-preview-border: rgba(0,0,0,0.08); --emoji-button-gradient-start: #667eea; --emoji-button-gradient-end: #764ba2; --emoji-button-shadow: rgba(0, 0, 0, 0.15); --emoji-button-hover-shadow: rgba(0, 0, 0, 0.2); } /* Dark theme */ @media (prefers-color-scheme: dark) { :root { --emoji-modal-bg: #2d2d2d; --emoji-modal-text: #e6e6e6; --emoji-modal-border: #444444; --emoji-modal-input-bg: #3a3a3a; --emoji-modal-label: #cccccc; --emoji-modal-button-bg: #444444; --emoji-modal-primary-bg: #1677ff; --emoji-preview-bg: rgba(32,33,36,0.94); --emoji-preview-text: #e6e6e6; --emoji-preview-border: rgba(255,255,255,0.12); --emoji-button-gradient-start: #4a5568; --emoji-button-gradient-end: #2d3748; --emoji-button-shadow: rgba(0, 0, 0, 0.3); --emoji-button-hover-shadow: rgba(0, 0, 0, 0.4); } } ` })); } var themeStylesInjected; var init_themeSupport = __esmMin((() => { init_createEl(); themeStylesInjected = false; })); function showTemporaryMessage(message, duration = 2e3) { const messageEl = createEl("div", { style: ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: var(--emoji-modal-primary-bg); color: white; padding: 12px 24px; border-radius: 6px; z-index: 9999999; font-size: 14px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); animation: fadeInOut 2s ease-in-out; `, text: message }); if (!document.querySelector("#tempMessageStyles")) { const style = createEl("style", { id: "tempMessageStyles", text: ` @keyframes fadeInOut { 0%, 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.9); } 20%, 80% { opacity: 1; transform: translate(-50%, -50%) scale(1); } } ` }); document.head.appendChild(style); } document.body.appendChild(messageEl); setTimeout(() => { try { messageEl.remove(); } catch {} }, duration); } var init_tempMessage = __esmMin((() => { init_createEl(); })); function createModalElement(options) { const modal = createEl("div", { style: ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 999999; display: flex; align-items: center; justify-content: center; `, className: options.className }); const content = createEl("div", { style: ` background: var(--secondary); color: var(--emoji-modal-text); border: 1px solid var(--emoji-modal-border); border-radius: 8px; padding: 24px; max-width: 90%; max-height: 90%; overflow-y: auto; position: relative; ` }); if (options.title) { const titleElement = createEl("div", { style: ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; `, innerHTML: `

${options.title}

` }); content.appendChild(titleElement); const closeButton = content.querySelector("#closeModal"); if (closeButton && options.onClose) closeButton.addEventListener("click", options.onClose); } if (options.content) { const contentDiv = createEl("div", { innerHTML: options.content }); content.appendChild(contentDiv); } modal.appendChild(content); return modal; } var init_editorUtils = __esmMin((() => { init_createEl(); })); var importExport_exports = /* @__PURE__ */ __export({ showImportExportModal: () => showImportExportModal }); function showImportExportModal(currentGroupId) { injectGlobalThemeStyles(); const currentGroup = currentGroupId ? userscriptState.emojiGroups.find((g) => g.id === currentGroupId) : null; const modal = createModalElement({ title: "分组表情导入/导出", content: ` ${currentGroup ? `

当前分组信息

${currentGroup.icon?.startsWith("http") ? `图标` : `${currentGroup.icon || "📁"}`} ${currentGroup.name || currentGroup.id}
分组 ID: ${currentGroup.id} | 表情数量:${currentGroup.emojis?.length || 0}
` : ""}

导出分组表情

导入分组表情

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

设置

高级功能
` })); document.body.appendChild(modal); const content = modal.querySelector("div:last-child"); const scaleSlider = content.querySelector("#scaleSlider"); const scaleValue = content.querySelector("#scaleValue"); content.querySelector("#closeModal")?.addEventListener("click", () => { modal.remove(); }); scaleSlider?.addEventListener("input", () => { if (scaleValue) scaleValue.textContent = scaleSlider.value + "%"; }); content.querySelector("#resetSettings")?.addEventListener("click", async () => { if (confirm("确定要重置所有设置吗?")) { userscriptState.settings = { ...DEFAULT_USER_SETTINGS }; modal.remove(); } }); content.querySelector("#saveSettings")?.addEventListener("click", () => { userscriptState.settings.imageScale = parseInt(scaleSlider?.value || "30"); const outputFormat = content.querySelector("input[name=\"outputFormat\"]:checked"); if (outputFormat) userscriptState.settings.outputFormat = outputFormat.value; const showSearchBar = content.querySelector("#showSearchBar"); if (showSearchBar) userscriptState.settings.showSearchBar = showSearchBar.checked; const enableFloatingPreview = content.querySelector("#enableFloatingPreview"); if (enableFloatingPreview) userscriptState.settings.enableFloatingPreview = enableFloatingPreview.checked; const enableCalloutEl = content.querySelector("#enableCalloutSuggestions"); if (enableCalloutEl) userscriptState.settings.enableCalloutSuggestions = !!enableCalloutEl.checked; const enableBatchEl = content.querySelector("#enableBatchParseImages"); if (enableBatchEl) userscriptState.settings.enableBatchParseImages = !!enableBatchEl.checked; const forceMobileEl = content.querySelector("#forceMobileMode"); if (forceMobileEl) userscriptState.settings.forceMobileMode = !!forceMobileEl.checked; saveDataToLocalStorage({ settings: userscriptState.settings }); try { const remoteInput = content.querySelector("#remoteConfigUrl"); if (remoteInput && remoteInput.value.trim()) localStorage.setItem("emoji_extension_remote_config_url", remoteInput.value.trim()); } catch (e) {} alert("设置已保存"); modal.remove(); }); content.querySelector("#openGroupEditor")?.addEventListener("click", () => { modal.remove(); showGroupEditorModal(); }); content.querySelector("#openPopularEmojis")?.addEventListener("click", () => { modal.remove(); showPopularEmojisModal(); }); content.querySelector("#openImportExport")?.addEventListener("click", () => { modal.remove(); showImportExportModal(); }); } var init_settings = __esmMin((() => { init_state(); init_userscript_storage(); init_createEl(); init_themeSupport(); init_groupEditor(); init_popularEmojis(); init_importExport(); })); init_userscript_storage(); init_state(); async function initializeUserscriptData() { const data = await loadDataFromLocalStorageAsync().catch((err) => { console.warn("[Manager] loadDataFromLocalStorageAsync failed, falling back to sync loader", err); return loadDataFromLocalStorage(); }); userscriptState.emojiGroups = data.emojiGroups || []; userscriptState.settings = data.settings || userscriptState.settings; } function isDiscoursePage() { if (document.querySelectorAll("meta[name*=\"discourse\"], meta[content*=\"discourse\"], meta[property*=\"discourse\"]").length > 0) { console.log("[Emoji Manager] Discourse detected via meta tags"); return true; } const generatorMeta = document.querySelector("meta[name=\"generator\"]"); if (generatorMeta) { if ((generatorMeta.getAttribute("content")?.toLowerCase() || "").includes("discourse")) { console.log("[Emoji Manager] Discourse detected via generator meta"); return true; } } if (document.querySelectorAll("#main-outlet, .ember-application, textarea.d-editor-input, .ProseMirror.d-editor-input").length > 0) { console.log("[Emoji Manager] Discourse elements detected"); return true; } console.log("[Emoji Manager] Not a Discourse site"); return false; } async function initializeEmojiManager() { console.log("[Emoji Manager] Initializing..."); logPlatformInfo(); await initializeUserscriptData(); const managerButton = document.createElement("button"); managerButton.id = "emoji-manager-floating-button"; managerButton.textContent = "⚙️ 表情管理"; managerButton.title = "Open Emoji Management Interface"; Object.assign(managerButton.style, { position: "fixed", right: "12px", bottom: "12px", zIndex: "2147483647", padding: "12px 16px", borderRadius: "8px", border: "none", background: "#1f2937", color: "#fff", fontSize: "14px", fontWeight: "500", boxShadow: "0 6px 18px rgba(0,0,0,0.3)", cursor: "pointer", transition: "transform 0.2s" }); managerButton.addEventListener("mouseenter", () => { managerButton.style.transform = "scale(1.05)"; }); managerButton.addEventListener("mouseleave", () => { managerButton.style.transform = "scale(1)"; }); managerButton.addEventListener("click", async () => { try { const { openManagementInterface: openManagementInterface$1 } = await __vitePreload(async () => { const { openManagementInterface: openManagementInterface$2 } = await Promise.resolve().then(() => (init_manager(), manager_exports)); return { openManagementInterface: openManagementInterface$2 }; }, void 0); openManagementInterface$1(); } catch (e) { console.error("[Emoji Manager] Failed to open management interface:", e); } }); const settingsButton = document.createElement("button"); settingsButton.id = "emoji-settings-floating-button"; settingsButton.textContent = "🔧 设置"; settingsButton.title = "Open Settings"; Object.assign(settingsButton.style, { position: "fixed", right: "12px", bottom: "70px", zIndex: "2147483647", padding: "10px 14px", borderRadius: "8px", border: "none", background: "#374151", color: "#fff", fontSize: "13px", fontWeight: "500", boxShadow: "0 4px 12px rgba(0,0,0,0.2)", cursor: "pointer", transition: "transform 0.2s" }); settingsButton.addEventListener("mouseenter", () => { settingsButton.style.transform = "scale(1.05)"; }); settingsButton.addEventListener("mouseleave", () => { settingsButton.style.transform = "scale(1)"; }); settingsButton.addEventListener("click", async () => { try { const { showSettingsModal: showSettingsModal$1 } = await __vitePreload(async () => { const { showSettingsModal: showSettingsModal$2 } = await Promise.resolve().then(() => (init_settings(), settings_exports)); return { showSettingsModal: showSettingsModal$2 }; }, void 0); showSettingsModal$1(); } catch (e) { console.error("[Emoji Manager] Failed to open settings:", e); } }); const importExportButton = document.createElement("button"); importExportButton.id = "emoji-importexport-floating-button"; importExportButton.textContent = "📦 导入/导出"; importExportButton.title = "Import/Export Data"; Object.assign(importExportButton.style, { position: "fixed", right: "12px", bottom: "128px", zIndex: "2147483647", padding: "10px 14px", borderRadius: "8px", border: "none", background: "#374151", color: "#fff", fontSize: "13px", fontWeight: "500", boxShadow: "0 4px 12px rgba(0,0,0,0.2)", cursor: "pointer", transition: "transform 0.2s" }); importExportButton.addEventListener("mouseenter", () => { importExportButton.style.transform = "scale(1.05)"; }); importExportButton.addEventListener("mouseleave", () => { importExportButton.style.transform = "scale(1)"; }); importExportButton.addEventListener("click", async () => { try { const { showImportExportModal: showImportExportModal$1 } = await __vitePreload(async () => { const { showImportExportModal: showImportExportModal$2 } = await Promise.resolve().then(() => (init_importExport(), importExport_exports)); return { showImportExportModal: showImportExportModal$2 }; }, void 0); showImportExportModal$1(); } catch (e) { console.error("[Emoji Manager] Failed to open import/export:", e); } }); if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", () => { document.body.appendChild(managerButton); document.body.appendChild(settingsButton); document.body.appendChild(importExportButton); }); else { document.body.appendChild(managerButton); document.body.appendChild(settingsButton); document.body.appendChild(importExportButton); } console.log("[Emoji Manager] Initialization complete"); } if (isDiscoursePage()) { console.log("[Emoji Manager] Discourse detected, initializing management interface"); initializeEmojiManager(); } else console.log("[Emoji Manager] Not a Discourse site, skipping initialization"); })(); })();