// ==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("
`;
}).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(`- ${inlineFormat(ol[1])}
`);
continue;
}
const ul = line.match(/^\s*[-*]\s+(.*)/);
if (ul) {
if (listType !== "ul") {
if (listType === "ol") out.push("
");
listType = "ul";
out.push("");
}
out.push(`- ${inlineFormat(ul[1])}
`);
continue;
}
if (/^\s*$/.test(line)) {
if (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 `
`;
}
return `
`;
});
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 = ``;
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}
` : ""}
导出分组表情
导入分组表情
`,
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 || "📁"}
`) + `
`).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) => `
${emoji.name}
使用${emoji.count}次
`).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 = ``;
else emojiText = `
`;
const start = textArea.selectionStart || 0;
const end = textArea.selectionEnd || 0;
const currentValue = textArea.value;
textArea.value = currentValue.slice(0, start) + emojiText + currentValue.slice(end);
const newPosition = start + emojiText.length;
textArea.setSelectionRange(newPosition, newPosition);
textArea.dispatchEvent(new Event("input", { bubbles: true }));
textArea.focus();
} else {
const textAreas = document.querySelectorAll("textarea, input[type=\"text\"], [contenteditable=\"true\"]");
const lastTextArea = Array.from(textAreas).pop();
if (lastTextArea) {
lastTextArea.focus();
if (lastTextArea.tagName === "TEXTAREA" || lastTextArea.tagName === "INPUT") {
const format = userscriptState.settings.outputFormat;
let emojiText = "";
if (format === "markdown") emojiText = ``;
else emojiText = `
`;
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 = `
`;
} else insertText = ` `;
const selectionStart = textarea.selectionStart;
const selectionEnd = textarea.selectionEnd;
textarea.value = textarea.value.substring(0, selectionStart) + insertText + textarea.value.substring(selectionEnd, textarea.value.length);
textarea.selectionStart = textarea.selectionEnd = selectionStart + insertText.length;
textarea.focus();
const inputEvent = new Event("input", {
bubbles: true,
cancelable: true
});
textarea.dispatchEvent(inputEvent);
} else if (proseMirror) {
const imgWidth = Number(width) || 500;
const scaledWidth = Math.max(1, Math.round(imgWidth * (scale / 100)));
const htmlContent = `
`;
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 = `
`;
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 = ` `;
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");
})();
})();