// ==UserScript== // @name Discourse 表情扩展 (Emoji Extension for Discourse) lite // @namespace https://github.com/stevessr/bug-v3 // @version 1.2.1 // @description 为 Discourse 论坛添加表情选择器功能 (Add emoji picker functionality to Discourse forums) // @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/549615/Discourse%20%E8%A1%A8%E6%83%85%E6%89%A9%E5%B1%95%20%28Emoji%20Extension%20for%20Discourse%29%20lite.user.js // @updateURL https://update.greasyfork.icu/scripts/549615/Discourse%20%E8%A1%A8%E6%83%85%E6%89%A9%E5%B1%95%20%28Emoji%20Extension%20for%20Discourse%29%20lite.meta.js // ==/UserScript== (function() { 'use strict'; (function() { var __defProp = Object.defineProperty; var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res); var __export = (all) => { let target = {}; for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); return target; }; async function fetchPackagedJSON(url) { try { if (typeof fetch === "undefined") return null; const res = await fetch(url || "/assets/defaultEmojiGroups.json", { cache: "no-cache" }); if (!res.ok) return null; return await res.json(); } catch (err) { return null; } } async function loadDefaultEmojiGroups(url) { const packaged = await fetchPackagedJSON(url); if (packaged && Array.isArray(packaged.groups)) return packaged.groups; return []; } var init_defaultEmojiGroups_loader = __esmMin((() => {})); function loadDataFromLocalStorage() { try { const groupsData = localStorage.getItem(STORAGE_KEY); let emojiGroups = []; if (groupsData) try { const parsed = JSON.parse(groupsData); if (Array.isArray(parsed) && parsed.length > 0) emojiGroups = parsed; } catch (e) { console.warn("[Userscript] Failed to parse stored emoji groups:", e); } if (emojiGroups.length === 0) emojiGroups = []; const settingsData = localStorage.getItem(SETTINGS_KEY); let settings = { ...DEFAULT_USER_SETTINGS }; if (settingsData) try { const parsed = JSON.parse(settingsData); if (parsed && typeof parsed === "object") settings = { ...settings, ...parsed }; } catch (e) { console.warn("[Userscript] Failed to parse stored settings:", e); } emojiGroups = emojiGroups.filter((g) => g.id !== "favorites"); console.log("[Userscript] Loaded data from localStorage:", { groupsCount: emojiGroups.length, emojisCount: emojiGroups.reduce((acc, g) => acc + (g.emojis?.length || 0), 0), settings }); return { emojiGroups, settings }; } catch (error) { console.error("[Userscript] Failed to load from localStorage:", error); return { emojiGroups: [], settings: { ...DEFAULT_USER_SETTINGS } }; } } async function loadDataFromLocalStorageAsync() { try { const local = loadDataFromLocalStorage(); if (local.emojiGroups && local.emojiGroups.length > 0) return local; const remoteUrl = localStorage.getItem("emoji_extension_remote_config_url"); if (remoteUrl && typeof remoteUrl === "string" && remoteUrl.trim().length > 0) try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5e3); const res = await fetch(remoteUrl, { signal: controller.signal }); clearTimeout(timeout); if (res && res.ok) { const json = await res.json(); const groups = Array.isArray(json.emojiGroups) ? json.emojiGroups : Array.isArray(json.groups) ? json.groups : null; const settings = json.settings && typeof json.settings === "object" ? json.settings : local.settings; if (groups && groups.length > 0) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(groups)); } catch (e) { console.warn("[Userscript] Failed to persist fetched remote groups to localStorage", e); } return { emojiGroups: groups.filter((g) => g.id !== "favorites"), settings }; } } } catch (err) { console.warn("[Userscript] Failed to fetch remote default config:", err); } try { const runtime = await loadDefaultEmojiGroups("https://video2gif-pages.pages.dev/assets/defaultEmojiGroups.json"); const source = runtime && runtime.length ? runtime : []; const filtered = JSON.parse(JSON.stringify(source)).filter((g) => g.id !== "favorites"); try { localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered)); } catch (e) {} return { emojiGroups: filtered, settings: local.settings }; } catch (e) { console.error("[Userscript] Failed to load default groups in async fallback:", e); return { emojiGroups: [], settings: local.settings }; } } catch (error) { console.error("[Userscript] loadDataFromLocalStorageAsync failed:", error); return { emojiGroups: [], settings: { ...DEFAULT_USER_SETTINGS } }; } } function saveDataToLocalStorage(data) { try { if (data.emojiGroups) localStorage.setItem(STORAGE_KEY, JSON.stringify(data.emojiGroups)); if (data.settings) localStorage.setItem(SETTINGS_KEY, JSON.stringify(data.settings)); } catch (error) { console.error("[Userscript] Failed to save to localStorage:", error); } } function 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 syncFromManager() { try { const managerGroups = localStorage.getItem("emoji_extension_manager_groups"); const managerSettings = localStorage.getItem("emoji_extension_manager_settings"); let updated = false; if (managerGroups) { const groups = JSON.parse(managerGroups); if (Array.isArray(groups)) { localStorage.setItem(STORAGE_KEY, JSON.stringify(groups)); updated = true; } } if (managerSettings) { const settings = JSON.parse(managerSettings); if (typeof settings === "object") { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); updated = true; } } if (updated) console.log("[Userscript] Synced data from manager"); return updated; } catch (error) { console.error("[Userscript] Failed to sync from manager:", error); return false; } } function trackEmojiUsage(emojiName, emojiUrl) { try { const key = `${emojiName}|${emojiUrl}`; const statsData = localStorage.getItem(USAGE_STATS_KEY); let stats = {}; if (statsData) try { stats = JSON.parse(statsData); } catch (e) { console.warn("[Userscript] Failed to parse usage stats:", e); } if (!stats[key]) stats[key] = { count: 0, lastUsed: 0 }; stats[key].count++; stats[key].lastUsed = Date.now(); localStorage.setItem(USAGE_STATS_KEY, JSON.stringify(stats)); } catch (error) { console.error("[Userscript] Failed to track emoji usage:", error); } } function getPopularEmojis(limit = 20) { try { const statsData = localStorage.getItem(USAGE_STATS_KEY); if (!statsData) return []; const stats = JSON.parse(statsData); return Object.entries(stats).map(([key, data]) => { const [name, url] = key.split("|"); return { name, url, count: data.count, lastUsed: data.lastUsed }; }).sort((a, b) => b.count - a.count).slice(0, limit); } catch (error) { console.error("[Userscript] Failed to get popular emojis:", error); return []; } } function clearEmojiUsageStats() { try { localStorage.removeItem(USAGE_STATS_KEY); console.log("[Userscript] Cleared emoji usage statistics"); } catch (error) { console.error("[Userscript] Failed to clear usage stats:", error); } } var STORAGE_KEY, SETTINGS_KEY, USAGE_STATS_KEY, DEFAULT_USER_SETTINGS; var init_userscript_storage = __esmMin((() => { init_defaultEmojiGroups_loader(); STORAGE_KEY = "emoji_extension_userscript_data"; SETTINGS_KEY = "emoji_extension_userscript_settings"; USAGE_STATS_KEY = "emoji_extension_userscript_usage_stats"; DEFAULT_USER_SETTINGS = { imageScale: 30, gridColumns: 4, outputFormat: "markdown", forceMobileMode: false, defaultGroup: "nachoneko", showSearchBar: true, enableFloatingPreview: true, enableCalloutSuggestions: true, enableBatchParseImages: true }; })); var userscriptState; var init_state = __esmMin((() => { init_userscript_storage(); userscriptState = { emojiGroups: [], settings: { ...DEFAULT_USER_SETTINGS }, emojiUsageStats: {} }; })); function createEl(tag, opts) { const el = document.createElement(tag); if (opts) { if (opts.width) el.style.width = opts.width; if (opts.height) el.style.height = opts.height; if (opts.className) el.className = opts.className; if (opts.text) el.textContent = opts.text; if (opts.placeholder && "placeholder" in el) el.placeholder = opts.placeholder; if (opts.type && "type" in el) el.type = opts.type; if (opts.value !== void 0 && "value" in el) el.value = opts.value; if (opts.style) el.style.cssText = opts.style; if (opts.src && "src" in el) el.src = opts.src; if (opts.attrs) for (const k in opts.attrs) el.setAttribute(k, opts.attrs[k]); if (opts.dataset) for (const k in opts.dataset) el.dataset[k] = opts.dataset[k]; if (opts.innerHTML) el.innerHTML = opts.innerHTML; if (opts.title) el.title = opts.title; if (opts.alt && "alt" in el) el.alt = opts.alt; if (opts.id) el.id = opts.id; if (opts.on) for (const [evt, handler] of Object.entries(opts.on)) el.addEventListener(evt, handler); } return el; } var init_createEl = __esmMin((() => {})); init_state(); init_userscript_storage(); init_createEl(); 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 = "linear-gradient(135deg, #d97706, #b45309)"; button.style.transform = "scale(1.02)"; } }); button.addEventListener("mouseleave", () => { if (!button.disabled && !button.innerHTML.includes("已处理")) { button.style.background = "linear-gradient(135deg, #f59e0b, #d97706)"; 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 = "linear-gradient(135deg, #6b7280, #4b5563)"; 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 }); } init_createEl(); init_userscript_storage(); 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(); } const ICONS = { info: { icon: "ℹ️", color: "rgba(2, 122, 255, 0.1)", svg: "" }, tip: { icon: "💡", color: "rgba(0, 191, 188, 0.1)", svg: "" }, faq: { icon: "❓", color: "rgba(236, 117, 0, 0.1)", svg: "" }, question: { icon: "🤔", color: "rgba(236, 117, 0, 0.1)", svg: "" }, note: { icon: "📝", color: "rgba(8, 109, 221, 0.1)", svg: "" }, abstract: { icon: "📋", color: "rgba(0, 191, 188, 0.1)", svg: "" }, todo: { icon: "☑️", color: "rgba(2, 122, 255, 0.1)", svg: "" }, success: { icon: "🎉", color: "rgba(68, 207, 110, 0.1)", svg: "" }, warning: { icon: "⚠️", color: "rgba(236, 117, 0, 0.1)", svg: "" }, failure: { icon: "❌", color: "rgba(233, 49, 71, 0.1)", svg: "" }, danger: { icon: "☠️", color: "rgba(233, 49, 71, 0.1)", svg: "" }, bug: { icon: "🐛", color: "rgba(233, 49, 71, 0.1)", svg: "" }, example: { icon: "🔎", color: "rgba(120, 82, 238, 0.1)", svg: "" }, quote: { icon: "💬", color: "rgba(158, 158, 158, 0.1)", svg: "" } }; const ALIASES = { summary: "abstract", tldr: "abstract", hint: "tip", check: "success", done: "success", help: "faq", caution: "warning", attention: "warning", fail: "failure", missing: "failure", error: "danger", cite: "quote" }; function getIcon(key) { return ICONS[ALIASES[key] || key] || { icon: "📝", color: "rgba(158, 158, 158, 0.1)", svg: "" }; } function ensureStyleInjected(id, css) { const style = document.createElement("style"); style.id = id; style.textContent = css; document.documentElement.appendChild(style); } var init_injectStyles = __esmMin((() => {})); init_injectStyles(); var da = document.addEventListener; var calloutKeywords = [ "note", "abstract", "summary", "tldr", "info", "todo", "tip", "hint", "success", "check", "done", "question", "help", "faq", "warning", "caution", "attention", "failure", "fail", "missing", "danger", "error", "bug", "example", "quote", "cite" ].sort(); var DEFAULT_ICON = { icon: "📝", color: "var(--secondary-low)", svg: "" }; var suggestionBox = null; var activeSuggestionIndex = 0; function createSuggestionBox() { if (suggestionBox) return; suggestionBox = document.createElement("div"); suggestionBox.id = "userscript-callout-suggestion-box"; document.body.appendChild(suggestionBox); injectStyles$1(); } function injectStyles$1() { ensureStyleInjected("userscript-callout-suggestion-styles", ` #userscript-callout-suggestion-box { position: absolute; background-color: var(--secondary); border: 1px solid #444; border-radius: 6px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); z-index: 999999; padding: 5px; display: none; font-size: 14px; max-height: 200px; overflow-y: auto; } .userscript-suggestion-item { padding: 8px 12px; cursor: pointer; color: var(--primary-high); border-radius: 4px; display: flex; align-items: center; } .userscript-suggestion-item:hover, .userscript-suggestion-item.active { background-color: var(--primary-low) !important; } `); } function hideSuggestionBox() { if (suggestionBox) suggestionBox.style.display = "none"; } function updateActiveSuggestion() { if (!suggestionBox) return; suggestionBox.querySelectorAll(".userscript-suggestion-item").forEach((it, idx) => { it.classList.toggle("active", idx === activeSuggestionIndex); if (idx === activeSuggestionIndex) it.scrollIntoView({ block: "nearest" }); }); } function applyCompletion(element, selectedKeyword) { if (element instanceof HTMLTextAreaElement) { const text = element.value; const selectionStart = element.selectionStart || 0; const textBeforeCursor = text.substring(0, selectionStart); let triggerIndex = textBeforeCursor.lastIndexOf("["); if (triggerIndex === -1) triggerIndex = textBeforeCursor.lastIndexOf("["); if (triggerIndex === -1) triggerIndex = textBeforeCursor.lastIndexOf("【"); if (triggerIndex === -1) return; const newText = `[!${selectedKeyword}]`; const textAfter = text.substring(selectionStart); element.value = textBeforeCursor.substring(0, triggerIndex) + newText + textAfter; element.selectionStart = element.selectionEnd = triggerIndex + newText.length; element.dispatchEvent(new Event("input", { bubbles: true })); } else if (element.classList.contains("ProseMirror")) { const newText = `[!${selectedKeyword}]`; try { const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) return; const range = selection.getRangeAt(0); const textBeforeCursor = range.startContainer.textContent?.substring(0, range.startOffset) || ""; let triggerIndex = textBeforeCursor.lastIndexOf("["); if (triggerIndex === -1) triggerIndex = textBeforeCursor.lastIndexOf("["); if (triggerIndex === -1) triggerIndex = textBeforeCursor.lastIndexOf("【"); if (triggerIndex === -1) return; const deleteRange = document.createRange(); deleteRange.setStart(range.startContainer, triggerIndex); deleteRange.setEnd(range.startContainer, range.startOffset); deleteRange.deleteContents(); const textNode = document.createTextNode(newText); deleteRange.insertNode(textNode); const newRange = document.createRange(); newRange.setStartAfter(textNode); newRange.collapse(true); selection.removeAllRanges(); selection.addRange(newRange); element.dispatchEvent(new Event("input", { bubbles: true })); } catch (e) { console.error("ProseMirror completion failed", e); } } } function getCursorXY(element, position) { if (element instanceof HTMLTextAreaElement) { const mirrorId = "userscript-textarea-mirror-div"; let mirror = document.getElementById(mirrorId); const rect = element.getBoundingClientRect(); if (!mirror) { mirror = document.createElement("div"); mirror.id = mirrorId; document.body.appendChild(mirror); } const style = window.getComputedStyle(element); const props = [ "boxSizing", "fontFamily", "fontSize", "fontWeight", "letterSpacing", "lineHeight", "textTransform", "textAlign", "direction", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft", "borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth" ]; const ms = mirror.style; props.forEach((p) => { ms[p] = style.getPropertyValue(p); }); ms.position = "absolute"; ms.left = `${rect.left + window.scrollX}px`; ms.top = `${rect.top + window.scrollY}px`; ms.width = `${rect.width}px`; ms.height = `${rect.height}px`; ms.overflow = "hidden"; ms.visibility = "hidden"; ms.whiteSpace = "pre-wrap"; ms.wordWrap = "break-word"; ms.boxSizing = style.getPropertyValue("box-sizing") || "border-box"; const cursorPosition = position !== void 0 ? position : element.selectionEnd; mirror.textContent = element.value.substring(0, cursorPosition); const span = document.createElement("span"); span.textContent = "​"; mirror.appendChild(span); const spanRect = span.getBoundingClientRect(); const offsetX = span.offsetLeft - element.scrollLeft; const offsetY = span.offsetTop - element.scrollTop; return { x: spanRect.left + window.scrollX, y: spanRect.top + window.scrollY, bottom: spanRect.bottom + window.scrollY, offsetX, offsetY }; } else { const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) { const rect$1 = element.getBoundingClientRect(); return { x: rect$1.left + window.scrollX, y: rect$1.top + window.scrollY, bottom: rect$1.bottom + window.scrollY, offsetX: 0, offsetY: 0 }; } const rect = selection.getRangeAt(0).getBoundingClientRect(); return { x: rect.left + window.scrollX, y: rect.top + window.scrollY, bottom: rect.bottom + window.scrollY, offsetX: 0, offsetY: 0 }; } } function updateSuggestionBox(element, matches, triggerIndex) { if (!suggestionBox || matches.length === 0) { hideSuggestionBox(); return; } suggestionBox.innerHTML = matches.map((keyword, index) => { const iconData = getIcon(keyword) || DEFAULT_ICON; const backgroundColor = iconData.color || "transparent"; const iconColor = iconData.color ? iconData.color.replace("rgba", "rgb").replace(/, [0-9.]+\)/, ")") : "var(--primary-medium)"; return `\n
\n ${(iconData.svg || DEFAULT_ICON.svg).replace("${keyword}\n
`; }).join(""); suggestionBox.querySelectorAll(".userscript-suggestion-item").forEach((item) => { item.addEventListener("mousedown", (e) => { e.preventDefault(); const idx = item.dataset.key; if (!idx) return; applyCompletion(element, idx); hideSuggestionBox(); }); }); const cursorPos = getCursorXY(element, triggerIndex); const margin = 6; const prevVisibility = suggestionBox.style.visibility; suggestionBox.style.display = "block"; suggestionBox.style.visibility = "hidden"; const boxRect = suggestionBox.getBoundingClientRect(); const spaceBelow = window.innerHeight - (cursorPos.bottom - window.scrollY); const left = cursorPos.x; let top = cursorPos.y + margin; if (spaceBelow < boxRect.height + margin) top = cursorPos.y - boxRect.height - margin; const cursorViewportX = cursorPos.x - window.scrollX; const viewportWidth = window.innerWidth; const spaceRight = viewportWidth - cursorViewportX; const spaceLeft = cursorViewportX; let finalLeft = left; if (spaceRight < boxRect.width + margin && spaceLeft >= boxRect.width + margin) finalLeft = cursorPos.x - boxRect.width; const minLeft = window.scrollX + 0; const maxLeft = window.scrollX + viewportWidth - boxRect.width - margin; if (finalLeft < minLeft) finalLeft = minLeft; if (finalLeft > maxLeft) finalLeft = maxLeft; suggestionBox.style.left = `${finalLeft}px`; suggestionBox.style.top = `${top}px`; suggestionBox.style.visibility = prevVisibility || ""; suggestionBox.style.display = "block"; activeSuggestionIndex = 0; updateActiveSuggestion(); } function handleInput(event) { const target = event.target; if (!target) return; if (target instanceof HTMLTextAreaElement) { const textarea = target; const text = textarea.value; const selectionStart = textarea.selectionStart || 0; const match = text.substring(0, selectionStart).match(/(?:\[|[|【])(?:!|!)?([a-z]*)$/i); if (match) { const keyword = match[1].toLowerCase(); const filtered = calloutKeywords.filter((k) => k.startsWith(keyword)); const triggerIndex = selectionStart - match[0].length; if (filtered.length > 0) updateSuggestionBox(textarea, filtered, triggerIndex); else hideSuggestionBox(); } else hideSuggestionBox(); } else if (target.classList?.contains("ProseMirror")) { const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) { hideSuggestionBox(); return; } const range = selection.getRangeAt(0); const match = (range.startContainer.textContent?.substring(0, range.startOffset) || "").match(/(?:\[|[|【])(?:!|!)?([a-z]*)$/i); if (match) { const keyword = match[1].toLowerCase(); const filtered = calloutKeywords.filter((k) => k.startsWith(keyword)); const triggerIndex = range.startOffset - match[0].length; if (filtered.length > 0) updateSuggestionBox(target, filtered, triggerIndex); else hideSuggestionBox(); } else hideSuggestionBox(); } } function handleKeydown$1(event) { if (!suggestionBox || suggestionBox.style.display === "none") return; const items = suggestionBox.querySelectorAll(".userscript-suggestion-item"); if (items.length === 0) return; if ([ "ArrowDown", "ArrowUp", "Tab", "Enter", "Escape" ].includes(event.key)) { event.preventDefault(); event.stopPropagation(); } switch (event.key) { case "ArrowDown": activeSuggestionIndex = (activeSuggestionIndex + 1) % items.length; updateActiveSuggestion(); break; case "ArrowUp": activeSuggestionIndex = (activeSuggestionIndex - 1 + items.length) % items.length; updateActiveSuggestion(); break; case "Tab": case "Enter": { const selectedKey = items[activeSuggestionIndex]?.dataset.key; if (selectedKey) { const focused = document.activeElement; if (focused) { if (focused instanceof HTMLTextAreaElement) applyCompletion(focused, selectedKey); else if (focused.classList?.contains("ProseMirror")) applyCompletion(focused, selectedKey); } } hideSuggestionBox(); break; } case "Escape": hideSuggestionBox(); break; } } function initCalloutSuggestionsUserscript() { try { createSuggestionBox(); da("input", handleInput, true); da("keydown", handleKeydown$1, true); da("click", (e) => { if (e.target?.tagName !== "TEXTAREA" && !suggestionBox?.contains(e.target)) hideSuggestionBox(); }); } catch (e) { console.error("initCalloutSuggestionsUserscript failed", e); } } init_createEl(); init_injectStyles(); ensureStyleInjected("raw-preview-styles", ` .raw-preview-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 2147483647; } .raw-preview-modal { width: 80%; height: 80%; background: var(--color-bg, #fff); border-radius: 8px; box-shadow: 0 8px 32px rgba(0,0,0,0.5); display: flex; flex-direction: column; overflow: hidden; } .raw-preview-header { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: var(--secondary); border-bottom: 1px solid rgba(0,0,0,0.06); } .raw-preview-title { flex: 1; font-weight: 600; } .raw-preview-ctrls button { margin-left: 6px; } .raw-preview-iframe { border: none; width: 100%; height: 100%; flex: 1 1 auto; background: var(--secondary); color: var(--title-color); } .raw-preview-small-btn { display: inline-flex; align-items: center; justify-content: center; padding: 2px 8px; margin-left: 6px; font-size: 12px; border-radius: 4px; border: 1px solid rgba(0,0,0,0.08); background: var(--d-button-primary-bg-color); cursor: pointer; color: var(--d-button-primary-text-color); } .raw-preview-small-btn.md { background: linear-gradient(90deg,#fffbe6,#f0f7ff); border-color: rgba(3,102,214,0.12); } .raw-preview-small-btn.json { background: var(--d-button-default-bg-color); border-color: rgba(0,128,96,0.12); color: var(--d-button-default-text-color); } `); var overlay = null; var iframeEl = null; var currentTopicId = null; var currentPage = 1; var renderMode = "iframe"; var currentTopicSlug = null; var jsonScrollAttached = false; var jsonIsLoading = false; var jsonReachedEnd = false; function rawUrl(topicId, page) { return new URL(`/raw/${topicId}?page=${page}`, window.location.origin).toString(); } function jsonUrl(topicId, page, slug) { const usedSlug = slug || currentTopicSlug || "topic"; return new URL(`/t/${usedSlug}/${topicId}.json?page=${page}`, window.location.origin).toString(); } function createOverlay(topicId, startPage = 1, mode = "iframe", slug) { if (overlay) return; currentTopicId = topicId; currentPage = startPage; renderMode = mode; currentTopicSlug = slug || null; overlay = createEl("div", { className: "raw-preview-overlay" }); const modal = createEl("div", { className: "raw-preview-modal" }); const header = createEl("div", { className: "raw-preview-header" }); const title = createEl("div", { className: "raw-preview-title", text: `话题预览 ${topicId}` }); const ctrls = createEl("div", { className: "raw-preview-ctrls" }); const modeLabel = createEl("span", { text: mode === "markdown" ? "模式:Markdown" : "模式:原始" }); const prevBtn = createEl("button", { className: "raw-preview-small-btn", text: "◀ 上一页" }); const nextBtn = createEl("button", { className: "raw-preview-small-btn", text: "下一页 ▶" }); const closeBtn = createEl("button", { className: "raw-preview-small-btn", text: "关闭 ✖" }); prevBtn.addEventListener("click", () => { if (!currentTopicId) return; if (currentPage > 1) { currentPage -= 1; updateIframeSrc(); } }); nextBtn.addEventListener("click", () => { if (!currentTopicId) return; currentPage += 1; updateIframeSrc(); }); closeBtn.addEventListener("click", () => { removeOverlay(); }); ctrls.appendChild(modeLabel); ctrls.appendChild(prevBtn); ctrls.appendChild(nextBtn); ctrls.appendChild(closeBtn); header.appendChild(title); header.appendChild(ctrls); iframeEl = createEl("iframe", { className: "raw-preview-iframe", attrs: { sandbox: "allow-same-origin allow-scripts" } }); modal.appendChild(header); modal.appendChild(iframeEl); overlay.appendChild(modal); overlay.addEventListener("click", (e) => { if (e.target === overlay) removeOverlay(); }); window.addEventListener("keydown", handleKeydown); document.body.appendChild(overlay); if (renderMode === "iframe") iframeEl.src = rawUrl(topicId, currentPage); else if (renderMode === "markdown") fetchAndRenderMarkdown(topicId, currentPage); else if (renderMode === "json") fetchAndRenderJson(topicId, currentPage, currentTopicSlug || void 0).then(() => { attachJsonAutoPager(); }); } async function updateIframeSrc() { if (!iframeEl || !currentTopicId) return; if (renderMode === "iframe") iframeEl.src = rawUrl(currentTopicId, currentPage); else if (renderMode === "markdown") fetchAndRenderMarkdown(currentTopicId, currentPage); else if (renderMode === "json") try { const doc = getIframeDoc(); const targetId = `json-page-${currentPage}`; if (doc.getElementById(targetId)) { scrollToJsonPage(currentPage); return; } const nodes = Array.from(doc.querySelectorAll("[id^=\"json-page-\"]")); let maxLoaded = 0; for (const n of nodes) { const m = n.id.match(/json-page-(\d+)/); if (m) maxLoaded = Math.max(maxLoaded, parseInt(m[1], 10)); } let start = Math.max(1, maxLoaded + 1); for (let p = start; p <= currentPage; p++) if (await fetchAndRenderJson(currentTopicId, p, currentTopicSlug || void 0) === 0) break; scrollToJsonPage(currentPage); } catch { fetchAndRenderJson(currentTopicId, currentPage, currentTopicSlug || void 0); } } function removeOverlay() { if (!overlay) return; window.removeEventListener("keydown", handleKeydown); overlay.remove(); overlay = null; iframeEl = null; currentTopicId = null; currentPage = 1; jsonScrollAttached = false; jsonIsLoading = false; jsonReachedEnd = false; } function handleKeydown(e) { if (!overlay) return; if (e.key === "ArrowLeft") { if (currentPage > 1) { currentPage -= 1; updateIframeSrc(); } } if (e.key === "ArrowRight") { currentPage += 1; updateIframeSrc(); } if (e.key === "Escape") removeOverlay(); } function createTriggerButtonFor(mode) { const btn = createEl("button", { className: `raw-preview-small-btn ${mode === "markdown" ? "md" : mode === "json" ? "json" : "iframe"}`, text: mode === "json" ? "预览 (JSON)" : mode === "markdown" ? "预览 (MD)" : "预览" }); btn.dataset.previewMode = mode; return btn; } function escapeHtml(str) { return str.replace(/&/g, "&").replace(//g, ">"); } function simpleMarkdownToHtml(md) { const lines = md.replace(/\r\n/g, "\n").split("\n"); let inCode = false; const out = []; let listType = null; for (let i = 0; i < lines.length; i++) { let line = lines[i]; if (line.match(/^```\s*(\S*)/)) { if (!inCode) { inCode = true; out.push("
");
				} else {
					inCode = false;
					out.push("
"); } continue; } if (inCode) { out.push(escapeHtml(line) + "\n"); continue; } const h = line.match(/^(#{1,6})\s+(.*)/); if (h) { out.push(`${escapeHtml(h[2])}`); continue; } const ol = line.match(/^\s*\d+\.\s+(.*)/); if (ol) { if (listType !== "ol") { if (listType === "ul") out.push(""); listType = "ol"; out.push("
    "); } out.push(`
  1. ${inlineFormat(ol[1])}
  2. `); continue; } const ul = line.match(/^\s*[-*]\s+(.*)/); if (ul) { if (listType !== "ul") { if (listType === "ol") out.push("
"); listType = "ul"; out.push(""); listType = null; } else if (listType === "ol") { out.push(""); listType = null; } out.push("

"); continue; } out.push(`

${inlineFormat(line)}

`); } if (listType === "ul") out.push(""); if (listType === "ol") out.push(""); return out.join("\n"); } function inlineFormat(text) { let t = escapeHtml(text); t = t.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_m, altRaw, urlRaw) => { const altParts = (altRaw || "").split("|"); const alt = escapeHtml(altParts[0] || ""); let widthAttr = ""; let heightAttr = ""; if (altParts[1]) { const dim = altParts[1].match(/(\d+)x(\d+)/); if (dim) { widthAttr = ` width="${dim[1]}"`; heightAttr = ` height="${dim[2]}"`; } } const url = String(urlRaw || ""); if (url.startsWith("upload://")) { const filename = url.replace(/^upload:\/\//, ""); return `${alt}`; } return `${alt}`; }); t = t.replace(/`([^`]+)`/g, "$1"); t = t.replace(/~~([\s\S]+?)~~/g, "$1"); t = t.replace(/\*\*([^*]+)\*\*/g, "$1"); t = t.replace(/\*([^*]+)\*/g, "$1"); t = t.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1"); return t; } async function fetchAndRenderMarkdown(topicId, page) { if (!iframeEl) return; const url = rawUrl(topicId, page); try { const res = await fetch(url, { credentials: "include" }); if (!res.ok) throw new Error("fetch failed " + res.status); const text = await res.text(); let html; try { const md = await loadMarkdownIt(); if (md) try { const parser = md({ html: true, linkify: true }); try { parser.use(uploadUrlPlugin); } catch (e) {} html = parser.render(text); } catch (e) { console.warn("[rawPreview] markdown-it render failed, falling back", e); html = simpleMarkdownToHtml(text); } else html = simpleMarkdownToHtml(text); } catch (e) { console.warn("[rawPreview] loadMarkdownIt failed, falling back to simple renderer", e); html = simpleMarkdownToHtml(text); } const doc = iframeEl.contentDocument || iframeEl.contentWindow?.document; if (!doc) throw new Error("iframe document unavailable"); const css = ``; doc.open(); doc.write(`${html}`); doc.close(); } catch (err) { console.warn("[rawPreview] fetchAndRenderMarkdown failed", err); iframeEl.src = url; } } function getIframeDoc() { const doc = iframeEl.contentDocument || iframeEl.contentWindow?.document; if (!doc) throw new Error("iframe document unavailable"); return doc; } function ensureJsonSkeleton() { const doc = getIframeDoc(); const existing = doc.getElementById("json-container"); if (existing) return existing; const css = ``; const baseHref = window.location.origin; doc.open(); doc.write(`
`); doc.close(); return doc.getElementById("json-container"); } async function fetchAndRenderJson(topicId, page, slug) { if (!iframeEl) return 0; const url = jsonUrl(topicId, page, slug); try { const res = await fetch(url, { credentials: "include" }); if (!res.ok) throw new Error("fetch failed " + res.status); const data = await res.json(); const posts = data && data.post_stream && Array.isArray(data.post_stream.posts) ? data.post_stream.posts : []; const parts = []; for (const p of posts) { const cooked = typeof p.cooked === "string" ? p.cooked : ""; const header = `
#${p.post_number || ""} @${p.username || ""} ${p.created_at || ""}
`; parts.push(`
${header}
${cooked}
`); } const container = ensureJsonSkeleton(); const wrapper = `
${parts.join("\n")}
`; container.insertAdjacentHTML("beforeend", wrapper); return posts.length; } catch (err) { console.warn("[rawPreview] fetchAndRenderJson failed", err); iframeEl.src = url; return 0; } } function scrollToJsonPage(page) { try { if (!iframeEl) return; const el = getIframeDoc().getElementById(`json-page-${page}`); if (!el) return; const top = el.offsetTop; const win = iframeEl.contentWindow || iframeEl.contentDocument?.defaultView; if (win) win.scrollTo({ top, behavior: "smooth" }); } catch {} } function attachJsonAutoPager() { if (jsonScrollAttached || !iframeEl || renderMode !== "json") return; try { const win = iframeEl.contentWindow || iframeEl.contentDocument?.defaultView; if (!win) return; const onScroll = async () => { if (jsonIsLoading || jsonReachedEnd) return; const doc = win.document; const scrollTop = win.scrollY || doc.documentElement.scrollTop || doc.body.scrollTop; const ih = win.innerHeight; const sh = doc.documentElement.scrollHeight || doc.body.scrollHeight; if (scrollTop + ih >= sh - 200) { jsonIsLoading = true; try { const nextPage = currentPage + 1; if (await fetchAndRenderJson(currentTopicId, nextPage, currentTopicSlug || void 0) > 0) currentPage = nextPage; else jsonReachedEnd = true; } catch (e) { jsonReachedEnd = true; } finally { jsonIsLoading = false; } } }; win.addEventListener("scroll", onScroll, { passive: true }); jsonScrollAttached = true; } catch (e) {} } function loadMarkdownIt() { return new Promise((resolve, reject) => { try { const win = window; if (win && win.markdownit) return resolve(win.markdownit); const src = "https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"; if (document.querySelector(`script[src="${src}"]`)) { const check = () => { if (window.markdownit) return resolve(window.markdownit); setTimeout(check, 50); }; check(); return; } const s = document.createElement("script"); s.src = src; s.async = true; s.onload = () => { const md = window.markdownit; if (md) resolve(md); else reject(/* @__PURE__ */ new Error("markdownit not found after script load")); }; s.onerror = () => reject(/* @__PURE__ */ new Error("failed to load markdown-it script")); document.head.appendChild(s); } catch (e) { reject(e); } }); } function uploadUrlPlugin(mdLib) { const defaultRender = mdLib.renderer.rules.image || function(tokens, idx, options, _env, self) { return self.renderToken(tokens, idx, options); }; mdLib.renderer.rules.image = function(tokens, idx, options, env, self) { try { const token = tokens[idx]; if (token.attrs) { const srcIdx = token.attrIndex("src"); if (srcIdx >= 0) { const srcVal = token.attrs[srcIdx][1]; if (typeof srcVal === "string" && srcVal.startsWith("upload://")) { const filename = srcVal.replace(/^upload:\/\//, ""); token.attrs[srcIdx][1] = `${window.location.origin}/uploads/short-url/${filename}`; } } const altIdx = token.attrIndex("alt"); if (altIdx >= 0) { const altVal = token.attrs[altIdx][1] || ""; const parts = String(altVal).split("|"); if (parts.length > 1) { token.attrs[altIdx][1] = parts[0]; const dim = parts[1].match(/(\d+)x(\d+)/); if (dim) { token.attrSet("width", dim[1]); token.attrSet("height", dim[2]); } } } } } catch (e) { console.warn("[rawPreview] uploadUrlPlugin error", e); } return defaultRender(tokens, idx, options, env, self); }; } function injectIntoTopicList() { document.querySelectorAll("tr[data-topic-id]").forEach((row) => { try { const topicId = row.dataset.topicId; if (!topicId) return; if (row.querySelector(".raw-preview-list-trigger")) return; const titleLink = row.querySelector("a.title, a.raw-topic-link, a.raw-link"); const btn = createTriggerButtonFor("iframe"); btn.classList.add("raw-preview-list-trigger"); btn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); createOverlay(topicId, 1, "iframe"); }); const jsonBtn = createTriggerButtonFor("json"); jsonBtn.classList.add("raw-preview-list-trigger-json"); jsonBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); let slug; const m = (titleLink?.getAttribute("href") || "").match(/\/t\/([^/]+)\/(\d+)/); if (m) slug = m[1]; createOverlay(topicId, 1, "json", slug); }); if (titleLink && titleLink.parentElement) { titleLink.parentElement.appendChild(btn); titleLink.parentElement.appendChild(jsonBtn); } else { row.appendChild(btn); row.appendChild(jsonBtn); } } catch (err) { console.warn("[rawPreview] injectIntoTopicList error", err); } }); } function initRawPreview() { try { injectIntoTopicList(); } catch (e) { console.warn("[rawPreview] initial injection failed", e); } new MutationObserver(() => { injectIntoTopicList(); }).observe(document.body, { childList: true, subtree: true }); } 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); } function ensureHoverPreview() { if (_sharedPreview && document.body.contains(_sharedPreview)) return _sharedPreview; _sharedPreview = createEl("div", { className: "emoji-picker-hover-preview", style: "position:fixed;pointer-events:none;display:none;z-index:1000002;max-width:300px;max-height:300px;overflow:hidden;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,0.25);background:transparent;padding:6px;" }); const img = createEl("img", { className: "emoji-picker-hover-img", style: "display:block;max-width:100%;max-height:220px;object-fit:contain;" }); const label = createEl("div", { className: "emoji-picker-hover-label", style: "font-size:12px;color:var(--primary);margin-top:6px;text-align:center;" }); _sharedPreview.appendChild(img); _sharedPreview.appendChild(label); document.body.appendChild(_sharedPreview); return _sharedPreview; } var _sharedPreview; var init_hoverPreview = __esmMin((() => { init_createEl(); _sharedPreview = null; })); function injectGlobalThemeStyles() { if (themeStylesInjected || typeof document === "undefined") return; themeStylesInjected = true; document.head.appendChild(createEl("style", { id: "emoji-extension-theme-globals", text: ` /* Global CSS variables for emoji extension theme support */ :root { /* Light theme (default) */ --emoji-modal-bg: #ffffff; --emoji-modal-text: #333333; --emoji-modal-border: #dddddd; --emoji-modal-input-bg: #ffffff; --emoji-modal-label: #555555; --emoji-modal-button-bg: #f5f5f5; --emoji-modal-primary-bg: #1890ff; --emoji-preview-bg: #ffffff; --emoji-preview-text: #222222; --emoji-preview-border: rgba(0,0,0,0.08); --emoji-button-gradient-start: #667eea; --emoji-button-gradient-end: #764ba2; --emoji-button-shadow: rgba(0, 0, 0, 0.15); --emoji-button-hover-shadow: rgba(0, 0, 0, 0.2); } /* Dark theme */ @media (prefers-color-scheme: dark) { :root { --emoji-modal-bg: #2d2d2d; --emoji-modal-text: #e6e6e6; --emoji-modal-border: #444444; --emoji-modal-input-bg: #3a3a3a; --emoji-modal-label: #cccccc; --emoji-modal-button-bg: #444444; --emoji-modal-primary-bg: #1677ff; --emoji-preview-bg: rgba(32,33,36,0.94); --emoji-preview-text: #e6e6e6; --emoji-preview-border: rgba(255,255,255,0.12); --emoji-button-gradient-start: #4a5568; --emoji-button-gradient-end: #2d3748; --emoji-button-shadow: rgba(0, 0, 0, 0.3); --emoji-button-hover-shadow: rgba(0, 0, 0, 0.4); } } ` })); } var themeStylesInjected; var init_themeSupport = __esmMin((() => { init_createEl(); themeStylesInjected = false; })); init_hoverPreview(); init_themeSupport(); init_injectStyles(); 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; } 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; ensureStyleInjected("emoji-manager-styles", ` /* Modal backdrop */ .emoji-manager-wrapper { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 999999; display: flex; align-items: center; justify-content: center; } /* Main modal panel */ .emoji-manager-panel { border-radius: 8px; width: 90%; height: 95%; display: grid; grid-template-columns: 300px 1fr; grid-template-rows: 1fr auto; overflow: hidden; box-shadow: 0 10px 40px rgba(0,0,0,0.3); } /* Left panel - groups list */ .emoji-manager-left { background: var(--primary-very-low) border-right: 1px solid #e9ecef; display: flex; flex-direction: column; overflow: hidden; } .emoji-manager-left-header { display: flex; align-items: center; padding: 16px; background: var(--primary-low); } .emoji-manager-addgroup-row { display: flex; padding: 12px; } .emoji-manager-groups-list { background: var(--primary-very-low); flex: 1; overflow-y: auto; padding: 8px; } .emoji-manager-groups-list > div { margin-bottom: 4px; transition: background-color 0.2s; } .emoji-manager-groups-list > div:hover { background: var(--d-selected); } .emoji-manager-groups-list > div:focus { outline: none; box-shadow: inset 0 0 0 2px #007bff; background: var(--d-selected); } /* Right panel - emoji display and editing */ .emoji-manager-right { background: var(--primary-low); display: flex; flex-direction: column; overflow: hidden; } .emoji-manager-right-header { display: flex; align-items: center; justify-content: space-between; padding: 16px; } .emoji-manager-right-main { flex: 1; overflow-y: auto; } .emoji-manager-emojis { display: grid; grid-template-columns: repeat(auto-fill, minmax(25%, 1fr)); gap: 12px; } .emoji-manager-card { display: flex; flex-direction: column; align-items: center; padding: 12px; background: var(--primary-medium); } .emoji-manager-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); } .emoji-manager-card-img { max-width: 90%; max-height: 100%; /* allow tall images but cap at viewport height */ object-fit: contain; border-radius: 6px; background: white; } .emoji-manager-card-name { font-size: 12px; color: var(--primary); text-align: center; width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-weight: 500; } .emoji-manager-card-actions { display: flex; gap: 6px; } /* Add emoji form */ .emoji-manager-add-emoji-form { padding: 16px; background: var(--primary-very-low) border-top: 1px solid #e9ecef; display: flex; gap: 8px; align-items: center; } /* Footer */ .emoji-manager-footer { grid-column: 1 / -1; display: flex; justify-content: space-between; padding: 16px; background: var(--primary-very-low); } /* Editor panel - popup modal */ .emoji-manager-editor-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: var( --primary-medium ); padding: 2%; z-index: 1000000; } .emoji-manager-editor-preview { max-width: 100%; max-height: 40vh; } /* Hover preview (moved from inline styles) */ .emoji-manager-hover-preview { position: fixed; pointer-events: none; z-index: 1000002; display: none; max-width: 60%; max-height: 60%; border: 1px solid rgba(0,0,0,0.1); object-fit: contain; background: var(--primary); padding: 4px; border-radius: 6px; box-shadow: 0 6px 18px rgba(0,0,0,0.12); } /* Form styling */ .form-control { width: 100%; display: flex; } .btn { padding: 8px 16px; border: 1px solid transparent; border-radius: 4px; font-size: 14px; cursor: pointer; transition: all 0.2s; } .btn-sm { padding: 4px 8px; font-size: 12px; } `); } var __managerStylesInjected; var init_styles = __esmMin((() => { init_injectStyles(); __managerStylesInjected = false; })); function showTemporaryMessage(message, duration = 2e3) { const messageEl = createEl("div", { style: ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: var(--emoji-modal-primary-bg); color: white; padding: 12px 24px; border-radius: 6px; z-index: 9999999; font-size: 14px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); animation: fadeInOut 2s ease-in-out; `, text: message }); if (!document.querySelector("#tempMessageStyles")) { const style = createEl("style", { id: "tempMessageStyles", text: ` @keyframes fadeInOut { 0%, 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.9); } 20%, 80% { opacity: 1; transform: translate(-50%, -50%) scale(1); } } ` }); document.head.appendChild(style); } document.body.appendChild(messageEl); setTimeout(() => { try { messageEl.remove(); } catch {} }, duration); } var init_tempMessage = __esmMin((() => { init_createEl(); })); function insertIntoEditor(text) { const chatComposer = document.querySelector("textarea#channel-composer.chat-composer__input"); if (chatComposer) { const start = chatComposer.selectionStart ?? 0; const end = chatComposer.selectionEnd ?? start; const value = chatComposer.value; chatComposer.value = value.slice(0, start) + text + value.slice(end); const pos = start + text.length; if ("setSelectionRange" in chatComposer) try { chatComposer.setSelectionRange(pos, pos); } catch (e) {} chatComposer.dispatchEvent(new Event("input", { bubbles: true })); return; } const active = document.activeElement; const isTextarea = (el) => !!el && el.tagName === "TEXTAREA"; if (isTextarea(active)) { const textarea = active; const start = textarea.selectionStart ?? 0; const end = textarea.selectionEnd ?? start; const value = textarea.value; textarea.value = value.slice(0, start) + text + value.slice(end); const pos = start + text.length; if ("setSelectionRange" in textarea) try { textarea.setSelectionRange(pos, pos); } catch (e) {} textarea.dispatchEvent(new Event("input", { bubbles: true })); return; } if (active && active.isContentEditable) { const sel = window.getSelection(); if (!sel) return; const range = sel.getRangeAt(0); range.deleteContents(); const node = document.createTextNode(text); range.insertNode(node); range.setStartAfter(node); range.setEndAfter(node); sel.removeAllRanges(); sel.addRange(range); active.dispatchEvent(new Event("input", { bubbles: true })); return; } const fallback = document.querySelector("textarea"); if (fallback) { fallback.focus(); const start = fallback.selectionStart ?? fallback.value.length; const end = fallback.selectionEnd ?? start; const value = fallback.value; fallback.value = value.slice(0, start) + text + value.slice(end); const pos = start + text.length; if ("setSelectionRange" in fallback) try { fallback.setSelectionRange(pos, pos); } catch (e) {} fallback.dispatchEvent(new Event("input", { bubbles: true })); } } function createModalElement(options) { const modal = createEl("div", { style: ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 999999; display: flex; align-items: center; justify-content: center; `, className: options.className }); const content = createEl("div", { style: ` background: var(--secondary); color: var(--emoji-modal-text); border: 1px solid var(--emoji-modal-border); border-radius: 8px; padding: 24px; max-width: 90%; max-height: 90%; overflow-y: auto; position: relative; ` }); if (options.title) { const titleElement = createEl("div", { style: ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; `, innerHTML: `

${options.title}

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

当前分组信息

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

导出分组表情

导入分组表情

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

设置

高级功能
` })); document.body.appendChild(modal); const content = modal.querySelector("div:last-child"); const scaleSlider = content.querySelector("#scaleSlider"); const scaleValue = content.querySelector("#scaleValue"); scaleSlider?.addEventListener("input", () => { if (scaleValue) scaleValue.textContent = scaleSlider.value + "%"; }); content.querySelector("#resetSettings")?.addEventListener("click", async () => { if (confirm("确定要重置所有设置吗?")) { userscriptState.settings = { ...DEFAULT_USER_SETTINGS }; modal.remove(); } }); content.querySelector("#saveSettings")?.addEventListener("click", () => { userscriptState.settings.imageScale = parseInt(scaleSlider?.value || "30"); const outputFormat = content.querySelector("input[name=\"outputFormat\"]:checked"); if (outputFormat) userscriptState.settings.outputFormat = outputFormat.value; const showSearchBar = content.querySelector("#showSearchBar"); if (showSearchBar) userscriptState.settings.showSearchBar = showSearchBar.checked; const enableFloatingPreview = content.querySelector("#enableFloatingPreview"); if (enableFloatingPreview) userscriptState.settings.enableFloatingPreview = enableFloatingPreview.checked; const enableCalloutEl = content.querySelector("#enableCalloutSuggestions"); if (enableCalloutEl) userscriptState.settings.enableCalloutSuggestions = !!enableCalloutEl.checked; const enableBatchEl = content.querySelector("#enableBatchParseImages"); if (enableBatchEl) userscriptState.settings.enableBatchParseImages = !!enableBatchEl.checked; const forceMobileEl = content.querySelector("#forceMobileMode"); if (forceMobileEl) userscriptState.settings.forceMobileMode = !!forceMobileEl.checked; saveDataToLocalStorage({ settings: userscriptState.settings }); try { const remoteInput = content.querySelector("#remoteConfigUrl"); if (remoteInput && remoteInput.value.trim()) localStorage.setItem("emoji_extension_remote_config_url", remoteInput.value.trim()); } catch (e) {} alert("设置已保存"); modal.remove(); }); content.querySelector("#openGroupEditor")?.addEventListener("click", () => { modal.remove(); showGroupEditorModal(); }); content.querySelector("#openPopularEmojis")?.addEventListener("click", () => { modal.remove(); showPopularEmojisModal(); }); content.querySelector("#openImportExport")?.addEventListener("click", () => { modal.remove(); showImportExportModal(); }); } var init_settings = __esmMin((() => { init_state(); init_userscript_storage(); init_createEl(); init_themeSupport(); init_groupEditor(); init_popularEmojis(); init_importExport(); })); init_state(); init_userscript_storage(); init_createEl(); init_hoverPreview(); 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 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", on: { 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", on: { click: async () => { try { const { showSettingsModal: showSettingsModal$1 } = await __vitePreload(async () => { const { showSettingsModal: showSettingsModal$2 } = await Promise.resolve().then(() => (init_settings(), settings_exports)); return { showSettingsModal: showSettingsModal$2 }; }, void 0); showSettingsModal$1(); } catch (e) { console.error("[Userscript] Failed to load settings module:", e); } } } }); 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", 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 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: "管理表情 - 点击打开完整管理界面", on: { 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: "设置", on: { click: async () => { try { const { showSettingsModal: showSettingsModal$1 } = await __vitePreload(async () => { const { showSettingsModal: showSettingsModal$2 } = await Promise.resolve().then(() => (init_settings(), settings_exports)); return { showSettingsModal: showSettingsModal$2 }; }, void 0); showSettingsModal$1(); } catch (e) { console.error("[Userscript] Failed to load settings module:", e); } } } }); sectionsNav.appendChild(settingsButton); const scrollableContent = createEl("div", { className: "emoji-picker__scrollable-content" }); const sections = createEl("div", { className: "emoji-picker__sections", attrs: { role: "button" } }); groups.forEach((group, index) => { if (!group?.emojis?.length) return; const navButton = createEl("button", { className: `btn no-text btn-flat emoji-picker__section-btn ${index === 0 ? "active" : ""}`, attrs: { tabindex: "-1", "data-section": group.id }, type: "button" }); const iconVal = group.icon || "📁"; if (isImageUrl(iconVal)) { const img = createEl("img", { src: iconVal, alt: group.name || "", className: "emoji-group-icon", style: "width: 18px; height: 18px; object-fit: contain;" }); navButton.appendChild(img); } else navButton.textContent = String(iconVal); navButton.title = group.name; navButton.addEventListener("click", () => { sectionsNav.querySelectorAll(".emoji-picker__section-btn").forEach((btn) => btn.classList.remove("active")); navButton.classList.add("active"); const target = sections.querySelector(`[data-section="${group.id}"]`); if (target) target.scrollIntoView({ behavior: "smooth", block: "start" }); }); sectionsNav.appendChild(navButton); const section = createEl("div", { className: "emoji-picker__section", attrs: { "data-section": group.id, role: "region", "aria-label": group.name } }); const titleContainer = createEl("div", { className: "emoji-picker__section-title-container" }); const title = createEl("h2", { className: "emoji-picker__section-title", text: group.name }); titleContainer.appendChild(title); const sectionEmojis = createEl("div", { className: "emoji-picker__section-emojis" }); let added = 0; group.emojis.forEach((emoji) => { if (!emoji || typeof emoji !== "object" || !emoji.url || !emoji.name) return; const img = createEl("img", { width: "32px", height: "32px", className: "emoji", src: emoji.url, alt: emoji.name, title: `:${emoji.name}:`, attrs: { "data-emoji": emoji.name, tabindex: "0", loading: "lazy" } }); (function bindHover(imgEl, emo) { if (!userscriptState.settings?.enableFloatingPreview) return; const preview = ensureHoverPreview(); const previewImg = preview.querySelector("img"); const previewLabel = preview.querySelector(".emoji-picker-hover-label"); let fadeTimer = null; function onEnter(e) { previewImg.src = emo.url; previewLabel.textContent = emo.name || ""; preview.style.display = "block"; preview.style.opacity = "1"; preview.style.transition = "opacity 0.12s ease, transform 0.12s ease"; if (fadeTimer) { clearTimeout(fadeTimer); fadeTimer = null; } fadeTimer = window.setTimeout(() => { preview.style.opacity = "0"; setTimeout(() => { if (preview.style.opacity === "0") preview.style.display = "none"; }, 300); }, 5e3); move(e); } function move(e) { const pad = 12; const vw = window.innerWidth; const vh = window.innerHeight; const rect = preview.getBoundingClientRect(); let left = e.clientX + pad; let top = e.clientY + pad; if (left + rect.width > vw) left = e.clientX - rect.width - pad; if (top + rect.height > vh) top = e.clientY - rect.height - pad; preview.style.left = left + "px"; preview.style.top = top + "px"; } function onLeave() { if (fadeTimer) { clearTimeout(fadeTimer); fadeTimer = null; } preview.style.display = "none"; } imgEl.addEventListener("mouseenter", onEnter); imgEl.addEventListener("mousemove", move); imgEl.addEventListener("mouseleave", onLeave); })(img, emoji); img.addEventListener("click", () => { insertEmojiIntoEditor(emoji); picker.remove(); }); img.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); insertEmojiIntoEditor(emoji); picker.remove(); } }); sectionEmojis.appendChild(img); added++; }); if (added === 0) { const msg = createEl("div", { text: `${group.name} 组暂无有效表情`, style: "padding: 20px; text-align: center; color: #999;" }); sectionEmojis.appendChild(msg); } section.appendChild(titleContainer); section.appendChild(sectionEmojis); sections.appendChild(section); }); searchInput.addEventListener("input", (e) => { const q = (e.target.value || "").toLowerCase(); sections.querySelectorAll("img").forEach((img) => { const emojiName = img.getAttribute("data-emoji")?.toLowerCase() || ""; img.style.display = q === "" || emojiName.includes(q) ? "" : "none"; }); sections.querySelectorAll(".emoji-picker__section").forEach((section) => { const visibleEmojis = section.querySelectorAll("img:not([style*=\"none\"])"); const titleContainer = section.querySelector(".emoji-picker__section-title-container"); if (titleContainer) titleContainer.style.display = visibleEmojis.length > 0 ? "" : "none"; }); }); scrollableContent.appendChild(sections); content.appendChild(sectionsNav); content.appendChild(scrollableContent); emojiPickerDiv.appendChild(filterContainer); emojiPickerDiv.appendChild(content); innerContent.appendChild(emojiPickerDiv); picker.appendChild(innerContent); return picker; } async function createEmojiPicker() { const groups = userscriptState.emojiGroups; const mobile = isMobileView(); try { injectEmojiPickerStyles(); } catch (e) { console.warn("injectEmojiPickerStyles failed", e); } if (mobile) return createMobileEmojiPicker(groups); else return createDesktopEmojiPicker(groups); } init_createEl(); init_popularEmojis(); init_editorUtils(); var QUICK_INSERTS = [ "info", "tip", "faq", "question", "note", "abstract", "todo", "success", "warning", "failure", "danger", "bug", "example", "quote" ]; function createQuickInsertMenu() { const menu = createEl("div", { className: "fk-d-menu toolbar-menu__options-content toolbar-popup-menu-options -animated -expanded" }); const inner = createEl("div", { className: "fk-d-menu__inner-content" }); const list = createEl("ul", { className: "dropdown-menu" }); QUICK_INSERTS.forEach((key) => { const li = createEl("li", { className: "dropdown-menu__item" }); const btn = createEl("button", { className: "btn btn-icon-text", type: "button", title: key.charAt(0).toUpperCase() + key.slice(1), style: "background: " + (getIcon(key)?.color || "auto") }); btn.addEventListener("click", () => { if (menu.parentElement) menu.parentElement.removeChild(menu); insertIntoEditor(`>[!${key}]+\n`); }); const emojiSpan = createEl("span", { className: "d-button-emoji", text: getIcon(key)?.icon || "✳️", style: "margin-right: 6px;" }); const labelWrap = createEl("span", { className: "d-button-label" }); const labelText = createEl("span", { className: "d-button-label__text", text: key.charAt(0).toUpperCase() + key.slice(1) }); labelWrap.appendChild(labelText); const svgHtml = getIcon(key)?.svg || ""; if (svgHtml) { const svgSpan = createEl("span", { className: "d-button-label__svg", innerHTML: svgHtml, style: "margin-left: 6px; display: inline-flex; align-items: center;" }); labelWrap.appendChild(svgSpan); } btn.appendChild(emojiSpan); btn.appendChild(labelWrap); li.appendChild(btn); list.appendChild(li); }); inner.appendChild(list); menu.appendChild(inner); return menu; } 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); const quickInsertItem = createEl("li", { className: itemClassName }); const quickInsertBtn = createEl("button", { className: btnClassName, type: "button", title: "快捷输入", innerHTML: ` 快捷输入 ` }); quickInsertBtn.addEventListener("click", (e) => { e.stopPropagation(); if (menu.parentElement) menu.remove(); const quickMenu = createQuickInsertMenu(); (document.querySelector("#d-menu-portals") || document.body).appendChild(quickMenu); const rect = quickInsertBtn.getBoundingClientRect(); quickMenu.style.position = "fixed"; quickMenu.style.zIndex = "10000"; quickMenu.style.top = `${rect.bottom + 5}px`; quickMenu.style.left = `${Math.max(8, Math.min(rect.left + rect.width / 2 - 150, window.innerWidth - 300))}px`; const removeMenu = (ev) => { if (!quickMenu.contains(ev.target)) { if (quickMenu.parentElement) quickMenu.parentElement.removeChild(quickMenu); document.removeEventListener("click", removeMenu); } }; setTimeout(() => document.addEventListener("click", removeMenu), 100); }); quickInsertItem.appendChild(quickInsertBtn); dropdownMenu.appendChild(quickInsertItem); 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(); }); const quickInsertButton = createEl("button", { className: "btn no-text btn-icon toolbar__button quick-insert-button", title: "快捷输入", type: "button", innerHTML: "⎘" }); if (isChatComposer) { quickInsertButton.classList.add("fk-d-menu__trigger", "chat-composer-button", "btn-transparent"); quickInsertButton.setAttribute("aria-expanded", "false"); quickInsertButton.setAttribute("data-trigger", ""); } quickInsertButton.addEventListener("click", (e) => { e.stopPropagation(); const menu = createQuickInsertMenu(); (document.querySelector("#d-menu-portals") || document.body).appendChild(menu); const rect = quickInsertButton.getBoundingClientRect(); menu.style.position = "fixed"; menu.style.zIndex = "10000"; menu.style.top = `${rect.bottom + 5}px`; menu.style.left = `${Math.max(8, Math.min(rect.left + rect.width / 2 - 150, window.innerWidth - 300))}px`; const removeMenu = (ev) => { if (!menu.contains(ev.target)) { if (menu.parentElement) menu.parentElement.removeChild(menu); document.removeEventListener("click", removeMenu); } }; setTimeout(() => document.addEventListener("click", removeMenu), 100); }); try { if (isChatComposer) { const existingEmojiTrigger = toolbar.querySelector(".emoji-picker-trigger:not(.emoji-extension-button)"); if (existingEmojiTrigger) { toolbar.insertBefore(button, existingEmojiTrigger); toolbar.insertBefore(quickInsertButton, existingEmojiTrigger); toolbar.insertBefore(popularButton, existingEmojiTrigger); } else { toolbar.appendChild(button); toolbar.appendChild(quickInsertButton); toolbar.appendChild(popularButton); } } else { toolbar.appendChild(button); toolbar.appendChild(quickInsertButton); toolbar.appendChild(popularButton); } } catch (error) { console.error("[Emoji Extension Userscript] Failed to inject button:", error); } } init_state(); 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); } init_createEl(); 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 () => {}; } } init_createEl(); init_themeSupport(); init_state(); init_injectStyles(); 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$1 = /* @__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$1.has(id)) continue; seen$1.add(id); await autoReadAll(id, start); await sleep(200); } } } } window.autoReadAllReplies = autoReadAll; window.autoReadAllRepliesV2 = autoReadAllv2; init_userscript_storage(); init_state(); if (!window.autoReadAllRepliesV2) window.autoReadAllRepliesV2 = autoReadAllv2; 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 isDiscoursePage() { 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) { if ((generatorMeta.getAttribute("content")?.toLowerCase() || "").includes("discourse")) { console.log("[Emoji Extension Userscript] 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 Extension Userscript] Discourse elements detected"); return true; } console.log("[Emoji Extension Userscript] Not a Discourse site"); return false; } async function initializeEmojiFeature(maxAttempts = 10, delay = 1e3) { console.log("[Emoji Extension Userscript] Initializing..."); logPlatformInfo(); await initializeUserscriptData(); try { if (userscriptState.settings?.enableBatchParseImages !== false) { initOneClickAdd(); initPhotoSwipeTopbarUserscript(); console.log("[Userscript] One-click batch parse images enabled"); } else console.log("[Userscript] One-click batch parse images disabled by setting"); } catch (e) { console.warn("[Userscript] initOneClickAdd failed", e); } try { if (userscriptState.settings?.enableCalloutSuggestions !== false) { initCalloutSuggestionsUserscript(); console.log("[Userscript] Callout suggestions enabled"); } else console.log("[Userscript] Callout suggestions disabled by user setting"); } catch (e) { console.warn("[Userscript] initCalloutSuggestionsUserscript failed", e); } try { initRawPreview(); console.log("[Userscript] Raw preview initialized"); } catch (e) { console.warn("[Userscript] initRawPreview failed", e); } try { showAutoReadInMenu(); } catch (e) { console.warn("[Userscript] 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("[Userscript] callAutoReadRepliesV2 invocation failed", e); } }; console.log("[Userscript] callAutoReadRepliesV2 is exposed"); return; } window.callAutoReadRepliesV2 = (topicId) => { try { const fn = window.autoReadAllRepliesV2; if (fn && typeof fn === "function") return fn(topicId); } catch (e) { console.warn("[Userscript] callAutoReadRepliesV2 invocation failed", e); } console.warn("[Userscript] autoReadAllRepliesV2 not available on this page yet"); }; } catch (e) { console.warn("[Userscript] exposeAutoReadWrapper failed", e); } } exposeAutoReadWrapper(); let attempts = 0; function attemptToolbarInjection() { attempts++; const result = attemptInjection(); if (result.injectedCount > 0 || result.totalToolbars > 0) { console.log(`[Emoji Extension Userscript] Injection successful: ${result.injectedCount} buttons injected into ${result.totalToolbars} toolbars`); return; } if (attempts < maxAttempts) { console.log(`[Emoji Extension Userscript] Toolbar not found, attempt ${attempts}/${maxAttempts}. Retrying in ${delay / 1e3}s.`); setTimeout(attemptToolbarInjection, delay); } else { console.error("[Emoji Extension Userscript] Failed to find toolbar after multiple attempts."); console.log("[Emoji Extension Userscript] Showing floating button as fallback"); showFloatingButton(); } } if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", attemptToolbarInjection); else attemptToolbarInjection(); startPeriodicInjection(); setInterval(() => { checkAndShowFloatingButton(); }, 5e3); } if (isDiscoursePage()) { console.log("[Emoji Extension Userscript] Discourse detected, initializing emoji feature"); initializeEmojiFeature(); } else console.log("[Emoji Extension Userscript] Not a Discourse site, skipping injection"); })(); })();