您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
为论坛网站添加表情选择器功能 (Add emoji picker functionality to forum websites)
当前为
// ==UserScript== // @name Linux do 表情扩展 (Emoji Extension) // @namespace https://github.com/stevessr/bug-v3 // @version 1.0.1 // @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 }; 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 } }; } } 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 } }; } } 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; } } var STORAGE_KEY, SETTINGS_KEY; var init_userscript_storage = __esmMin((() => { init_defaultEmojiGroups_loader(); STORAGE_KEY = "emoji_extension_userscript_data"; SETTINGS_KEY = "emoji_extension_userscript_settings"; })); var userscriptState; var init_state = __esmMin((() => { userscriptState = { emojiGroups: [], settings: { imageScale: 30, gridColumns: 4, outputFormat: "markdown", forceMobileMode: false, defaultGroup: "nachoneko", showSearchBar: true } }; })); 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 insertIntoEditor(text) { const textArea = document.querySelector("textarea.d-editor-input"); const richEle = document.querySelector(".ProseMirror.d-editor-input"); if (!textArea && !richEle) { console.error("找不到输入框"); return; } if (textArea) { const start = textArea.selectionStart; const end = textArea.selectionEnd; const value = textArea.value; textArea.value = value.substring(0, start) + text + value.substring(end); textArea.setSelectionRange(start + text.length, start + text.length); textArea.focus(); const event = new Event("input", { bubbles: true }); textArea.dispatchEvent(event); } else if (richEle) { const selection = window.getSelection(); if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); const textNode = document.createTextNode(text); range.insertNode(textNode); range.setStartAfter(textNode); range.setEndAfter(textNode); selection.removeAllRanges(); selection.addRange(range); } richEle.focus(); } } 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(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 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; } function injectEmojiPickerStyles() { if (typeof document === "undefined") return; if (document.getElementById("emoji-picker-styles")) return; 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:#ffffff; padding:8px; transition:opacity .12s ease, transform .12s ease; } .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:#222; margin-top:8px; text-align:center; word-break:break-word; } /* Dark theme adaptation */ @media (prefers-color-scheme: dark) { .emoji-picker-hover-preview{ background: rgba(32,33,36,0.94); box-shadow: 0 6px 20px rgba(0,0,0,0.6); border: 1px solid rgba(255,255,255,0.04); } .emoji-picker-hover-preview .emoji-picker-hover-label{ color: #e6e6e6; } } `; const style = document.createElement("style"); style.id = "emoji-picker-styles"; style.textContent = css; document.head.appendChild(style); } 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; 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; 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; max-width: 300px; max-height: 300px; 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 = __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(); })); var settings_exports = __export({ showSettingsModal: () => showSettingsModal }); function showSettingsModal() { 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: white; border-radius: 8px; padding: 24px; max-width: 500px; max-height: 80vh; overflow-y: auto; position: relative; `, innerHTML: ` <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;"> <h2 style="margin: 0; color: #333;">设置</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: #555; 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: #555; font-weight: 500;">输出格式:</label> <div style="display: flex; gap: 16px;"> <label style="display: flex; align-items: center; color: #666;"> <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: #666;"> <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: #555; 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: #555; font-weight: 500;"> <input type="checkbox" id="forceMobileMode" ${userscriptState.settings.forceMobileMode ? "checked" : ""} style="margin-right: 8px;"> 强制移动模式 (在不兼容检测时也注入移动版布局) </label> </div> <div style="display: flex; gap: 8px; justify-content: flex-end;"> <button id="resetSettings" style="padding: 8px 16px; background: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; cursor: pointer;">重置</button> <button id="saveSettings" style="padding: 8px 16px; background: #1890ff; 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 }; 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 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(); }); modal.addEventListener("click", (e) => { if (e.target === modal) modal.remove(); }); } var init_settings = __esmMin((() => { init_state(); init_userscript_storage(); init_createEl(); })); init_state(); init_createEl(); function isMobileView() { try { return !!(userscriptState && userscriptState.settings && userscriptState.settings.forceMobileMode); } catch (e) { return false; } } function insertEmojiIntoEditor(emoji) { console.log("[Emoji Extension Userscript] Inserting emoji:", emoji); 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) { const preview = ensureHoverPreview$1(); const previewImg = preview.querySelector("img"); const previewLabel = preview.querySelector(".emoji-picker-hover-label"); function onEnter(e) { previewImg.src = emo.url; previewLabel.textContent = emo.name || ""; preview.style.display = "block"; 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() { 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) { const preview = ensureHoverPreview(); const previewImg = preview.querySelector("img"); const previewLabel = preview.querySelector(".emoji-picker-hover-label"); function onEnter(e) { previewImg.src = emo.url; previewLabel.textContent = emo.name || ""; preview.style.display = "block"; 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() { 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(); var toolbarSelectors = [".d-editor-button-bar[role=\"toolbar\"]", ".chat-composer__inner-container"]; function findAllToolbars() { const toolbars = []; for (const selector of toolbarSelectors) { 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: "🐈⬛" }); 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", ""); } 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); }); try { if (isChatComposer) { const existingEmojiTrigger = toolbar.querySelector(".emoji-picker-trigger:not(.emoji-extension-button)"); if (existingEmojiTrigger) toolbar.insertBefore(button, existingEmojiTrigger); else toolbar.appendChild(button); } else toolbar.appendChild(button); } 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_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..."); 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."); } if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", attemptToolbarInjection); else attemptToolbarInjection(); startPeriodicInjection(); } if (shouldInjectEmoji()) { console.log("[Emoji Extension Userscript] Initializing emoji feature"); initializeEmojiFeature(); } else console.log("[Emoji Extension Userscript] Skipping injection - incompatible platform"); })(); })();