// ==UserScript== // @name Discourse 表情选择器 (Emoji Picker) core lite // @namespace https://github.com/stevessr/bug-v3 // @version 1.2.4 // @description Discourse 论坛表情选择器 - 核心功能 (Emoji picker for Discourse - Core features only) // @author stevessr // @match https://linux.do/* // @match https://meta.discourse.org/* // @match https://*.discourse.org/* // @match http://localhost:5173/* // @exclude https://linux.do/a/* // @match https://idcflare.com/* // @grant none // @license MIT // @homepageURL https://github.com/stevessr/bug-v3 // @supportURL https://github.com/stevessr/bug-v3/issues // @run-at document-end // @downloadURL https://update.greasyfork.icu/scripts/554979/Discourse%20%E8%A1%A8%E6%83%85%E9%80%89%E6%8B%A9%E5%99%A8%20%28Emoji%20Picker%29%20core%20lite.user.js // @updateURL https://update.greasyfork.icu/scripts/554979/Discourse%20%E8%A1%A8%E6%83%85%E9%80%89%E6%8B%A9%E5%99%A8%20%28Emoji%20Picker%29%20core%20lite.meta.js // ==/UserScript== (function() { 'use strict'; (function() { async function fetchPackagedJSON(url) { try { if (typeof fetch === "undefined") return null; const res = await fetch(url || "/assets/defaultEmojiGroups.json", { cache: "no-cache", credentials: "omit" }); if (!res.ok) return null; return await res.json(); } catch (err) { return null; } } async function loadAndFilterDefaultEmojiGroups(url, hostname) { const packaged = await fetchPackagedJSON(url); if (!packaged || !Array.isArray(packaged.groups)) return []; if (!hostname) return packaged.groups; return packaged.groups.map((group) => { const filteredEmojis = group.emojis.filter((emoji) => { try { const url$1 = emoji.url; if (!url$1) return false; const emojiHostname = new URL(url$1).hostname; return emojiHostname === hostname || emojiHostname.endsWith("." + hostname); } catch (e) { return true; } }); return { ...group, emojis: filteredEmojis }; }).filter((group) => group.emojis.length > 0); } var STORAGE_KEY = "emoji_extension_userscript_data"; var SETTINGS_KEY = "emoji_extension_userscript_settings"; var USAGE_STATS_KEY = "emoji_extension_userscript_usage_stats"; const DEFAULT_USER_SETTINGS = { imageScale: 30, gridColumns: 4, outputFormat: "markdown", forceMobileMode: false, defaultGroup: "nachoneko", showSearchBar: true, enableFloatingPreview: true, enableCalloutSuggestions: true, enableBatchParseImages: true }; function loadDataFromLocalStorage() { try { const groupsData = localStorage.getItem(STORAGE_KEY); let emojiGroups = []; if (groupsData) try { const parsed = JSON.parse(groupsData); if (Array.isArray(parsed) && parsed.length > 0) emojiGroups = parsed; } catch (e) { console.warn("[Userscript] Failed to parse stored emoji groups:", e); } if (emojiGroups.length === 0) emojiGroups = []; const settingsData = localStorage.getItem(SETTINGS_KEY); let settings = { ...DEFAULT_USER_SETTINGS }; if (settingsData) try { const parsed = JSON.parse(settingsData); if (parsed && typeof parsed === "object") settings = { ...settings, ...parsed }; } catch (e) { console.warn("[Userscript] Failed to parse stored settings:", e); } emojiGroups = emojiGroups.filter((g) => g.id !== "favorites"); console.log("[Userscript] Loaded data from localStorage:", { groupsCount: emojiGroups.length, emojisCount: emojiGroups.reduce((acc, g) => acc + (g.emojis?.length || 0), 0), settings }); return { emojiGroups, settings }; } catch (error) { console.error("[Userscript] Failed to load from localStorage:", error); return { emojiGroups: [], settings: { ...DEFAULT_USER_SETTINGS } }; } } async function loadDataFromLocalStorageAsync(hostname) { try { const local = loadDataFromLocalStorage(); if (local.emojiGroups && local.emojiGroups.length > 0) return local; const remoteUrl = localStorage.getItem("emoji_extension_remote_config_url"); const configUrl = remoteUrl && typeof remoteUrl === "string" && remoteUrl.trim().length > 0 ? remoteUrl : "https://video2gif-pages.pages.dev/assets/defaultEmojiGroups.json"; try { const groups = await loadAndFilterDefaultEmojiGroups(configUrl, hostname); if (groups && groups.length > 0) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(groups)); } catch (e) { console.warn("[Userscript] Failed to persist fetched groups to localStorage", e); } return { emojiGroups: groups.filter((g) => g.id !== "favorites"), settings: local.settings }; } } catch (err) { console.warn(`[Userscript] Failed to fetch config from ${configUrl}:`, err); } return { emojiGroups: [], settings: local.settings }; } catch (error) { console.error("[Userscript] loadDataFromLocalStorageAsync failed:", error); return { emojiGroups: [], settings: { ...DEFAULT_USER_SETTINGS } }; } } function saveDataToLocalStorage(data) { try { if (data.emojiGroups) localStorage.setItem(STORAGE_KEY, JSON.stringify(data.emojiGroups)); if (data.settings) localStorage.setItem(SETTINGS_KEY, JSON.stringify(data.settings)); } catch (error) { console.error("[Userscript] Failed to save to localStorage:", error); } } function addEmojiToUserscript(emojiData) { try { const data = loadDataFromLocalStorage(); let userGroup = data.emojiGroups.find((g) => g.id === "user_added"); if (!userGroup) { userGroup = { id: "user_added", name: "用户添加", icon: "⭐", order: 999, emojis: [] }; data.emojiGroups.push(userGroup); } if (!userGroup.emojis.some((e) => e.url === emojiData.url || e.name === emojiData.name)) { userGroup.emojis.push({ packet: Date.now(), name: emojiData.name, url: emojiData.url }); saveDataToLocalStorage({ emojiGroups: data.emojiGroups }); console.log("[Userscript] Added emoji to user group:", emojiData.name); } else console.log("[Userscript] Emoji already exists:", emojiData.name); } catch (error) { console.error("[Userscript] Failed to add emoji:", error); } } function 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); } } const userscriptState = { emojiGroups: [], settings: { ...DEFAULT_USER_SETTINGS }, emojiUsageStats: {} }; function createEl(tag, opts) { const el = document.createElement(tag); if (opts) { if (opts.width) el.style.width = opts.width; if (opts.height) el.style.height = opts.height; if (opts.className) el.className = opts.className; if (opts.text) el.textContent = opts.text; if (opts.placeholder && "placeholder" in el) el.placeholder = opts.placeholder; if (opts.type && "type" in el) el.type = opts.type; if (opts.value !== void 0 && "value" in el) el.value = opts.value; if (opts.style) el.style.cssText = opts.style; if (opts.src && "src" in el) el.src = opts.src; if (opts.attrs) for (const k in opts.attrs) el.setAttribute(k, opts.attrs[k]); if (opts.dataset) for (const k in opts.dataset) el.dataset[k] = opts.dataset[k]; if (opts.innerHTML) el.innerHTML = opts.innerHTML; if (opts.title) el.title = opts.title; if (opts.alt && "alt" in el) el.alt = opts.alt; if (opts.id) el.id = opts.id; if (opts.on) for (const [evt, handler] of Object.entries(opts.on)) el.addEventListener(evt, handler); } return el; } function extractEmojiFromImage(img, titleElement) { const url = img.src; if (!url || !url.startsWith("http")) return null; let name = ""; const parts = (titleElement.textContent || "").split("·"); if (parts.length > 0) name = parts[0].trim(); if (!name || name.length < 2) name = img.alt || img.title || extractNameFromUrl$1(url); name = name.trim(); if (name.length === 0) name = "表情"; return { name, url }; } function extractEmojiDataFromLightboxWrapper(lightboxWrapper) { const results = []; const anchor = lightboxWrapper.querySelector("a.lightbox"); const img = lightboxWrapper.querySelector("img"); if (!anchor || !img) return results; const title = anchor.getAttribute("title") || ""; const originalUrl = anchor.getAttribute("href") || ""; const downloadUrl = anchor.getAttribute("data-download-href") || ""; const imgSrc = img.getAttribute("src") || ""; let name = title || img.getAttribute("alt") || ""; if (!name || name.length < 2) name = extractNameFromUrl$1(originalUrl || downloadUrl || imgSrc); name = name.replace(/\\.(webp|jpg|jpeg|png|gif)$/i, "").trim() || "表情"; const urlToUse = originalUrl || downloadUrl || imgSrc; if (urlToUse && urlToUse.startsWith("http")) results.push({ name, url: urlToUse }); return results; } function extractNameFromUrl$1(url) { try { const nameWithoutExt = (new URL(url).pathname.split("/").pop() || "").replace(/\.[^/.]+$/, ""); const decoded = decodeURIComponent(nameWithoutExt); if (/^[0-9a-f]{8,}$/i.test(decoded) || decoded.length < 2) return "表情"; return decoded || "表情"; } catch { return "表情"; } } function createAddButton$1(emojiData) { const link = createEl("a", { className: "image-source-link emoji-add-link", style: ` color: #ffffff; text-decoration: none; cursor: pointer; display: inline-flex; align-items: center; font-size: inherit; font-family: inherit; background: linear-gradient(135deg, #4f46e5, #7c3aed); border: 2px solid #ffffff; border-radius: 6px; padding: 4px 8px; margin: 0 2px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); transition: all 0.2s ease; font-weight: 600; ` }); link.addEventListener("mouseenter", () => { if (!link.innerHTML.includes("已添加") && !link.innerHTML.includes("失败")) { link.style.background = "linear-gradient(135deg, #3730a3, #5b21b6)"; link.style.transform = "scale(1.05)"; link.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.3)"; } }); link.addEventListener("mouseleave", () => { if (!link.innerHTML.includes("已添加") && !link.innerHTML.includes("失败")) { link.style.background = "linear-gradient(135deg, #4f46e5, #7c3aed)"; link.style.transform = "scale(1)"; link.style.boxShadow = "0 2px 4px rgba(0, 0, 0, 0.2)"; } }); link.innerHTML = ` 添加表情 `; link.title = "添加到用户表情"; link.addEventListener("click", async (e) => { e.preventDefault(); e.stopPropagation(); const originalHTML = link.innerHTML; const originalStyle = link.style.cssText; try { addEmojiToUserscript(emojiData); link.innerHTML = ` 已添加 `; 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 = ` 失败 `; link.style.background = "linear-gradient(135deg, #ef4444, #dc2626)"; link.style.color = "#ffffff"; link.style.border = "2px solid #ffffff"; link.style.boxShadow = "0 2px 4px rgba(239, 68, 68, 0.3)"; setTimeout(() => { link.innerHTML = originalHTML; link.style.cssText = originalStyle; }, 2e3); } }); return link; } function processLightbox(lightbox) { if (lightbox.querySelector(".emoji-add-link")) return; const img = lightbox.querySelector(".mfp-img"); const title = lightbox.querySelector(".mfp-title"); if (!img || !title) return; const emojiData = extractEmojiFromImage(img, title); if (!emojiData) return; const addButton = createAddButton$1(emojiData); const sourceLink = title.querySelector("a.image-source-link"); if (sourceLink) { const separator = document.createTextNode(" · "); title.insertBefore(separator, sourceLink); title.insertBefore(addButton, sourceLink); } else { title.appendChild(document.createTextNode(" · ")); title.appendChild(addButton); } } function processAllLightboxes() { document.querySelectorAll(".mfp-wrap.mfp-gallery").forEach((lightbox) => { if (lightbox.classList.contains("mfp-wrap") && lightbox.classList.contains("mfp-gallery") && lightbox.querySelector(".mfp-img")) processLightbox(lightbox); }); } function initOneClickAdd() { console.log("[Emoji Extension Userscript] Initializing one-click add functionality"); setTimeout(processAllLightboxes, 500); new MutationObserver((mutations) => { let hasNewLightbox = false; mutations.forEach((mutation) => { if (mutation.type === "childList") mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { const element = node; if (element.classList && element.classList.contains("mfp-wrap")) hasNewLightbox = true; } }); }); if (hasNewLightbox) setTimeout(processAllLightboxes, 100); }).observe(document.body, { childList: true, subtree: true }); document.addEventListener("visibilitychange", () => { if (!document.hidden) setTimeout(processAllLightboxes, 200); }); initBatchParseButtons(); } function createBatchParseButton(cookedElement) { const button = createEl("button", { className: "emoji-batch-parse-button", style: ` display: inline-flex; align-items: center; gap: 6px; background: var(--tertiary-low); color: var(--d-button-default-icon-color); border-radius: 8px; padding: 8px 12px; margin: 10px 0; font-weight: 600; cursor: pointer; border: none; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); transition: all 0.2s ease; ` }); button.innerHTML = ` 一键解析并添加所有图片 `; button.title = "解析当前内容中的所有图片并添加到用户表情"; button.addEventListener("mouseenter", () => { if (!button.disabled) { button.style.background = "var(--tertiary-high)"; button.style.transform = "scale(1.02)"; } }); button.addEventListener("mouseleave", () => { if (!button.disabled && !button.innerHTML.includes("已处理")) { button.style.background = "var(--tertiary-very-low)"; button.style.transform = "scale(1)"; } }); button.addEventListener("click", async (e) => { e.preventDefault(); e.stopPropagation(); const originalHTML = button.innerHTML; const originalStyle = button.style.cssText; try { button.innerHTML = "正在解析..."; button.style.background = "var(--primary-medium)"; button.disabled = true; const lightboxWrappers = cookedElement.querySelectorAll(".lightbox-wrapper"); const allEmojiData = []; lightboxWrappers.forEach((wrapper) => { const items = extractEmojiDataFromLightboxWrapper(wrapper); allEmojiData.push(...items); }); if (allEmojiData.length === 0) throw new Error("未找到可解析的图片"); let successCount = 0; for (const emojiData of allEmojiData) try { addEmojiToUserscript(emojiData); successCount++; } catch (e$1) { console.error("[Userscript OneClick] 添加图片失败", emojiData.name, e$1); } button.innerHTML = ` 已处理 ${successCount}/${allEmojiData.length} 张图片 `; button.style.background = "linear-gradient(135deg, #10b981, #059669)"; setTimeout(() => { button.innerHTML = originalHTML; button.style.cssText = originalStyle; button.disabled = false; }, 3e3); } catch (error) { console.error("[Userscript OneClick] Batch parse failed:", error); button.innerHTML = ` 解析失败 `; button.style.background = "linear-gradient(135deg, #ef4444, #dc2626)"; setTimeout(() => { button.innerHTML = originalHTML; button.style.cssText = originalStyle; button.disabled = false; }, 3e3); } }); return button; } function processCookedContent(cookedElement) { if (cookedElement.querySelector(".emoji-batch-parse-button")) return; if (!cookedElement.querySelector(".lightbox-wrapper")) return; const batchButton = createBatchParseButton(cookedElement); cookedElement.insertBefore(batchButton, cookedElement.firstChild); } function processCookedContents() { document.querySelectorAll(".cooked").forEach((element) => { if (element.classList.contains("cooked") && element.querySelector(".lightbox-wrapper")) processCookedContent(element); }); } function initBatchParseButtons() { setTimeout(processCookedContents, 500); new MutationObserver((mutations) => { let hasNewCooked = false; mutations.forEach((mutation) => { if (mutation.type === "childList") mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { const element = node; if (element.classList && element.classList.contains("cooked")) hasNewCooked = true; if (element.querySelectorAll && element.querySelectorAll(".cooked").length > 0) hasNewCooked = true; } }); }); if (hasNewCooked) setTimeout(processCookedContents, 100); }).observe(document.body, { childList: true, subtree: true }); } function extractNameFromUrl(url) { try { const nameWithoutExt = (new URL(url).pathname.split("/").pop() || "").replace(/\.[^/.]+$/, ""); const decoded = decodeURIComponent(nameWithoutExt); if (/^[0-9a-f]{8,}$/i.test(decoded) || decoded.length < 2) return "表情"; return decoded || "表情"; } catch { return "表情"; } } function createAddButton(data) { const button = createEl("a", { className: "emoji-add-link", style: ` color:#fff; border-radius:6px; padding:4px 8px; margin:0 2px; display:inline-flex; align-items:center; font-weight:600; text-decoration:none; border: 1px solid rgba(255,255,255,0.7); cursor: pointer; `, title: "添加到未分组表情" }); button.innerHTML = `添加表情`; function addToUngrouped(emoji) { const data$1 = loadDataFromLocalStorage(); let group = data$1.emojiGroups.find((g) => g.id === "ungrouped"); if (!group) { group = { id: "ungrouped", name: "未分组", icon: "📦", order: 999, emojis: [] }; data$1.emojiGroups.push(group); } if (!group.emojis.some((e) => e.url === emoji.url || e.name === emoji.name)) { group.emojis.push({ packet: Date.now(), name: emoji.name, url: emoji.url }); saveDataToLocalStorage({ emojiGroups: data$1.emojiGroups }); } } button.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); try { addToUngrouped({ name: data.name, url: data.url }); const original = button.textContent || ""; button.textContent = "已添加"; button.style.background = "linear-gradient(135deg,#10b981,#059669)"; setTimeout(() => { button.textContent = original || "添加表情"; button.style.background = ""; }, 1500); } catch (err) { console.warn("[Userscript] add emoji failed", err); const original = button.textContent || ""; button.textContent = "失败"; button.style.background = "linear-gradient(135deg,#ef4444,#dc2626)"; setTimeout(() => { button.textContent = original || "添加表情"; button.style.background = "linear-gradient(135deg, #4f46e5, #7c3aed)"; }, 1500); } }); return button; } function addEmojiButtonToPswp(container) { const topBar = container.querySelector(".pswp__top-bar") || (container.classList.contains("pswp__top-bar") ? container : null); if (!topBar) return; if (topBar.querySelector(".emoji-add-link")) return; const originalBtn = topBar.querySelector(".pswp__button--original-image"); const downloadBtn = topBar.querySelector(".pswp__button--download-image"); let imgUrl = ""; if (originalBtn?.href) imgUrl = originalBtn.href; else if (downloadBtn?.href) imgUrl = downloadBtn.href; if (!imgUrl) return; let name = ""; const captionTitle = document.querySelector(".pswp__caption-title"); if (captionTitle?.textContent?.trim()) name = captionTitle.textContent.trim(); if (!name) { if (originalBtn?.title) name = originalBtn.title; else if (downloadBtn?.title) name = downloadBtn.title; } if (!name || name.length < 2) name = extractNameFromUrl(imgUrl); name = name.trim() || "表情"; const addButton = createAddButton({ name, url: imgUrl }); if (downloadBtn?.parentElement) downloadBtn.parentElement.insertBefore(addButton, downloadBtn.nextSibling); else topBar.appendChild(addButton); } function scanForPhotoSwipeTopBar() { document.querySelectorAll(".pswp__top-bar").forEach((topBar) => addEmojiButtonToPswp(topBar)); } function observePhotoSwipeTopBar() { scanForPhotoSwipeTopBar(); function debounce(fn, wait = 100) { let timer = null; return (...args) => { if (timer !== null) window.clearTimeout(timer); timer = window.setTimeout(() => { timer = null; fn(...args); }, wait); }; } const debouncedScan = debounce(scanForPhotoSwipeTopBar, 100); const observer = new MutationObserver((mutations) => { for (const m of mutations) { if (m.type === "childList" && (m.addedNodes.length > 0 || m.removedNodes.length > 0)) { debouncedScan(); return; } if (m.type === "attributes") { debouncedScan(); return; } } }); observer.observe(document.body, { childList: true, subtree: true, attributes: false }); return observer; } function initPhotoSwipeTopbarUserscript() { scanForPhotoSwipeTopBar(); observePhotoSwipeTopBar(); } function detectRuntimePlatform() { try { const isMobileSize = window.innerWidth <= 768; const userAgent = navigator.userAgent.toLowerCase(); const isMobileUserAgent = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent); const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 0; if (isMobileSize && (isMobileUserAgent || isTouchDevice)) return "mobile"; else if (!isMobileSize && !isMobileUserAgent) return "pc"; return "original"; } catch { return "original"; } } function getEffectivePlatform() { return detectRuntimePlatform(); } function getPlatformUIConfig() { switch (getEffectivePlatform()) { case "mobile": return { emojiPickerMaxHeight: "60vh", emojiPickerColumns: 4, emojiSize: 32, isModal: true, useCompactLayout: true, showSearchBar: true, floatingButtonSize: 48 }; case "pc": return { emojiPickerMaxHeight: "400px", emojiPickerColumns: 6, emojiSize: 24, isModal: false, useCompactLayout: false, showSearchBar: true, floatingButtonSize: 40 }; default: return { emojiPickerMaxHeight: "350px", emojiPickerColumns: 5, emojiSize: 28, isModal: false, useCompactLayout: false, showSearchBar: true, floatingButtonSize: 44 }; } } function getPlatformToolbarSelectors() { const platform = getEffectivePlatform(); const baseSelectors = [ ".d-editor-button-bar[role=\"toolbar\"]", ".chat-composer__inner-container", ".d-editor-button-bar" ]; switch (platform) { case "mobile": return [ ...baseSelectors, ".mobile-composer .d-editor-button-bar", ".discourse-mobile .d-editor-button-bar", "[data-mobile-toolbar]" ]; case "pc": return [ ...baseSelectors, ".desktop-composer .d-editor-button-bar", ".discourse-desktop .d-editor-button-bar", "[data-desktop-toolbar]" ]; default: return baseSelectors; } } function logPlatformInfo() { const buildPlatform = "original"; const runtimePlatform = detectRuntimePlatform(); const effectivePlatform = getEffectivePlatform(); const config = getPlatformUIConfig(); console.log("[Platform] Build target:", buildPlatform); console.log("[Platform] Runtime detected:", runtimePlatform); console.log("[Platform] Effective platform:", effectivePlatform); console.log("[Platform] UI config:", config); console.log("[Platform] Screen size:", `${window.innerWidth}x${window.innerHeight}`); console.log("[Platform] User agent mobile:", /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent.toLowerCase())); console.log("[Platform] Touch device:", "ontouchstart" in window || navigator.maxTouchPoints > 0); } var _sharedPreview = null; function ensureHoverPreview() { if (_sharedPreview && document.body.contains(_sharedPreview)) return _sharedPreview; _sharedPreview = createEl("div", { className: "emoji-picker-hover-preview", style: "position:fixed;pointer-events:none;display:none;z-index:1000002;max-width:300px;max-height:300px;overflow:hidden;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,0.25);background:transparent;padding:6px;" }); const img = createEl("img", { className: "emoji-picker-hover-img", style: "display:block;max-width:100%;max-height:220px;object-fit:contain;" }); const label = createEl("div", { className: "emoji-picker-hover-label", style: "font-size:12px;color:var(--primary);margin-top:6px;text-align:center;" }); _sharedPreview.appendChild(img); _sharedPreview.appendChild(label); document.body.appendChild(_sharedPreview); return _sharedPreview; } var themeStylesInjected = false; function injectGlobalThemeStyles() { if (themeStylesInjected || typeof document === "undefined") return; themeStylesInjected = true; document.head.appendChild(createEl("style", { id: "emoji-extension-theme-globals", text: ` /* Global CSS variables for emoji extension theme support */ :root { /* Light theme (default) */ --emoji-modal-bg: #ffffff; --emoji-modal-text: #333333; --emoji-modal-border: #dddddd; --emoji-modal-input-bg: #ffffff; --emoji-modal-label: #555555; --emoji-modal-button-bg: #f5f5f5; --emoji-modal-primary-bg: #1890ff; --emoji-preview-bg: #ffffff; --emoji-preview-text: #222222; --emoji-preview-border: rgba(0,0,0,0.08); --emoji-button-gradient-start: #667eea; --emoji-button-gradient-end: #764ba2; --emoji-button-shadow: rgba(0, 0, 0, 0.15); --emoji-button-hover-shadow: rgba(0, 0, 0, 0.2); } /* Dark theme */ @media (prefers-color-scheme: dark) { :root { --emoji-modal-bg: #2d2d2d; --emoji-modal-text: #e6e6e6; --emoji-modal-border: #444444; --emoji-modal-input-bg: #3a3a3a; --emoji-modal-label: #cccccc; --emoji-modal-button-bg: #444444; --emoji-modal-primary-bg: #1677ff; --emoji-preview-bg: rgba(32,33,36,0.94); --emoji-preview-text: #e6e6e6; --emoji-preview-border: rgba(255,255,255,0.12); --emoji-button-gradient-start: #4a5568; --emoji-button-gradient-end: #2d3748; --emoji-button-shadow: rgba(0, 0, 0, 0.3); --emoji-button-hover-shadow: rgba(0, 0, 0, 0.4); } } ` })); } function ensureStyleInjected(id, css) { const style = document.createElement("style"); style.id = id; style.textContent = css; document.documentElement.appendChild(style); } function injectEmojiPickerStyles() { if (typeof document === "undefined") return; if (document.getElementById("emoji-picker-styles")) return; injectGlobalThemeStyles(); ensureStyleInjected("emoji-picker-styles", ` .emoji-picker-hover-preview{ position:fixed; pointer-events:none; display:none; z-index:1000002; max-width:320px; max-height:320px; overflow:hidden; border-radius:8px; box-shadow:0 6px 20px rgba(0,0,0,0.32); background:var(--emoji-preview-bg); padding:8px; transition:opacity .3s ease, transform .12s ease; border: 1px solid var(--emoji-preview-border); backdrop-filter: blur(10px); } .emoji-picker-hover-preview img.emoji-picker-hover-img{ display:block; max-width:100%; max-height:220px; object-fit:contain; } .emoji-picker-hover-preview .emoji-picker-hover-label{ font-size:12px; color:var(--emoji-preview-text); margin-top:8px; text-align:center; word-break:break-word; font-weight: 500; } `); } function isImageUrl(value) { if (!value) return false; let v = value.trim(); if (/^url\(/i.test(v)) { const inner = v.replace(/^url\(/i, "").replace(/\)$/, "").trim(); if (inner.startsWith("\"") && inner.endsWith("\"") || inner.startsWith("'") && inner.endsWith("'")) v = inner.slice(1, -1).trim(); else v = inner; } if (v.startsWith("data:image/")) return true; if (v.startsWith("blob:")) return true; if (v.startsWith("//")) v = "https:" + v; if (/\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)(\?.*)?$/i.test(v)) return true; try { const url = new URL(v); const protocol = url.protocol; if (protocol === "http:" || protocol === "https:" || protocol.endsWith(":")) { if (/\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)(\?.*)?$/i.test(url.pathname)) return true; if (/format=|ext=|type=image|image_type=/i.test(url.search)) return true; } } catch {} return false; } function isMobileView() { try { return getEffectivePlatform() === "mobile" || !!(userscriptState && userscriptState.settings && userscriptState.settings.forceMobileMode); } catch (e) { return false; } } function insertEmojiIntoEditor(emoji) { console.log("[Emoji Extension Userscript] Inserting emoji:", emoji); if (emoji.name && emoji.url) trackEmojiUsage(emoji.name, emoji.url); const selectors = [ "textarea.d-editor-input", "textarea.ember-text-area", "#channel-composer", ".chat-composer__input", "textarea.chat-composer__input" ]; const proseMirror = document.querySelector(".ProseMirror.d-editor-input"); let textarea = null; for (const s of selectors) { const el = document.querySelector(s); if (el) { textarea = el; break; } } const contentEditable = document.querySelector("[contenteditable=\"true\"]"); if (!textarea && !proseMirror && !contentEditable) { console.error("找不到输入框"); return; } const dimensionMatch = emoji.url?.match(/_(\d{3,})x(\d{3,})\./); let width = "500"; let height = "500"; if (dimensionMatch) { width = dimensionMatch[1]; height = dimensionMatch[2]; } else if (emoji.width && emoji.height) { width = emoji.width.toString(); height = emoji.height.toString(); } const scale = userscriptState.settings?.imageScale || 30; const outputFormat = userscriptState.settings?.outputFormat || "markdown"; if (textarea) { let insertText = ""; if (outputFormat === "html") { const scaledWidth = Math.max(1, Math.round(Number(width) * (scale / 100))); const scaledHeight = Math.max(1, Math.round(Number(height) * (scale / 100))); insertText = `:${emoji.name}: `; } else insertText = `![${emoji.name}|${width}x${height},${scale}%](${emoji.url}) `; const selectionStart = textarea.selectionStart; const selectionEnd = textarea.selectionEnd; textarea.value = textarea.value.substring(0, selectionStart) + insertText + textarea.value.substring(selectionEnd, textarea.value.length); textarea.selectionStart = textarea.selectionEnd = selectionStart + insertText.length; textarea.focus(); const inputEvent = new Event("input", { bubbles: true, cancelable: true }); textarea.dispatchEvent(inputEvent); } else if (proseMirror) { const imgWidth = Number(width) || 500; const scaledWidth = Math.max(1, Math.round(imgWidth * (scale / 100))); const htmlContent = `${emoji.name}`; try { const dataTransfer = new DataTransfer(); dataTransfer.setData("text/html", htmlContent); const pasteEvent = new ClipboardEvent("paste", { clipboardData: dataTransfer, bubbles: true }); proseMirror.dispatchEvent(pasteEvent); } catch (error) { try { document.execCommand("insertHTML", false, htmlContent); } catch (fallbackError) { console.error("无法向富文本编辑器中插入表情", fallbackError); } } } else if (contentEditable) try { if (outputFormat === "html") { const imgWidth = Number(width) || 500; const scaledWidth = Math.max(1, Math.round(imgWidth * (scale / 100))); const htmlContent = `${emoji.name} `; const sel = window.getSelection(); if (sel && sel.rangeCount > 0) { const range = sel.getRangeAt(0); const frag = document.createRange().createContextualFragment(htmlContent); range.deleteContents(); range.insertNode(frag); range.collapse(false); sel.removeAllRanges(); sel.addRange(range); } else contentEditable.insertAdjacentHTML("beforeend", htmlContent); } else { const insertText = `![${emoji.name}|${width}x${height},${scale}%](${emoji.url}) `; const textNode = document.createTextNode(insertText); const sel = window.getSelection(); if (sel && sel.rangeCount > 0) { const range = sel.getRangeAt(0); range.deleteContents(); range.insertNode(textNode); range.setStartAfter(textNode); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } else contentEditable.appendChild(textNode); } const inputEvent = new Event("input", { bubbles: true, cancelable: true }); contentEditable.dispatchEvent(inputEvent); } catch (e) { console.error("无法向 contenteditable 插入表情", e); } } function createMobileEmojiPicker(groups) { const modal = createEl("div", { className: "modal d-modal fk-d-menu-modal emoji-picker-content", attrs: { "data-identifier": "emoji-picker", "data-keyboard": "false", "aria-modal": "true", role: "dialog" } }); const modalContainerDiv = createEl("div", { className: "d-modal__container" }); const modalBody = createEl("div", { className: "d-modal__body" }); modalBody.tabIndex = -1; const emojiPickerDiv = createEl("div", { className: "emoji-picker" }); const filterContainer = createEl("div", { className: "emoji-picker__filter-container" }); const filterInputContainer = createEl("div", { className: "emoji-picker__filter filter-input-container" }); const filterInput = createEl("input", { className: "filter-input", placeholder: "按表情符号名称搜索…", type: "text" }); filterInputContainer.appendChild(filterInput); const closeButton = createEl("button", { className: "btn no-text btn-icon btn-transparent emoji-picker__close-btn", type: "button", innerHTML: ``, on: { click: () => { (modal.closest(".modal-container") || modal)?.remove(); document.querySelector(".d-modal__backdrop")?.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 scrollableContent = createEl("div", { className: "emoji-picker__scrollable-content" }); const sections = createEl("div", { className: "emoji-picker__sections", attrs: { role: "button" } }); groups.forEach((group, index) => { if (!group?.emojis?.length) return; const navButton = createEl("button", { className: `btn no-text btn-flat emoji-picker__section-btn ${index === 0 ? "active" : ""}`, attrs: { tabindex: "-1", "data-section": group.id, type: "button" } }); const iconVal = group.icon || "📁"; if (isImageUrl(iconVal)) { const img = createEl("img", { src: iconVal, alt: group.name || "", className: "emoji", style: "width: 18px; height: 18px; object-fit: contain;" }); navButton.appendChild(img); } else navButton.textContent = String(iconVal); navButton.title = group.name; navButton.addEventListener("click", () => { sectionsNav.querySelectorAll(".emoji-picker__section-btn").forEach((btn) => btn.classList.remove("active")); navButton.classList.add("active"); const target = sections.querySelector(`[data-section="${group.id}"]`); if (target) target.scrollIntoView({ behavior: "smooth", block: "start" }); }); sectionsNav.appendChild(navButton); const section = createEl("div", { className: "emoji-picker__section", attrs: { "data-section": group.id, role: "region", "aria-label": group.name } }); const titleContainer = createEl("div", { className: "emoji-picker__section-title-container" }); titleContainer.appendChild(createEl("h2", { className: "emoji-picker__section-title", text: group.name })); const sectionEmojis = createEl("div", { className: "emoji-picker__section-emojis" }); group.emojis.forEach((emoji) => { if (!emoji || typeof emoji !== "object" || !emoji.url || !emoji.name) return; const img = createEl("img", { src: emoji.url, alt: emoji.name, className: "emoji", title: `:${emoji.name}:`, style: "width: 32px; height: 32px; object-fit: contain;", attrs: { "data-emoji": emoji.name, tabindex: "0", loading: "lazy" } }); (function bindHover(imgEl, emo) { if (!userscriptState.settings?.enableFloatingPreview) return; const preview = ensureHoverPreview(); const previewImg = preview.querySelector("img"); const previewLabel = preview.querySelector(".emoji-picker-hover-label"); let fadeTimer = null; function onEnter(e) { previewImg.src = emo.url; previewLabel.textContent = emo.name || ""; preview.style.display = "block"; preview.style.opacity = "1"; preview.style.transition = "opacity 0.12s ease, transform 0.12s ease"; if (fadeTimer) { clearTimeout(fadeTimer); fadeTimer = null; } fadeTimer = window.setTimeout(() => { preview.style.opacity = "0"; setTimeout(() => { if (preview.style.opacity === "0") preview.style.display = "none"; }, 300); }, 5e3); move(e); } function move(e) { const pad = 12; const vw = window.innerWidth; const vh = window.innerHeight; const rect = preview.getBoundingClientRect(); let left = e.clientX + pad; let top = e.clientY + pad; if (left + rect.width > vw) left = e.clientX - rect.width - pad; if (top + rect.height > vh) top = e.clientY - rect.height - pad; preview.style.left = left + "px"; preview.style.top = top + "px"; } function onLeave() { if (fadeTimer) { clearTimeout(fadeTimer); fadeTimer = null; } preview.style.display = "none"; } imgEl.addEventListener("mouseenter", onEnter); imgEl.addEventListener("mousemove", move); imgEl.addEventListener("mouseleave", onLeave); })(img, emoji); img.addEventListener("click", () => { insertEmojiIntoEditor(emoji); if (modal.parentElement) modal.parentElement.removeChild(modal); }); img.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); insertEmojiIntoEditor(emoji); if (modal.parentElement) modal.parentElement.removeChild(modal); } }); sectionEmojis.appendChild(img); }); section.appendChild(titleContainer); section.appendChild(sectionEmojis); sections.appendChild(section); }); filterInput.addEventListener("input", (e) => { const q = (e.target.value || "").toLowerCase(); sections.querySelectorAll("img").forEach((img) => { const emojiName = (img.dataset.emoji || "").toLowerCase(); img.style.display = q === "" || emojiName.includes(q) ? "" : "none"; }); sections.querySelectorAll(".emoji-picker__section").forEach((section) => { const visibleEmojis = section.querySelectorAll("img:not([style*=\"display: none\"])"); section.style.display = visibleEmojis.length > 0 ? "" : "none"; }); }); scrollableContent.appendChild(sections); content.appendChild(sectionsNav); content.appendChild(scrollableContent); emojiPickerDiv.appendChild(filterContainer); emojiPickerDiv.appendChild(content); modalBody.appendChild(emojiPickerDiv); modalContainerDiv.appendChild(modalBody); modal.appendChild(modalContainerDiv); return modal; } function createDesktopEmojiPicker(groups) { const picker = createEl("div", { className: "fk-d-menu -animated -expanded", style: "max-width: 400px; visibility: visible; z-index: 999999;", attrs: { "data-identifier": "emoji-picker", role: "dialog" } }); const innerContent = createEl("div", { className: "fk-d-menu__inner-content" }); const emojiPickerDiv = createEl("div", { className: "emoji-picker" }); const filterContainer = createEl("div", { className: "emoji-picker__filter-container" }); const filterDiv = createEl("div", { className: "emoji-picker__filter filter-input-container" }); const searchInput = createEl("input", { className: "filter-input", placeholder: "按表情符号名称搜索…", type: "text" }); filterDiv.appendChild(searchInput); filterContainer.appendChild(filterDiv); const content = createEl("div", { className: "emoji-picker__content" }); const sectionsNav = createEl("div", { className: "emoji-picker__sections-nav" }); const 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); } function showTemporaryMessage(message, duration = 2e3) { const messageEl = createEl("div", { style: ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: var(--emoji-modal-primary-bg); color: white; padding: 12px 24px; border-radius: 6px; z-index: 9999999; font-size: 14px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); animation: fadeInOut 2s ease-in-out; `, text: message }); if (!document.querySelector("#tempMessageStyles")) { const style = createEl("style", { id: "tempMessageStyles", text: ` @keyframes fadeInOut { 0%, 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.9); } 20%, 80% { opacity: 1; transform: translate(-50%, -50%) scale(1); } } ` }); document.head.appendChild(style); } document.body.appendChild(messageEl); setTimeout(() => { try { messageEl.remove(); } catch {} }, duration); } function createModalElement(options) { const modal = createEl("div", { style: ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 999999; display: flex; align-items: center; justify-content: center; `, className: options.className }); const content = createEl("div", { style: ` background: var(--secondary); color: var(--emoji-modal-text); border: 1px solid var(--emoji-modal-border); border-radius: 8px; padding: 24px; max-width: 90%; max-height: 90%; overflow-y: auto; position: relative; ` }); if (options.title) { const titleElement = createEl("div", { style: ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; `, innerHTML: `

