您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
为论坛网站添加表情选择器功能 (Add emoji picker functionality to forum websites)
// ==UserScript== // @name Linux do 表情扩展 (Emoji Extension) lite // @namespace https://github.com/stevessr/bug-v3 // @version 1.0.6 // @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() { 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() { 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 []; } 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 = { 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); } } var STORAGE_KEY, SETTINGS_KEY, USAGE_STATS_KEY; 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"; })); var userscriptState; var 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; } var 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$1(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(); } } var 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 = ``; insertIntoEditor$1(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"); } }; var 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); } var themeStylesInjected; var 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) { 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 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; } ` })); } var __managerStylesInjected; var init_styles = __esmMin((() => { init_createEl(); __managerStylesInjected = false; })); 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" }); 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"); } var 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); } var 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 = ``; 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 = ``; 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); } var init_popularEmojis = __esmMin((() => { init_state(); init_userscript_storage(); init_createEl(); init_themeSupport(); })); 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%; 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(); }); } var 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 = ` `; 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); } } } } var _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(); var QUICK_INSERTS = [ "info", "tip", "faq", "question", "note", "abstract", "todo", "success", "warning", "failure", "danger", "bug", "example", "quote" ]; var ICON_MAP = { info: "ℹ️", tip: "💡", faq: "❓", question: "🤔", note: "📝", abstract: "📋", todo: "☑️", success: "🎉", warning: "⚠️", failure: "❌", danger: "☠️", bug: "🐛", example: "🔎", quote: "💬" }; function insertIntoEditor(text) { const active = document.activeElement; const isTextarea = (el) => !!el && el.tagName === "TEXTAREA"; if (isTextarea(active)) { const textarea = active; const start = textarea.selectionStart ?? 0; const end = textarea.selectionEnd ?? start; const value = textarea.value; textarea.value = value.slice(0, start) + text + value.slice(end); const pos = start + text.length; if ("setSelectionRange" in textarea) try { textarea.setSelectionRange(pos, pos); } catch (e) {} textarea.dispatchEvent(new Event("input", { bubbles: true })); return; } if (active && active.isContentEditable) { const sel = window.getSelection(); if (!sel) return; const range = sel.getRangeAt(0); range.deleteContents(); const node = document.createTextNode(text); range.insertNode(node); range.setStartAfter(node); range.setEndAfter(node); sel.removeAllRanges(); sel.addRange(range); active.dispatchEvent(new Event("input", { bubbles: true })); return; } const fallback = document.querySelector("textarea"); if (fallback) { fallback.focus(); const start = fallback.selectionStart ?? fallback.value.length; const end = fallback.selectionEnd ?? start; const value = fallback.value; fallback.value = value.slice(0, start) + text + value.slice(end); const pos = start + text.length; if ("setSelectionRange" in fallback) try { fallback.setSelectionRange(pos, pos); } catch (e) {} fallback.dispatchEvent(new Event("input", { bubbles: true })); } } function createQuickInsertMenu() { const menu = document.createElement("div"); menu.className = "fk-d-menu toolbar-menu__options-content toolbar-popup-menu-options -animated -expanded"; const inner = document.createElement("div"); inner.className = "fk-d-menu__inner-content"; const list = document.createElement("ul"); list.className = "dropdown-menu"; QUICK_INSERTS.forEach((key) => { const li = document.createElement("li"); li.className = "dropdown-menu__item"; const btn = document.createElement("button"); btn.className = "btn btn-icon-text"; btn.type = "button"; const displayLabel = key.charAt(0).toUpperCase() + key.slice(1); btn.title = displayLabel; btn.addEventListener("click", () => { if (menu.parentElement) menu.parentElement.removeChild(menu); insertIntoEditor(`>[!${key}]`); }); const emojiSpan = document.createElement("span"); emojiSpan.textContent = ICON_MAP[key] || "✳️"; const labelWrap = document.createElement("span"); labelWrap.className = "d-button-label"; const labelText = document.createElement("span"); labelText.className = "d-button-label__text"; labelText.textContent = displayLabel; labelWrap.appendChild(labelText); btn.appendChild(emojiSpan); btn.appendChild(labelWrap); li.appendChild(btn); list.appendChild(li); }); inner.appendChild(list); menu.appendChild(inner); return menu; } function findAllToolbars() { const toolbars = []; const selectors = getPlatformToolbarSelectors(); for (const selector of selectors) { const elements = document.querySelectorAll(selector); toolbars.push(...Array.from(elements)); } return toolbars; } var currentPicker = null; function closeCurrentPicker() { if (currentPicker) { currentPicker.remove(); currentPicker = null; } } function injectEmojiButton(toolbar) { if (toolbar.querySelector(".emoji-extension-button")) return; const isChatComposer = toolbar.classList.contains("chat-composer__inner-container"); const button = createEl("button", { className: "btn no-text btn-icon toolbar__button nacho-emoji-picker-button emoji-extension-button", title: "表情包", type: "button", innerHTML: "🐈⬛" }); const popularButton = createEl("button", { className: "btn no-text btn-icon toolbar__button nacho-emoji-popular-button emoji-extension-button", title: "常用表情", type: "button", innerHTML: "⭐" }); if (isChatComposer) { button.classList.add("fk-d-menu__trigger", "emoji-picker-trigger", "chat-composer-button", "btn-transparent", "-emoji"); button.setAttribute("aria-expanded", "false"); button.setAttribute("data-identifier", "emoji-picker"); button.setAttribute("data-trigger", ""); popularButton.classList.add("fk-d-menu__trigger", "popular-emoji-trigger", "chat-composer-button", "btn-transparent", "-popular"); popularButton.setAttribute("aria-expanded", "false"); popularButton.setAttribute("data-identifier", "popular-emoji"); popularButton.setAttribute("data-trigger", ""); } button.addEventListener("click", async (e) => { e.stopPropagation(); if (currentPicker) { closeCurrentPicker(); return; } currentPicker = await createEmojiPicker(); if (!currentPicker) return; document.body.appendChild(currentPicker); const buttonRect = button.getBoundingClientRect(); if (currentPicker.classList.contains("modal") || currentPicker.className.includes("d-modal")) { currentPicker.style.position = "fixed"; currentPicker.style.top = "0"; currentPicker.style.left = "0"; currentPicker.style.right = "0"; currentPicker.style.bottom = "0"; currentPicker.style.zIndex = "999999"; } else { currentPicker.style.position = "fixed"; const margin = 8; const vpWidth = window.innerWidth; const vpHeight = window.innerHeight; currentPicker.style.top = buttonRect.bottom + margin + "px"; currentPicker.style.left = buttonRect.left + "px"; const pickerRect = currentPicker.getBoundingClientRect(); const spaceBelow = vpHeight - buttonRect.bottom; const neededHeight = pickerRect.height + margin; let top = buttonRect.bottom + margin; if (spaceBelow < neededHeight) top = Math.max(margin, buttonRect.top - pickerRect.height - margin); let left = buttonRect.left; if (left + pickerRect.width + margin > vpWidth) left = Math.max(margin, vpWidth - pickerRect.width - margin); if (left < margin) left = margin; currentPicker.style.top = top + "px"; currentPicker.style.left = left + "px"; } setTimeout(() => { const handleClick = (e$1) => { if (currentPicker && !currentPicker.contains(e$1.target) && e$1.target !== button) { closeCurrentPicker(); document.removeEventListener("click", handleClick); } }; document.addEventListener("click", handleClick); }, 100); }); popularButton.addEventListener("click", (e) => { e.stopPropagation(); closeCurrentPicker(); showPopularEmojisModal(); }); const quickInsertButton = createEl("button", { className: "btn no-text btn-icon toolbar__button quick-insert-button", title: "快捷输入", type: "button", innerHTML: "⎘" }); if (isChatComposer) { quickInsertButton.classList.add("fk-d-menu__trigger", "chat-composer-button", "btn-transparent"); quickInsertButton.setAttribute("aria-expanded", "false"); quickInsertButton.setAttribute("data-trigger", ""); } quickInsertButton.addEventListener("click", (e) => { e.stopPropagation(); const menu = createQuickInsertMenu(); (document.querySelector("#d-menu-portals") || document.body).appendChild(menu); const rect = quickInsertButton.getBoundingClientRect(); menu.style.position = "fixed"; menu.style.zIndex = "10000"; menu.style.top = `${rect.bottom + 5}px`; menu.style.left = `${Math.max(8, Math.min(rect.left + rect.width / 2 - 150, window.innerWidth - 300))}px`; const removeMenu = (ev) => { if (!menu.contains(ev.target)) { if (menu.parentElement) menu.parentElement.removeChild(menu); document.removeEventListener("click", removeMenu); } }; setTimeout(() => document.addEventListener("click", removeMenu), 100); }); try { if (isChatComposer) { const existingEmojiTrigger = toolbar.querySelector(".emoji-picker-trigger:not(.emoji-extension-button)"); if (existingEmojiTrigger) { toolbar.insertBefore(button, existingEmojiTrigger); toolbar.insertBefore(quickInsertButton, existingEmojiTrigger); toolbar.insertBefore(popularButton, existingEmojiTrigger); } else { toolbar.appendChild(button); toolbar.appendChild(quickInsertButton); toolbar.appendChild(popularButton); } } else { toolbar.appendChild(button); toolbar.appendChild(quickInsertButton); toolbar.appendChild(popularButton); } } catch (error) { console.error("[Emoji Extension Userscript] Failed to inject button:", error); } } function attemptInjection() { const toolbars = findAllToolbars(); let injectedCount = 0; toolbars.forEach((toolbar) => { if (!toolbar.querySelector(".emoji-extension-button")) { console.log("[Emoji Extension Userscript] Toolbar found, injecting button."); injectEmojiButton(toolbar); injectedCount++; } }); 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(); var floatingButton = null; var isButtonVisible = false; var 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"); })(); })();