${options.title}

` }); content.appendChild(titleElement); const closeButton = content.querySelector("#closeModal"); if (closeButton && options.onClose) closeButton.addEventListener("click", options.onClose); } if (options.content) { const contentDiv = createEl("div", { innerHTML: options.content }); content.appendChild(contentDiv); } modal.appendChild(content); return modal; } function showPopularEmojisModal() { injectGlobalThemeStyles(); const popularEmojis = getPopularEmojis(50); const contentHTML = `
表情按使用次数排序 点击表情直接使用
总使用次数:${popularEmojis.reduce((sum, emoji) => sum + emoji.count, 0)}
${popularEmojis.length === 0 ? "
还没有使用过表情
开始使用表情后,这里会显示常用的表情
" : popularEmojis.map((emoji) => ` `).join("")}
${popularEmojis.length > 0 ? `
统计数据保存在本地,清空统计将重置所有使用记录
` : ""} `; const modal = createModalElement({ title: `常用表情 (${popularEmojis.length})`, content: contentHTML, onClose: () => modal.remove() }); const titleDiv = modal.querySelector("div:first-child > div:first-child, div:first-child > h2 + div"); if (titleDiv) { const clearStatsButton = createEl("button", { id: "clearStats", text: "清空统计", style: "padding: 6px 12px; background: #ff4444; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; margin-right: 8px;" }); titleDiv.appendChild(clearStatsButton); clearStatsButton.addEventListener("click", () => { if (confirm("确定要清空所有表情使用统计吗?此操作不可撤销。")) { clearEmojiUsageStats(); modal.remove(); showTemporaryMessage("表情使用统计已清空"); setTimeout(() => showPopularEmojisModal(), 300); } }); } const content = modal.querySelector("div:last-child"); document.body.appendChild(modal); ensureStyleInjected("popular-emojis-styles", ` .popular-emoji-item:hover { transform: translateY(-2px); border-color: var(--emoji-modal-primary-bg) !important; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } `); content.querySelectorAll(".popular-emoji-item").forEach((item) => { item.addEventListener("click", () => { const name = item.getAttribute("data-name"); const url = item.getAttribute("data-url"); if (name && url) { trackEmojiUsage(name, url); useEmojiFromPopular(name, url); modal.remove(); showTemporaryMessage(`已使用表情: ${name}`); } }); }); } function useEmojiFromPopular(name, url) { const activeElement = document.activeElement; if (activeElement && (activeElement.tagName === "TEXTAREA" || activeElement.tagName === "INPUT")) { const textArea = activeElement; const format = userscriptState.settings.outputFormat; let emojiText = ""; if (format === "markdown") emojiText = `![${name}](${url})`; else emojiText = `${name}`; const start = textArea.selectionStart || 0; const end = textArea.selectionEnd || 0; const currentValue = textArea.value; textArea.value = currentValue.slice(0, start) + emojiText + currentValue.slice(end); const newPosition = start + emojiText.length; textArea.setSelectionRange(newPosition, newPosition); textArea.dispatchEvent(new Event("input", { bubbles: true })); textArea.focus(); } else { const textAreas = document.querySelectorAll("textarea, input[type=\"text\"], [contenteditable=\"true\"]"); const lastTextArea = Array.from(textAreas).pop(); if (lastTextArea) { lastTextArea.focus(); if (lastTextArea.tagName === "TEXTAREA" || lastTextArea.tagName === "INPUT") { const format = userscriptState.settings.outputFormat; let emojiText = ""; if (format === "markdown") emojiText = `![${name}](${url})`; else emojiText = `${name}`; const textarea = lastTextArea; textarea.value += emojiText; textarea.dispatchEvent(new Event("input", { bubbles: true })); } } } } var currentPicker = null; function closeCurrentPicker() { if (currentPicker) { currentPicker.remove(); currentPicker = null; } } function injectCustomMenuButtons(menu) { if (menu.querySelector(".emoji-extension-menu-item")) return; let dropdownMenu = menu.querySelector("ul.dropdown-menu"); if (!dropdownMenu) dropdownMenu = menu.querySelector("ul.chat-composer-dropdown__list"); if (!dropdownMenu) { console.warn("[Emoji Extension Userscript] No dropdown-menu or chat-composer-dropdown__list found in expanded menu"); return; } const isChatComposerMenu = dropdownMenu.classList.contains("chat-composer-dropdown__list"); const itemClassName = isChatComposerMenu ? "chat-composer-dropdown__item emoji-extension-menu-item" : "dropdown-menu__item emoji-extension-menu-item"; const btnClassName = isChatComposerMenu ? "btn btn-icon-text chat-composer-dropdown__action-btn btn-transparent" : "btn btn-icon-text"; const emojiPickerItem = createEl("li", { className: itemClassName }); const emojiPickerBtn = createEl("button", { className: btnClassName, type: "button", title: "表情包选择器", innerHTML: ` 表情包选择器 ` }); emojiPickerBtn.addEventListener("click", async (e) => { e.stopPropagation(); if (menu.parentElement) menu.remove(); if (currentPicker) { closeCurrentPicker(); return; } currentPicker = await createEmojiPicker(); if (!currentPicker) return; document.body.appendChild(currentPicker); currentPicker.style.position = "fixed"; currentPicker.style.top = "0"; currentPicker.style.left = "0"; currentPicker.style.right = "0"; currentPicker.style.bottom = "0"; currentPicker.style.zIndex = "999999"; setTimeout(() => { const handleClick = (e$1) => { if (currentPicker && !currentPicker.contains(e$1.target)) { closeCurrentPicker(); document.removeEventListener("click", handleClick); } }; document.addEventListener("click", handleClick); }, 100); }); emojiPickerItem.appendChild(emojiPickerBtn); dropdownMenu.appendChild(emojiPickerItem); console.log("[Emoji Extension Userscript] Custom menu buttons injected"); } function injectEmojiButton(toolbar) { if (toolbar.querySelector(".emoji-extension-button")) return; const isChatComposer = toolbar.classList.contains("chat-composer__inner-container"); const button = createEl("button", { className: "btn no-text btn-icon toolbar__button nacho-emoji-picker-button emoji-extension-button", title: "表情包", type: "button", innerHTML: "🐈‍⬛" }); const popularButton = createEl("button", { className: "btn no-text btn-icon toolbar__button nacho-emoji-popular-button emoji-extension-button", title: "常用表情", type: "button", innerHTML: "⭐" }); if (isChatComposer) { button.classList.add("fk-d-menu__trigger", "emoji-picker-trigger", "chat-composer-button", "btn-transparent", "-emoji"); button.setAttribute("aria-expanded", "false"); button.setAttribute("data-identifier", "emoji-picker"); button.setAttribute("data-trigger", ""); popularButton.classList.add("fk-d-menu__trigger", "popular-emoji-trigger", "chat-composer-button", "btn-transparent", "-popular"); popularButton.setAttribute("aria-expanded", "false"); popularButton.setAttribute("data-identifier", "popular-emoji"); popularButton.setAttribute("data-trigger", ""); } button.addEventListener("click", async (e) => { e.stopPropagation(); if (currentPicker) { closeCurrentPicker(); return; } currentPicker = await createEmojiPicker(); if (!currentPicker) return; document.body.appendChild(currentPicker); const buttonRect = button.getBoundingClientRect(); if (currentPicker.classList.contains("modal") || currentPicker.className.includes("d-modal")) { currentPicker.style.position = "fixed"; currentPicker.style.top = "0"; currentPicker.style.left = "0"; currentPicker.style.right = "0"; currentPicker.style.bottom = "0"; currentPicker.style.zIndex = "999999"; } else { currentPicker.style.position = "fixed"; const margin = 8; const vpWidth = window.innerWidth; const vpHeight = window.innerHeight; currentPicker.style.top = buttonRect.bottom + margin + "px"; currentPicker.style.left = buttonRect.left + "px"; const pickerRect = currentPicker.getBoundingClientRect(); const spaceBelow = vpHeight - buttonRect.bottom; const neededHeight = pickerRect.height + margin; let top = buttonRect.bottom + margin; if (spaceBelow < neededHeight) top = Math.max(margin, buttonRect.top - pickerRect.height - margin); let left = buttonRect.left; if (left + pickerRect.width + margin > vpWidth) left = Math.max(margin, vpWidth - pickerRect.width - margin); if (left < margin) left = margin; currentPicker.style.top = top + "px"; currentPicker.style.left = left + "px"; } setTimeout(() => { const handleClick = (e$1) => { if (currentPicker && !currentPicker.contains(e$1.target) && e$1.target !== button) { closeCurrentPicker(); document.removeEventListener("click", handleClick); } }; document.addEventListener("click", handleClick); }, 100); }); popularButton.addEventListener("click", (e) => { e.stopPropagation(); closeCurrentPicker(); showPopularEmojisModal(); }); try { if (isChatComposer) { const existingEmojiTrigger = toolbar.querySelector(".emoji-picker-trigger:not(.emoji-extension-button)"); if (existingEmojiTrigger) { toolbar.insertBefore(button, existingEmojiTrigger); toolbar.insertBefore(popularButton, existingEmojiTrigger); } else { toolbar.appendChild(button); toolbar.appendChild(popularButton); } } else { toolbar.appendChild(button); toolbar.appendChild(popularButton); } } catch (error) { console.error("[Emoji Extension Userscript] Failed to inject button:", error); } } var domObserver = null; function setupDomObserver() { if (domObserver) return; domObserver = new MutationObserver((mutations) => { let hasChanges = false; for (const mutation of mutations) { if (mutation.type === "childList" && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)) { hasChanges = true; break; } if (mutation.type === "attributes") { hasChanges = true; break; } } if (hasChanges) {} }); domObserver.observe(document, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "id"] }); console.log("[Emoji Extension Userscript] DOM observer set up for force mobile mode"); } var toolbarOptionsTriggerInitialized = false; var chatComposerTriggerInitialized = false; function setupForceMobileMenuTriggers() { if (!(userscriptState.settings?.forceMobileMode || false)) return; const portalContainer = document.querySelector("#d-menu-portals"); if (!portalContainer) { console.log("[Emoji Extension Userscript] #d-menu-portals not found, skipping force mobile menu triggers"); return; } console.log("[Emoji Extension Userscript] Force mobile mode enabled, setting up menu triggers"); setupDomObserver(); new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { const element = node; if (element.classList.contains("toolbar-menu__options-content") || element.classList.contains("chat-composer-dropdown__content") || element.classList.contains("chat-composer-dropdown__menu-content")) { console.log("[Emoji Extension Userscript] Menu expanded in portal, injecting custom buttons"); injectCustomMenuButtons(element); } } }); }); }).observe(portalContainer, { childList: true, subtree: true }); new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { const element = node; if (element.classList.contains("modal-container")) { let modalMenu = element.querySelector(".toolbar-menu__options-content[data-identifier=\"toolbar-menu__options\"]"); if (!modalMenu) modalMenu = element.querySelector(".chat-composer-dropdown__menu-content[data-identifier=\"chat-composer-dropdown__menu\"]"); if (modalMenu) { console.log("[Emoji Extension Userscript] Modal menu detected (immediate), injecting custom buttons"); injectCustomMenuButtons(modalMenu); } else { const modalContentObserver = new MutationObserver(() => { let delayedMenu = element.querySelector(".toolbar-menu__options-content[data-identifier=\"toolbar-menu__options\"]"); if (!delayedMenu) delayedMenu = element.querySelector(".chat-composer-dropdown__menu-content[data-identifier=\"chat-composer-dropdown__menu\"]"); if (delayedMenu) { console.log("[Emoji Extension Userscript] Modal menu detected (delayed), injecting custom buttons"); injectCustomMenuButtons(delayedMenu); modalContentObserver.disconnect(); } }); modalContentObserver.observe(element, { childList: true, subtree: true }); setTimeout(() => modalContentObserver.disconnect(), 1e3); } } } }); }); }).observe(document.body, { childList: true, subtree: false }); try { const existingModal = document.querySelector(".modal-container"); if (existingModal) { const existingMenu = existingModal.querySelector(".toolbar-menu__options-content[data-identifier=\"toolbar-menu__options\"]"); if (existingMenu) { console.log("[Emoji Extension Userscript] Found existing modal menu at init, injecting custom buttons"); injectCustomMenuButtons(existingMenu); } } } catch (e) {} const toolbarOptionsTrigger = document.querySelector("button.toolbar-menu__options-trigger[data-identifier=\"toolbar-menu__options\"]:not(.emoji-detected)"); if (toolbarOptionsTrigger) { toolbarOptionsTrigger.classList.add("emoji-detected"); toolbarOptionsTrigger.classList.add("emoji-attached"); if (!toolbarOptionsTrigger.dataset.emojiListenerAttached) { toolbarOptionsTrigger.addEventListener("click", () => { const checkMenu = (attempt = 0) => { const modalContainer = document.querySelector(".modal-container"); let menu = null; if (modalContainer) menu = modalContainer.querySelector(".toolbar-menu__options-content[data-identifier=\"toolbar-menu__options\"]"); if (!menu) menu = document.querySelector(".toolbar-menu__options-content[data-identifier=\"toolbar-menu__options\"]"); if (menu) injectCustomMenuButtons(menu); else if (attempt < 5) setTimeout(() => checkMenu(attempt + 1), 20); }; checkMenu(); }); toolbarOptionsTrigger.dataset.emojiListenerAttached = "true"; console.log("[Emoji Extension Userscript] Toolbar options trigger listener added"); } toolbarOptionsTriggerInitialized = true; } const chatComposerTrigger = document.querySelector("button.chat-composer-dropdown__trigger-btn[data-identifier=\"chat-composer-dropdown__menu\"]:not(.emoji-detected), button.chat-composer-dropdown__menu-trigger[data-identifier=\"chat-composer-dropdown__menu\"]:not(.emoji-detected), button.chat-composer-dropdown__menu-trigger.chat-composer-dropdown__trigger-btn:not(.emoji-detected)"); if (chatComposerTrigger) { chatComposerTrigger.classList.add("emoji-detected"); chatComposerTrigger.classList.add("emoji-attached"); if (!chatComposerTrigger.dataset.emojiListenerAttached) { chatComposerTrigger.addEventListener("click", () => { setTimeout(() => { const menu = document.querySelector(".chat-composer-dropdown__content[data-identifier=\"chat-composer-dropdown__menu\"], .chat-composer-dropdown__menu-content[data-identifier=\"chat-composer-dropdown__menu\"]"); if (menu) injectCustomMenuButtons(menu); }, 100); }); chatComposerTrigger.dataset.emojiListenerAttached = "true"; console.log("[Emoji Extension Userscript] Chat composer trigger listener added"); } chatComposerTriggerInitialized = true; } } var toolbarTriggersAttached = /* @__PURE__ */ new Set(); var chatComposerTriggersAttached = /* @__PURE__ */ new Set(); function setupForceMobileToolbarListeners() { if (!(userscriptState.settings?.forceMobileMode || false)) return; getPlatformToolbarSelectors().forEach((selector) => { Array.from(document.querySelectorAll(selector)).forEach((toolbar) => { try { Array.from(toolbar.querySelectorAll("button.toolbar-menu__options-trigger[data-identifier=\"toolbar-menu__options\"]:not(.emoji-detected):not(.emoji-attached), button.toolbar-menu__options-trigger:not(.emoji-detected):not(.emoji-attached)")).forEach((trigger) => { const triggerId = `toolbar-${trigger.id || Math.random().toString(36).substr(2, 9)}`; if (toolbarTriggersAttached.has(triggerId)) return; trigger.classList.add("emoji-detected"); trigger.classList.add("emoji-attached"); const handler = () => { const checkMenu = (attempt = 0) => { const modalContainer = document.querySelector(".modal-container"); let menu = null; if (modalContainer) menu = modalContainer.querySelector(".toolbar-menu__options-content[data-identifier=\"toolbar-menu__options\"]"); if (!menu) menu = document.querySelector(".toolbar-menu__options-content[data-identifier=\"toolbar-menu__options\"]"); if (menu) injectCustomMenuButtons(menu); else if (attempt < 5) setTimeout(() => checkMenu(attempt + 1), 20); }; checkMenu(); }; trigger.addEventListener("click", handler); trigger.dataset.emojiListenerAttached = "true"; toolbarTriggersAttached.add(triggerId); }); Array.from(toolbar.querySelectorAll("button.chat-composer-dropdown__trigger-btn:not(.emoji-detected):not(.emoji-attached), button.chat-composer-dropdown__menu-trigger:not(.emoji-detected):not(.emoji-attached), button.chat-composer-dropdown__trigger-btn:not(.emoji-detected):not(.emoji-attached), button.chat-composer-dropdown__menu-trigger:not(.emoji-detected):not(.emoji-attached)")).forEach((trigger) => { const triggerId = `chat-${trigger.id || Math.random().toString(36).substr(2, 9)}`; if (chatComposerTriggersAttached.has(triggerId)) return; trigger.classList.add("emoji-detected"); trigger.classList.add("emoji-attached"); const handler = () => { setTimeout(() => { const menu = document.querySelector(".chat-composer-dropdown__content[data-identifier=\"chat-composer-dropdown__menu\"], .chat-composer-dropdown__menu-content[data-identifier=\"chat-composer-dropdown__menu\"]"); if (menu) injectCustomMenuButtons(menu); }, 80); }; trigger.addEventListener("click", handler); trigger.dataset.emojiListenerAttached = "true"; chatComposerTriggersAttached.add(triggerId); }); } catch (e) { console.warn("[Emoji Extension Userscript] Failed to attach force-mobile listeners to toolbar", e); } }); }); } var _forceMobileToolbarIntervalId = null; var _domChangeCheckIntervalId = null; var _buttonExistenceCheckIntervalId = null; function startForceMobileToolbarListenerInterval(intervalMs = 1e3) { if (!(userscriptState.settings?.forceMobileMode || false)) return; if (_forceMobileToolbarIntervalId !== null) return; try { setupForceMobileToolbarListeners(); } catch (e) {} _forceMobileToolbarIntervalId = window.setInterval(() => { try { setupForceMobileToolbarListeners(); } catch (e) {} }, intervalMs); } function startDomChangeCheckInterval() { if (!(userscriptState.settings?.forceMobileMode || false)) return; if (_domChangeCheckIntervalId !== null) return; try { checkButtonsAndInjectIfNeeded(); } catch (e) {} _domChangeCheckIntervalId = window.setInterval(() => { try { checkButtonsAndInjectIfNeeded(); } catch (e) {} }, 1e3); } function startButtonExistenceCheckInterval() { if (_buttonExistenceCheckIntervalId !== null) return; _buttonExistenceCheckIntervalId = window.setInterval(() => { try { if (document.querySelectorAll(".emoji-extension-menu-item").length === 0) { setupForceMobileMenuTriggers(); setupForceMobileToolbarListeners(); } } catch (e) {} }, 1e4); } function checkButtonsAndInjectIfNeeded() { if (!(userscriptState.settings?.forceMobileMode || false)) return; const toolbarTrigger = document.querySelector("button.toolbar-menu__options-trigger[data-identifier=\"toolbar-menu__options\"]:not(.emoji-detected)"); const chatComposerTrigger = document.querySelector("button.chat-composer-dropdown__trigger-btn[data-identifier=\"chat-composer-dropdown__menu\"]:not(.emoji-detected), button.chat-composer-dropdown__menu-trigger[data-identifier=\"chat-composer-dropdown__menu\"]:not(.emoji-detected), button.chat-composer-dropdown__menu-trigger.chat-composer-dropdown__trigger-btn:not(.emoji-detected)"); if (toolbarTrigger && !toolbarOptionsTriggerInitialized) { if (document.querySelector("button.toolbar-menu__options-trigger[data-identifier=\"toolbar-menu__options\"]:not(.emoji-detected)")) setupForceMobileMenuTriggers(); } if (chatComposerTrigger && !chatComposerTriggerInitialized) { if (document.querySelector("button.chat-composer-dropdown__trigger-btn[data-identifier=\"chat-composer-dropdown__menu\"]:not(.emoji-detected), button.chat-composer-dropdown__menu-trigger[data-identifier=\"chat-composer-dropdown__menu\"]:not(.emoji-detected), button.chat-composer-dropdown__menu-trigger.chat-composer-dropdown__trigger-btn:not(.emoji-detected)")) setupForceMobileMenuTriggers(); } const selectors = getPlatformToolbarSelectors(); for (const selector of selectors) { const elements = Array.from(document.querySelectorAll(selector)); for (const toolbar of elements) if (Array.from(toolbar.querySelectorAll("button.toolbar-menu__options-trigger:not(.emoji-detected), button.chat-composer-dropdown__trigger-btn:not(.emoji-detected), button.chat-composer-dropdown__menu-trigger:not(.emoji-detected), button.chat-composer-dropdown__menu-trigger.chat-composer-dropdown__trigger-btn:not(.emoji-detected)")).length > 0) { setupForceMobileToolbarListeners(); break; } } } function startAllForceMobileIntervals() { startForceMobileToolbarListenerInterval(1e3); startDomChangeCheckInterval(); startButtonExistenceCheckInterval(); } function shouldSkipToolbarInjection() { if (!(userscriptState.settings?.forceMobileMode || false)) return false; return !!document.querySelector("#d-menu-portals"); } function findAllToolbars() { if (shouldSkipToolbarInjection()) { console.log("[Emoji Extension Userscript] Force mobile mode with #d-menu-portals detected, skipping toolbar injection"); return []; } const toolbars = []; const selectors = getPlatformToolbarSelectors(); for (const selector of selectors) { const elements = document.querySelectorAll(selector); toolbars.push(...Array.from(elements)); } return toolbars; } function attemptInjection() { const toolbars = findAllToolbars(); let injectedCount = 0; toolbars.forEach((toolbar) => { if (!toolbar.querySelector(".emoji-extension-button")) { console.log("[Emoji Extension Userscript] Toolbar found, injecting button."); injectEmojiButton(toolbar); injectedCount++; } }); setupForceMobileMenuTriggers(); try { setupForceMobileToolbarListeners(); try { startAllForceMobileIntervals(); } catch (e) {} } catch (e) {} return { injectedCount, totalToolbars: toolbars.length }; } function startPeriodicInjection() { setInterval(() => { findAllToolbars().forEach((toolbar) => { if (!toolbar.querySelector(".emoji-extension-button")) { console.log("[Emoji Extension Userscript] New toolbar found, injecting button."); injectEmojiButton(toolbar); } }); setupForceMobileMenuTriggers(); try { setupForceMobileToolbarListeners(); startAllForceMobileIntervals(); } catch (e) {} }, 3e4); } function userscriptNotify(message, type = "info", timeout = 4e3) { try { let container = document.getElementById("emoji-ext-userscript-toast"); if (!container) { container = createEl("div", { attrs: { id: "emoji-ext-userscript-toast", "aria-live": "polite" }, style: ` position: fixed; right: 12px; bottom: 12px; z-index: 2147483646; display:flex; flex-direction:column; gap:8px; ` }); try { if (document.body) document.body.appendChild(container); else document.documentElement.appendChild(container); } catch (e) { document.documentElement.appendChild(container); } container.style.position = "fixed"; container.style.right = "12px"; container.style.bottom = "12px"; container.style.zIndex = String(2147483646); try { container.style.setProperty("z-index", String(2147483646), "important"); } catch (_e) {} container.style.display = "flex"; container.style.flexDirection = "column"; container.style.gap = "8px"; container.style.pointerEvents = "auto"; } const el = createEl("div", { text: message, style: `padding:8px 12px; border-radius:6px; color:#fff; font-size:13px; max-width:320px; word-break:break-word; opacity:0; transform: translateY(8px); transition: all 220ms ease;` }); if (type === "success") el.style.setProperty("background", "#16a34a", "important"); else if (type === "error") el.style.setProperty("background", "#dc2626", "important"); else el.style.setProperty("background", "#0369a1", "important"); container.appendChild(el); el.offsetHeight; el.style.opacity = "1"; el.style.transform = "translateY(0)"; const id = setTimeout(() => { try { el.style.opacity = "0"; el.style.transform = "translateY(8px)"; setTimeout(() => el.remove(), 250); } catch (_e) {} clearTimeout(id); }, timeout); try { console.log("[UserscriptNotify] shown:", message, "type=", type); } catch (_e) {} return () => { try { el.remove(); } catch (_e) {} clearTimeout(id); }; } catch (_e) { return () => {}; } } var floatingButton = null; var isButtonVisible = false; var FLOATING_BUTTON_STYLES = ` .emoji-extension-floating-container { position: fixed !important; bottom: 20px !important; right: 20px !important; display: flex !important; flex-direction: column !important; gap: 10px !important; z-index: 999999 !important; } .emoji-extension-floating-button { width: 56px !important; height: 56px !important; border-radius: 50% !important; background: transparent; border: none !important; box-shadow: 0 4px 12px var(--emoji-button-shadow) !important; cursor: pointer !important; color: white !important; display: flex !important; align-items: center !important; justify-content: center !important; transition: all 0.2s ease !important; opacity: 0.95 !important; } .emoji-extension-floating-button:hover { transform: scale(1.05) !important; } .emoji-extension-floating-button:active { transform: scale(0.95) !important; } .emoji-extension-floating-button.secondary { background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%) !important; } .emoji-extension-floating-button.hidden { opacity: 0 !important; pointer-events: none !important; transform: translateY(20px) !important; } @media (max-width: 768px) { .emoji-extension-floating-button { width: 48px !important; height: 48px !important; font-size: 20px !important; } .emoji-extension-floating-container { bottom: 15px !important; right: 15px !important; } } `; function injectStyles() { injectGlobalThemeStyles(); ensureStyleInjected("emoji-extension-floating-button-styles", FLOATING_BUTTON_STYLES); } function createManualButton() { const button = createEl("button", { className: "emoji-extension-floating-button", title: "手动注入表情按钮 (Manual Emoji Injection)", innerHTML: "🐈‍⬛" }); button.addEventListener("click", async (e) => { e.stopPropagation(); e.preventDefault(); button.style.transform = "scale(0.9)"; button.innerHTML = "⏳"; try { if (attemptInjection().injectedCount > 0) { button.innerHTML = "✅"; setTimeout(() => { button.innerHTML = "🐈‍⬛"; button.style.transform = "scale(1)"; }, 1500); } else { button.innerHTML = "❌"; setTimeout(() => { button.innerHTML = "🐈‍⬛"; button.style.transform = "scale(1)"; }, 1500); } } catch (error) { button.innerHTML = "⚠️"; setTimeout(() => { button.innerHTML = "🐈‍⬛"; button.style.transform = "scale(1)"; }, 1500); console.error("[Emoji Extension Userscript] Manual injection error:", error); } }); return button; } async function invokeAutoRead(showNotify = false) { try { const fn = window.callAutoReadRepliesV2 || window.autoReadAllRepliesV2; console.log("[Emoji Extension] invokeAutoRead: found fn=", !!fn, " typeof=", typeof fn, " showNotify=", showNotify); if (fn && typeof fn === "function") { const res = await fn(); console.log("[Emoji Extension] invokeAutoRead: fn returned", res); if (showNotify) userscriptNotify("自动阅读已触发", "success"); } else { console.warn("[Emoji Extension] autoRead function not available on window"); console.log("[Emoji Extension] invokeAutoRead: attempting userscriptNotify fallback"); userscriptNotify("自动阅读功能当前不可用", "error"); } } catch (err) { console.error("[Emoji Extension] auto-read menu invocation failed", err); if (showNotify) userscriptNotify("自动阅读调用失败:" + (err && err.message ? err.message : String(err)), "error"); } } function createAutoReadMenuItem(variant = "dropdown") { if (variant === "dropdown") { const li$1 = createEl("li", { className: "submenu-item emoji-extension-auto-read" }); const a = createEl("a", { className: "submenu-link", attrs: { href: "#", title: "像插件一样自动阅读话题 (Auto-read topics)" }, innerHTML: ` 自动阅读 ` }); a.addEventListener("click", async (e) => { e.preventDefault(); e.stopPropagation(); await invokeAutoRead(true); }); li$1.appendChild(a); return li$1; } const li = createEl("li", { className: "submenu-item emoji-extension-auto-read sidebar-section-link-wrapper", style: "list-style: none; padding-left: 0;" }); const btn = createEl("button", { className: "fk-d-menu__trigger sidebar-more-section-trigger sidebar-section-link sidebar-more-section-links-details-summary sidebar-row --link-button sidebar-section-link sidebar-row", attrs: { type: "button", title: "像插件一样自动阅读话题 (Auto-read topics)", "aria-expanded": "false", "data-identifier": "emoji-ext-auto-read", "data-trigger": "" }, innerHTML: ` 自动阅读 `, style: ` background: transparent; border: none; ` }); btn.addEventListener("click", async (e) => { e.preventDefault(); e.stopPropagation(); await invokeAutoRead(true); }); li.appendChild(btn); return li; } function showFloatingButton() { if (userscriptState.settings?.forceMobileMode || false) { if (document.querySelector("#d-menu-portals")) { console.log("[Emoji Extension Userscript] Force mobile mode with #d-menu-portals detected, skipping floating button"); return; } } if (floatingButton) return; injectStyles(); const manual = createManualButton(); const wrapper = createEl("div", { className: "emoji-extension-floating-container" }); wrapper.appendChild(manual); document.body.appendChild(wrapper); floatingButton = wrapper; isButtonVisible = true; console.log("[Emoji Extension Userscript] Floating manual injection button shown (bottom-right)"); } async function injectIntoUserMenu() { const SELECTOR_SIDEBAR = "#sidebar-section-content-community"; const SELECTOR_OTHER_ANCHOR = "a.menu-item[title=\"其他服务\"], a.menu-item.vdm[title=\"其他服务\"]"; const SELECTOR_OTHER_DROPDOWN = ".d-header-dropdown .d-dropdown-menu"; for (;;) { const otherAnchor = document.querySelector(SELECTOR_OTHER_ANCHOR); if (otherAnchor) { const dropdown = otherAnchor.querySelector(SELECTOR_OTHER_DROPDOWN); if (dropdown) { dropdown.appendChild(createAutoReadMenuItem("dropdown")); isButtonVisible = true; console.log("[Emoji Extension Userscript] Auto-read injected into 其他服务 dropdown"); return; } } const sidebar = document.querySelector(SELECTOR_SIDEBAR); if (sidebar) { sidebar.appendChild(createAutoReadMenuItem("sidebar")); isButtonVisible = true; console.log("[Emoji Extension Userscript] Auto-read injected into sidebar #sidebar-section-content-community"); return; } await new Promise((resolve) => setTimeout(resolve, 500)); } } async function showAutoReadInMenu() { injectStyles(); try { await injectIntoUserMenu(); return; } catch (e) { console.warn("[Emoji Extension Userscript] injecting menu item failed", e); } } function hideFloatingButton() { if (floatingButton) { floatingButton.classList.add("hidden"); setTimeout(() => { if (floatingButton) { floatingButton.remove(); floatingButton = null; isButtonVisible = false; } }, 300); console.log("[Emoji Extension Userscript] Floating manual injection button hidden"); } } function autoShowFloatingButton() { if (!isButtonVisible) { console.log("[Emoji Extension Userscript] Auto-showing floating button due to injection difficulties"); showFloatingButton(); } } function checkAndShowFloatingButton() { if (userscriptState.settings?.forceMobileMode || false) { if (document.querySelector("#d-menu-portals")) { if (isButtonVisible) hideFloatingButton(); return; } } const existingButtons = document.querySelectorAll(".emoji-extension-button"); if (existingButtons.length === 0 && !isButtonVisible) setTimeout(() => { autoShowFloatingButton(); }, 2e3); else if (existingButtons.length > 0 && isButtonVisible) hideFloatingButton(); } function createE(tag, opts) { const el = document.createElement(tag); if (opts) { if (opts.wi) el.style.width = opts.wi; if (opts.he) el.style.height = opts.he; if (opts.class) el.className = opts.class; if (opts.text) el.textContent = opts.text; if (opts.ph && "placeholder" in el) el.placeholder = opts.ph; if (opts.type && "type" in el) el.type = opts.type; if (opts.val !== void 0 && "value" in el) el.value = opts.val; 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.in) el.innerHTML = opts.in; if (opts.ti) el.title = opts.ti; if (opts.alt && "alt" in el) el.alt = opts.alt; if (opts.id) el.id = opts.id; if (opts.accept && "accept" in el) el.accept = opts.accept; if (opts.multiple !== void 0 && "multiple" in el) el.multiple = opts.multiple; if (opts.role) el.setAttribute("role", opts.role); if (opts.tabIndex !== void 0) el.tabIndex = Number(opts.tabIndex); if (opts.ld && "loading" in el) el.loading = opts.ld; if (opts.on) for (const [evt, handler] of Object.entries(opts.on)) el.addEventListener(evt, handler); if (opts.child) opts.child.forEach((child) => el.appendChild(child)); } return el; } const DOA = document.body.appendChild.bind(document.body); document.head.appendChild.bind(document.head); const DEBI = document.getElementById.bind(document); document.addEventListener.bind(document); const DQSA = document.querySelectorAll.bind(document); const DQS = document.querySelector.bind(document); async function postTimings(topicId, timings) { function readCsrfToken() { try { const meta = DQS("meta[name=\"csrf-token\"]"); if (meta && meta.content) return meta.content; const input = DQS("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://${window.location.hostname}/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; const MAX_RETRIES = 5; const BASE_DELAY = 500; for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { const resp = await fetch(url, { method: "POST", body: params.toString(), credentials: "same-origin", headers }); if (resp.status !== 429) return resp; if (attempt === MAX_RETRIES) return resp; const retryAfter = resp.headers.get("Retry-After"); let waitMs = 0; if (retryAfter) { const asInt = parseInt(retryAfter, 10); if (!Number.isNaN(asInt)) waitMs = asInt * 1e3; else { const date = Date.parse(retryAfter); if (!Number.isNaN(date)) waitMs = Math.max(0, date - Date.now()); } } if (!waitMs) waitMs = BASE_DELAY * Math.pow(2, attempt) + Math.floor(Math.random() * BASE_DELAY); await new Promise((resolve) => setTimeout(resolve, waitMs)); } throw new Error("postTimings: unexpected execution path"); } function notify(message, type = "info", timeout = 4e3) { try { let container = DEBI("emoji-ext-toast-container"); if (!container) { container = createE("div", { id: "emoji-ext-toast-container", style: ` position: fixed; right: 12px; bottom: 12px; z-index: 2147483647; display: flex; flex-direction: column; gap: 8px; ` }); DOA(container); } const el = createE("div", { text: message, style: ` padding: 8px 12px; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.12); color: #ffffff; font-size: 13px; max-width: 320px; word-break: break-word; transform: translateY(20px); ` }); if (type === "success") el.style.background = "#16a34a"; else if (type === "error") el.style.background = "#dc2626"; else if (type === "transparent") el.style.background = "transparent"; else if (type === "rainbow") { el.style.background = "linear-gradient(90deg, red, orange, yellow, green, blue, indigo, violet, red)"; el.style.backgroundSize = "400% 100%"; el.style.animation = "color-shift 15s linear infinite"; if (!DEBI("color-shift-keyframes")) DOA(createE("style", { id: "color-shift-keyframes", text: ` @keyframes color-shift { 0% { background-position: 0% 50%; } 100% { background-position: 100% 50%; } } ` })); el.style.animation = "color-shift 1s linear infinite"; } else el.style.background = "#0369a1"; container.appendChild(el); const id = setTimeout(() => { el.remove(); clearTimeout(id); }, timeout); return () => { el.remove(); clearTimeout(id); }; } catch { try { alert(message); } catch {} return () => {}; } } 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 + 1; } 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 }; } function getCSRFToken() { try { const meta = document.querySelector("meta[name=\"csrf-token\"]"); if (meta && meta.content) return meta.content; const meta2 = document.querySelector("meta[name=\"x-csrf-token\"]"); if (meta2 && meta2.content) return meta2.content; const anyWin = window; if (anyWin && anyWin.csrfToken) return anyWin.csrfToken; if (anyWin && anyWin._csrf_token) return anyWin._csrf_token; const m = document.cookie.match(/(?:XSRF-TOKEN|_csrf)=([^;]+)/); if (m && m[1]) return decodeURIComponent(m[1]); } catch (e) {} return null; } async function setNotificationLevel(topicId, level = 1) { const token = getCSRFToken(); if (!token) { notify("无法获取 CSRF token,未能设置追踪等级", "error"); return; } const url = `${location.origin}/t/${topicId}/notifications`; const body = `notification_level=${encodeURIComponent(String(level))}`; try { const resp = await fetch(url, { method: "POST", headers: { accept: "*/*", "content-type": "application/x-www-form-urlencoded; charset=UTF-8", "x-csrf-token": token, "x-requested-with": "XMLHttpRequest", "discourse-logged-in": "true", "discourse-present": "true", priority: "u=1, i" }, body, mode: "cors", credentials: "include" }); if (!resp.ok) throw new Error(`设置追踪等级请求失败:${resp.status}`); notify(`话题 ${topicId} 的追踪等级已设置为 ${level}`, "rainbow"); } catch (e) { notify("设置追踪等级失败:" + (e && e.message ? e.message : String(e)), "error"); } } async function autoReadAll(topicId, startFrom = 1) { 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 = DQS("[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 = []; if (startFrom > total) { notify(`起始帖子号 ${startFrom} 超过总帖子数 ${total},已跳过`, "transparent"); return; } for (let n = startFrom; n <= total; n++) postNumbers.push(n); let BATCH_SIZE = Math.floor(Math.random() * 951) + 50; const ran = () => { BATCH_SIZE = Math.floor(Math.random() * 1e3) + 50; }; for (let i = 0; i < postNumbers.length; i += BATCH_SIZE) { const batch = postNumbers.slice(i, i + BATCH_SIZE); ran(); const timings = {}; for (const pn of batch) timings[pn] = Math.random() * 1e4; try { await postTimings(tid, timings); notify(`已标记 ${Object.keys(timings).length} 个帖子为已读(发送)`, "success"); } catch (e) { notify("发送阅读标记失败:" + (e && e.message ? e.message : String(e)), "error"); } await sleep(500 + Math.floor(Math.random() * 1e3)); } try { await setNotificationLevel(tid, 1); } catch (e) {} notify("自动阅读完成", "success"); } catch (e) { notify("自动阅读异常:" + (e && e.message ? e.message : String(e)), "error"); } } async function autoReadAllv2(topicId) { 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 anchors = Array.from(DQSA("a[href]")); const seen = /* @__PURE__ */ new Set(); for (const a of anchors) { const m = (a.getAttribute("href") || "").match(/^\/t\/topic\/(\d+)(?:\/(\d+))?$/); if (!m) continue; const id = Number(m[1]); const readPart = m[2] ? Number(m[2]) : void 0; const start = readPart && !Number.isNaN(readPart) ? readPart : 2; if (!id || seen.has(id)) continue; seen.add(id); await autoReadAll(id, start); await sleep(200); } } } } window.autoReadAllReplies = autoReadAll; window.autoReadAllRepliesV2 = autoReadAllv2; if (!window.autoReadAllRepliesV2) window.autoReadAllRepliesV2 = autoReadAllv2; async function initializeUserscriptData() { const data = await loadDataFromLocalStorageAsync(window.location.hostname).catch((err) => { console.warn("[Emoji Picker] loadDataFromLocalStorageAsync failed, falling back to sync loader", err); return loadDataFromLocalStorage(); }); userscriptState.emojiGroups = data.emojiGroups || []; userscriptState.settings = data.settings || userscriptState.settings; } function isDiscoursePage() { if (document.querySelectorAll("meta[name*=\"discourse\"], meta[content*=\"discourse\"], meta[property*=\"discourse\"]").length > 0) { console.log("[Emoji Picker] Discourse detected via meta tags"); return true; } const generatorMeta = document.querySelector("meta[name=\"generator\"]"); if (generatorMeta) { if ((generatorMeta.getAttribute("content")?.toLowerCase() || "").includes("discourse")) { console.log("[Emoji Picker] Discourse detected via generator meta"); return true; } } if (document.querySelectorAll("#main-outlet, .ember-application, textarea.d-editor-input, .ProseMirror.d-editor-input").length > 0) { console.log("[Emoji Picker] Discourse elements detected"); return true; } console.log("[Emoji Picker] Not a Discourse site"); return false; } async function initializeEmojiFeature(maxAttempts = 10, delay = 1e3) { console.log("[Emoji Picker] Initializing..."); logPlatformInfo(); await initializeUserscriptData(); try { if (userscriptState.settings?.enableBatchParseImages !== false) { initOneClickAdd(); initPhotoSwipeTopbarUserscript(); console.log("[Emoji Picker] One-click batch parse images enabled"); } else console.log("[Emoji Picker] One-click batch parse images disabled by setting"); } catch (e) { console.warn("[Emoji Picker] initOneClickAdd failed", e); } try { showAutoReadInMenu(); } catch (e) { console.warn("[Emoji Picker] showAutoReadInMenu failed", e); } function exposeAutoReadWrapper() { try { const existing = window.autoReadAllRepliesV2; if (existing && typeof existing === "function") { window.callAutoReadRepliesV2 = (topicId) => { try { return existing(topicId); } catch (e) { console.warn("[Emoji Picker] callAutoReadRepliesV2 invocation failed", e); } }; console.log("[Emoji Picker] callAutoReadRepliesV2 is exposed"); return; } window.callAutoReadRepliesV2 = (topicId) => { try { const fn = window.autoReadAllRepliesV2; if (fn && typeof fn === "function") return fn(topicId); } catch (e) { console.warn("[Emoji Picker] callAutoReadRepliesV2 invocation failed", e); } console.warn("[Emoji Picker] autoReadAllRepliesV2 not available on this page yet"); }; } catch (e) { console.warn("[Emoji Picker] exposeAutoReadWrapper failed", e); } } exposeAutoReadWrapper(); let attempts = 0; function attemptToolbarInjection() { attempts++; const result = attemptInjection(); if (result.injectedCount > 0 || result.totalToolbars > 0) { console.log(`[Emoji Picker] Injection successful: ${result.injectedCount} buttons injected into ${result.totalToolbars} toolbars`); return; } if (attempts < maxAttempts) { console.log(`[Emoji Picker] Toolbar not found, attempt ${attempts}/${maxAttempts}. Retrying in ${delay / 1e3}s.`); setTimeout(attemptToolbarInjection, delay); } else { console.error("[Emoji Picker] Failed to find toolbar after multiple attempts."); console.log("[Emoji Picker] Showing floating button as fallback"); showFloatingButton(); } } if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", attemptToolbarInjection); else attemptToolbarInjection(); startPeriodicInjection(); setInterval(() => { checkAndShowFloatingButton(); }, 5e3); } if (isDiscoursePage()) { console.log("[Emoji Picker] Discourse detected, initializing emoji picker feature"); initializeEmojiFeature(); } else console.log("[Emoji Picker] Not a Discourse site, skipping injection"); })(); })();