// ==UserScript==
// @name Linux do 表情扩展 (Emoji Extension) lite
// @namespace https://github.com/stevessr/bug-v3
// @version 1.0.2
// @description 为论坛网站添加表情选择器功能 (Add emoji picker functionality to forum websites)
// @author stevessr
// @match https://linux.do/*
// @match https://meta.discourse.org/*
// @match https://*.discourse.org/*
// @match http://localhost:5173/*
// @grant none
// @license MIT
// @homepageURL https://github.com/stevessr/bug-v3
// @supportURL https://github.com/stevessr/bug-v3/issues
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
(function() {
var __defProp = Object.defineProperty;
var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
var __export = (all) => {
let target = {};
for (var name in all) __defProp(target, name, {
get: all[name],
enumerable: true
});
return target;
};
async function fetchPackagedJSON() {
try {
if (typeof fetch === "undefined") return null;
const res = await fetch("/assets/defaultEmojiGroups.json", { cache: "no-cache" });
if (!res.ok) return null;
return await res.json();
} catch (err) {
return null;
}
}
async function loadDefaultEmojiGroups() {
const packaged = await fetchPackagedJSON();
if (packaged && Array.isArray(packaged.groups)) return packaged.groups;
return [];
}
var init_defaultEmojiGroups_loader = __esmMin((() => {}));
function loadDataFromLocalStorage() {
try {
const groupsData = localStorage.getItem(STORAGE_KEY);
let emojiGroups = [];
if (groupsData) try {
const parsed = JSON.parse(groupsData);
if (Array.isArray(parsed) && parsed.length > 0) emojiGroups = parsed;
} catch (e) {
console.warn("[Userscript] Failed to parse stored emoji groups:", e);
}
if (emojiGroups.length === 0) emojiGroups = [];
const settingsData = localStorage.getItem(SETTINGS_KEY);
let settings = {
imageScale: 30,
gridColumns: 4,
outputFormat: "markdown",
forceMobileMode: false,
defaultGroup: "nachoneko",
showSearchBar: true
};
if (settingsData) try {
const parsed = JSON.parse(settingsData);
if (parsed && typeof parsed === "object") settings = {
...settings,
...parsed
};
} catch (e) {
console.warn("[Userscript] Failed to parse stored settings:", e);
}
emojiGroups = emojiGroups.filter((g) => g.id !== "favorites");
console.log("[Userscript] Loaded data from localStorage:", {
groupsCount: emojiGroups.length,
emojisCount: emojiGroups.reduce((acc, g) => acc + (g.emojis?.length || 0), 0),
settings
});
return {
emojiGroups,
settings
};
} catch (error) {
console.error("[Userscript] Failed to load from localStorage:", error);
console.error("[Userscript] Failed to load from localStorage:", error);
return {
emojiGroups: [],
settings: {
imageScale: 30,
gridColumns: 4,
outputFormat: "markdown",
forceMobileMode: false,
defaultGroup: "nachoneko",
showSearchBar: true
}
};
}
}
async function loadDataFromLocalStorageAsync() {
try {
const local = loadDataFromLocalStorage();
if (local.emojiGroups && local.emojiGroups.length > 0) return local;
const remoteUrl = localStorage.getItem("emoji_extension_remote_config_url");
if (remoteUrl && typeof remoteUrl === "string" && remoteUrl.trim().length > 0) try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5e3);
const res = await fetch(remoteUrl, { signal: controller.signal });
clearTimeout(timeout);
if (res && res.ok) {
const json = await res.json();
const groups = Array.isArray(json.emojiGroups) ? json.emojiGroups : Array.isArray(json.groups) ? json.groups : null;
const settings = json.settings && typeof json.settings === "object" ? json.settings : local.settings;
if (groups && groups.length > 0) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(groups));
} catch (e) {
console.warn("[Userscript] Failed to persist fetched remote groups to localStorage", e);
}
return {
emojiGroups: groups.filter((g) => g.id !== "favorites"),
settings
};
}
}
} catch (err) {
console.warn("[Userscript] Failed to fetch remote default config:", err);
}
try {
const runtime = await loadDefaultEmojiGroups();
const source = runtime && runtime.length ? runtime : [];
const filtered = JSON.parse(JSON.stringify(source)).filter((g) => g.id !== "favorites");
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered));
} catch (e) {}
return {
emojiGroups: filtered,
settings: local.settings
};
} catch (e) {
console.error("[Userscript] Failed to load default groups in async fallback:", e);
return {
emojiGroups: [],
settings: local.settings
};
}
} catch (error) {
console.error("[Userscript] loadDataFromLocalStorageAsync failed:", error);
return {
emojiGroups: [],
settings: {
imageScale: 30,
gridColumns: 4,
outputFormat: "markdown",
forceMobileMode: false,
defaultGroup: "nachoneko",
showSearchBar: true
}
};
}
}
function saveDataToLocalStorage(data) {
try {
if (data.emojiGroups) localStorage.setItem(STORAGE_KEY, JSON.stringify(data.emojiGroups));
if (data.settings) localStorage.setItem(SETTINGS_KEY, JSON.stringify(data.settings));
} catch (error) {
console.error("[Userscript] Failed to save to localStorage:", error);
}
}
function addEmojiToUserscript(emojiData) {
try {
const data = loadDataFromLocalStorage();
let userGroup = data.emojiGroups.find((g) => g.id === "user_added");
if (!userGroup) {
userGroup = {
id: "user_added",
name: "用户添加",
icon: "⭐",
order: 999,
emojis: []
};
data.emojiGroups.push(userGroup);
}
if (!userGroup.emojis.some((e) => e.url === emojiData.url || e.name === emojiData.name)) {
userGroup.emojis.push({
packet: Date.now(),
name: emojiData.name,
url: emojiData.url
});
saveDataToLocalStorage({ emojiGroups: data.emojiGroups });
console.log("[Userscript] Added emoji to user group:", emojiData.name);
} else console.log("[Userscript] Emoji already exists:", emojiData.name);
} catch (error) {
console.error("[Userscript] Failed to add emoji:", error);
}
}
function exportUserscriptData() {
try {
const data = loadDataFromLocalStorage();
return JSON.stringify(data, null, 2);
} catch (error) {
console.error("[Userscript] Failed to export data:", error);
return "";
}
}
function importUserscriptData(jsonData) {
try {
const data = JSON.parse(jsonData);
if (data.emojiGroups && Array.isArray(data.emojiGroups)) saveDataToLocalStorage({ emojiGroups: data.emojiGroups });
if (data.settings && typeof data.settings === "object") saveDataToLocalStorage({ settings: data.settings });
console.log("[Userscript] Data imported successfully");
return true;
} catch (error) {
console.error("[Userscript] Failed to import data:", error);
return false;
}
}
function syncFromManager() {
try {
const managerGroups = localStorage.getItem("emoji_extension_manager_groups");
const managerSettings = localStorage.getItem("emoji_extension_manager_settings");
let updated = false;
if (managerGroups) {
const groups = JSON.parse(managerGroups);
if (Array.isArray(groups)) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(groups));
updated = true;
}
}
if (managerSettings) {
const settings = JSON.parse(managerSettings);
if (typeof settings === "object") {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
updated = true;
}
}
if (updated) console.log("[Userscript] Synced data from manager");
return updated;
} catch (error) {
console.error("[Userscript] Failed to sync from manager:", error);
return false;
}
}
var STORAGE_KEY, SETTINGS_KEY;
var init_userscript_storage = __esmMin((() => {
init_defaultEmojiGroups_loader();
STORAGE_KEY = "emoji_extension_userscript_data";
SETTINGS_KEY = "emoji_extension_userscript_settings";
}));
var userscriptState;
var init_state = __esmMin((() => {
userscriptState = {
emojiGroups: [],
settings: {
imageScale: 30,
gridColumns: 4,
outputFormat: "markdown",
forceMobileMode: false,
defaultGroup: "nachoneko",
showSearchBar: true
}
};
}));
function createEl(tag, opts) {
const el = document.createElement(tag);
if (opts) {
if (opts.width) el.style.width = opts.width;
if (opts.height) el.style.height = opts.height;
if (opts.className) el.className = opts.className;
if (opts.text) el.textContent = opts.text;
if (opts.placeholder && "placeholder" in el) el.placeholder = opts.placeholder;
if (opts.type && "type" in el) el.type = opts.type;
if (opts.value !== void 0 && "value" in el) el.value = opts.value;
if (opts.style) el.style.cssText = opts.style;
if (opts.src && "src" in el) el.src = opts.src;
if (opts.attrs) for (const k in opts.attrs) el.setAttribute(k, opts.attrs[k]);
if (opts.dataset) for (const k in opts.dataset) el.dataset[k] = opts.dataset[k];
if (opts.innerHTML) el.innerHTML = opts.innerHTML;
if (opts.title) el.title = opts.title;
if (opts.alt && "alt" in el) el.alt = opts.alt;
}
return el;
}
var init_createEl = __esmMin((() => {}));
init_createEl();
init_state();
init_userscript_storage();
function insertIntoEditor(text) {
const textArea = document.querySelector("textarea.d-editor-input");
const richEle = document.querySelector(".ProseMirror.d-editor-input");
if (!textArea && !richEle) {
console.error("找不到输入框");
return;
}
if (textArea) {
const start = textArea.selectionStart;
const end = textArea.selectionEnd;
const value = textArea.value;
textArea.value = value.substring(0, start) + text + value.substring(end);
textArea.setSelectionRange(start + text.length, start + text.length);
textArea.focus();
const event = new Event("input", { bubbles: true });
textArea.dispatchEvent(event);
} else if (richEle) {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const textNode = document.createTextNode(text);
range.insertNode(textNode);
range.setStartAfter(textNode);
range.setEndAfter(textNode);
selection.removeAllRanges();
selection.addRange(range);
}
richEle.focus();
}
}
var ImageUploader = class {
waitingQueue = [];
uploadingQueue = [];
failedQueue = [];
successQueue = [];
isProcessing = false;
maxRetries = 2;
progressDialog = null;
async uploadImage(file) {
return new Promise((resolve, reject) => {
const item = {
id: `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
file,
resolve,
reject,
retryCount: 0,
status: "waiting",
timestamp: Date.now()
};
this.waitingQueue.push(item);
this.updateProgressDialog();
this.processQueue();
});
}
moveToQueue(item, targetStatus) {
this.waitingQueue = this.waitingQueue.filter((i) => i.id !== item.id);
this.uploadingQueue = this.uploadingQueue.filter((i) => i.id !== item.id);
this.failedQueue = this.failedQueue.filter((i) => i.id !== item.id);
this.successQueue = this.successQueue.filter((i) => i.id !== item.id);
item.status = targetStatus;
switch (targetStatus) {
case "waiting":
this.waitingQueue.push(item);
break;
case "uploading":
this.uploadingQueue.push(item);
break;
case "failed":
this.failedQueue.push(item);
break;
case "success":
this.successQueue.push(item);
break;
}
this.updateProgressDialog();
}
async processQueue() {
if (this.isProcessing || this.waitingQueue.length === 0) return;
this.isProcessing = true;
while (this.waitingQueue.length > 0) {
const item = this.waitingQueue.shift();
if (!item) continue;
this.moveToQueue(item, "uploading");
try {
const result = await this.performUpload(item.file);
item.result = result;
this.moveToQueue(item, "success");
item.resolve(result);
const markdown = ``;
insertIntoEditor(markdown);
} catch (error) {
item.error = error;
if (this.shouldRetry(error, item)) {
item.retryCount++;
if (error.error_type === "rate_limit" && error.extras?.wait_seconds) await this.sleep(error.extras.wait_seconds * 1e3);
else await this.sleep(Math.pow(2, item.retryCount) * 1e3);
this.moveToQueue(item, "waiting");
} else {
this.moveToQueue(item, "failed");
item.reject(error);
}
}
}
this.isProcessing = false;
}
shouldRetry(error, item) {
if (item.retryCount >= this.maxRetries) return false;
return error.error_type === "rate_limit";
}
retryFailedItem(itemId) {
const item = this.failedQueue.find((i) => i.id === itemId);
if (item && item.retryCount < this.maxRetries) {
item.retryCount++;
this.moveToQueue(item, "waiting");
this.processQueue();
}
}
showProgressDialog() {
if (this.progressDialog) return;
this.progressDialog = this.createProgressDialog();
document.body.appendChild(this.progressDialog);
}
hideProgressDialog() {
if (this.progressDialog) {
this.progressDialog.remove();
this.progressDialog = null;
}
}
updateProgressDialog() {
if (!this.progressDialog) return;
const allItems = [
...this.waitingQueue,
...this.uploadingQueue,
...this.failedQueue,
...this.successQueue
];
this.renderQueueItems(this.progressDialog, allItems);
}
async sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
createProgressDialog() {
const dialog = document.createElement("div");
dialog.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
width: 350px;
max-height: 400px;
background: white;
border-radius: 8px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
border: 1px solid #e5e7eb;
overflow: hidden;
`;
const header = document.createElement("div");
header.style.cssText = `
padding: 16px 20px;
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
font-weight: 600;
font-size: 14px;
color: #374151;
display: flex;
justify-content: space-between;
align-items: center;
`;
header.textContent = "图片上传队列";
const closeButton = document.createElement("button");
closeButton.innerHTML = "✕";
closeButton.style.cssText = `
background: none;
border: none;
font-size: 16px;
cursor: pointer;
color: #6b7280;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s;
`;
closeButton.addEventListener("click", () => {
this.hideProgressDialog();
});
closeButton.addEventListener("mouseenter", () => {
closeButton.style.backgroundColor = "#e5e7eb";
});
closeButton.addEventListener("mouseleave", () => {
closeButton.style.backgroundColor = "transparent";
});
header.appendChild(closeButton);
const content = document.createElement("div");
content.className = "upload-queue-content";
content.style.cssText = `
max-height: 320px;
overflow-y: auto;
padding: 12px;
`;
dialog.appendChild(header);
dialog.appendChild(content);
return dialog;
}
renderQueueItems(dialog, allItems) {
const content = dialog.querySelector(".upload-queue-content");
if (!content) return;
content.innerHTML = "";
if (allItems.length === 0) {
const emptyState = document.createElement("div");
emptyState.style.cssText = `
text-align: center;
color: #6b7280;
font-size: 14px;
padding: 20px;
`;
emptyState.textContent = "暂无上传任务";
content.appendChild(emptyState);
return;
}
allItems.forEach((item) => {
const itemEl = document.createElement("div");
itemEl.style.cssText = `
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
margin-bottom: 8px;
background: #f9fafb;
border-radius: 6px;
border-left: 4px solid ${this.getStatusColor(item.status)};
`;
const leftSide = document.createElement("div");
leftSide.style.cssText = `
flex: 1;
min-width: 0;
`;
const fileName = document.createElement("div");
fileName.style.cssText = `
font-size: 13px;
font-weight: 500;
color: #374151;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
fileName.textContent = item.file.name;
const status = document.createElement("div");
status.style.cssText = `
font-size: 12px;
color: #6b7280;
margin-top: 2px;
`;
status.textContent = this.getStatusText(item);
leftSide.appendChild(fileName);
leftSide.appendChild(status);
const rightSide = document.createElement("div");
rightSide.style.cssText = `
display: flex;
align-items: center;
gap: 8px;
`;
if (item.status === "failed" && item.retryCount < this.maxRetries) {
const retryButton = document.createElement("button");
retryButton.innerHTML = "🔄";
retryButton.style.cssText = `
background: none;
border: none;
cursor: pointer;
font-size: 14px;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s;
`;
retryButton.title = "重试上传";
retryButton.addEventListener("click", () => {
this.retryFailedItem(item.id);
});
retryButton.addEventListener("mouseenter", () => {
retryButton.style.backgroundColor = "#e5e7eb";
});
retryButton.addEventListener("mouseleave", () => {
retryButton.style.backgroundColor = "transparent";
});
rightSide.appendChild(retryButton);
}
const statusIcon = document.createElement("div");
statusIcon.style.cssText = `
font-size: 16px;
`;
statusIcon.textContent = this.getStatusIcon(item.status);
rightSide.appendChild(statusIcon);
itemEl.appendChild(leftSide);
itemEl.appendChild(rightSide);
content.appendChild(itemEl);
});
}
getStatusColor(status) {
switch (status) {
case "waiting": return "#f59e0b";
case "uploading": return "#3b82f6";
case "success": return "#10b981";
case "failed": return "#ef4444";
default: return "#6b7280";
}
}
getStatusText(item) {
switch (item.status) {
case "waiting": return "等待上传";
case "uploading": return "正在上传...";
case "success": return "上传成功";
case "failed":
if (item.error?.error_type === "rate_limit") return `上传失败 - 请求过于频繁 (重试 ${item.retryCount}/${this.maxRetries})`;
return `上传失败 (重试 ${item.retryCount}/${this.maxRetries})`;
default: return "未知状态";
}
}
getStatusIcon(status) {
switch (status) {
case "waiting": return "⏳";
case "uploading": return "📤";
case "success": return "✅";
case "failed": return "❌";
default: return "❓";
}
}
async performUpload(file) {
const sha1 = await this.calculateSHA1(file);
const formData = new FormData();
formData.append("upload_type", "composer");
formData.append("relativePath", "null");
formData.append("name", file.name);
formData.append("type", file.type);
formData.append("sha1_checksum", sha1);
formData.append("file", file, file.name);
const csrfToken = this.getCSRFToken();
const headers = { "X-Csrf-Token": csrfToken };
if (document.cookie) headers["Cookie"] = document.cookie;
const response = await fetch(`https://linux.do/uploads.json?client_id=f06cb5577ba9410d94b9faf94e48c2d8`, {
method: "POST",
headers,
body: formData
});
if (!response.ok) throw await response.json();
return await response.json();
}
getCSRFToken() {
const metaToken = document.querySelector("meta[name=\"csrf-token\"]");
if (metaToken) return metaToken.content;
const match = document.cookie.match(/csrf_token=([^;]+)/);
if (match) return decodeURIComponent(match[1]);
const hiddenInput = document.querySelector("input[name=\"authenticity_token\"]");
if (hiddenInput) return hiddenInput.value;
console.warn("[Image Uploader] No CSRF token found");
return "";
}
async calculateSHA1(file) {
const text = `${file.name}-${file.size}-${file.lastModified}`;
const data = new TextEncoder().encode(text);
if (crypto.subtle) try {
const hashBuffer = await crypto.subtle.digest("SHA-1", data);
return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
} catch (e) {
console.warn("[Image Uploader] Could not calculate SHA1, using fallback");
}
let hash = 0;
for (let i = 0; i < text.length; i++) {
const char = text.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return Math.abs(hash).toString(16).padStart(40, "0");
}
};
var uploader = new ImageUploader();
function extractEmojiFromImage(img, titleElement) {
const url = img.src;
if (!url || !url.startsWith("http")) return null;
let name = "";
const parts = (titleElement.textContent || "").split("·");
if (parts.length > 0) name = parts[0].trim();
if (!name || name.length < 2) name = img.alt || img.title || extractNameFromUrl(url);
name = name.trim();
if (name.length === 0) name = "表情";
return {
name,
url
};
}
function extractNameFromUrl(url) {
try {
const nameWithoutExt = (new URL(url).pathname.split("/").pop() || "").replace(/\.[^/.]+$/, "");
const decoded = decodeURIComponent(nameWithoutExt);
if (/^[0-9a-f]{8,}$/i.test(decoded) || decoded.length < 2) return "表情";
return decoded || "表情";
} catch {
return "表情";
}
}
function createAddButton(emojiData) {
const link = createEl("a", {
className: "image-source-link emoji-add-link",
style: `
color: #ffffff;
text-decoration: none;
cursor: pointer;
display: inline-flex;
align-items: center;
font-size: inherit;
font-family: inherit;
background: linear-gradient(135deg, #4f46e5, #7c3aed);
border: 2px solid #ffffff;
border-radius: 6px;
padding: 4px 8px;
margin: 0 2px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: all 0.2s ease;
font-weight: 600;
`
});
link.addEventListener("mouseenter", () => {
if (!link.innerHTML.includes("已添加") && !link.innerHTML.includes("失败")) {
link.style.background = "linear-gradient(135deg, #3730a3, #5b21b6)";
link.style.transform = "scale(1.05)";
link.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.3)";
}
});
link.addEventListener("mouseleave", () => {
if (!link.innerHTML.includes("已添加") && !link.innerHTML.includes("失败")) {
link.style.background = "linear-gradient(135deg, #4f46e5, #7c3aed)";
link.style.transform = "scale(1)";
link.style.boxShadow = "0 2px 4px rgba(0, 0, 0, 0.2)";
}
});
link.innerHTML = `
<svg class="fa d-icon d-icon-plus svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="width: 1em; height: 1em; fill: currentColor; margin-right: 4px;">
<path d="M12 4c.55 0 1 .45 1 1v6h6c.55 0 1 .45 1 1s-.45 1-1 1h-6v6c0 .55-.45 1-1 1s-1-.45-1-1v-6H5c-.55 0-1-.45-1-1s.45-1 1-1h6V5c0-.55.45-1 1-1z"/>
</svg>添加表情
`;
link.title = "添加到用户表情";
link.addEventListener("click", async (e) => {
e.preventDefault();
e.stopPropagation();
const originalHTML = link.innerHTML;
const originalStyle = link.style.cssText;
try {
addEmojiToUserscript(emojiData);
try {
uploader.showProgressDialog();
} catch (e$1) {
console.warn("[Userscript] uploader.showProgressDialog failed:", e$1);
}
link.innerHTML = `
<svg class="fa d-icon d-icon-check svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="width: 1em; height: 1em; fill: currentColor; margin-right: 4px;">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg>已添加
`;
link.style.background = "linear-gradient(135deg, #10b981, #059669)";
link.style.color = "#ffffff";
link.style.border = "2px solid #ffffff";
link.style.boxShadow = "0 2px 4px rgba(16, 185, 129, 0.3)";
setTimeout(() => {
link.innerHTML = originalHTML;
link.style.cssText = originalStyle;
}, 2e3);
} catch (error) {
console.error("[Emoji Extension Userscript] Failed to add emoji:", error);
link.innerHTML = `
<svg class="fa d-icon d-icon-times svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="width: 1em; height: 1em; fill: currentColor; margin-right: 4px;">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>失败
`;
link.style.background = "linear-gradient(135deg, #ef4444, #dc2626)";
link.style.color = "#ffffff";
link.style.border = "2px solid #ffffff";
link.style.boxShadow = "0 2px 4px rgba(239, 68, 68, 0.3)";
setTimeout(() => {
link.innerHTML = originalHTML;
link.style.cssText = originalStyle;
}, 2e3);
}
});
return link;
}
function processLightbox(lightbox) {
if (lightbox.querySelector(".emoji-add-link")) return;
const img = lightbox.querySelector(".mfp-img");
const title = lightbox.querySelector(".mfp-title");
if (!img || !title) return;
const emojiData = extractEmojiFromImage(img, title);
if (!emojiData) return;
const addButton = createAddButton(emojiData);
const sourceLink = title.querySelector("a.image-source-link");
if (sourceLink) {
const separator = document.createTextNode(" · ");
title.insertBefore(separator, sourceLink);
title.insertBefore(addButton, sourceLink);
} else {
title.appendChild(document.createTextNode(" · "));
title.appendChild(addButton);
}
}
function processAllLightboxes() {
document.querySelectorAll(".mfp-wrap.mfp-gallery").forEach((lightbox) => {
if (lightbox.classList.contains("mfp-wrap") && lightbox.classList.contains("mfp-gallery") && lightbox.querySelector(".mfp-img")) processLightbox(lightbox);
});
}
function initOneClickAdd() {
console.log("[Emoji Extension Userscript] Initializing one-click add functionality");
setTimeout(processAllLightboxes, 500);
new MutationObserver((mutations) => {
let hasNewLightbox = false;
mutations.forEach((mutation) => {
if (mutation.type === "childList") mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node;
if (element.classList && element.classList.contains("mfp-wrap")) hasNewLightbox = true;
}
});
});
if (hasNewLightbox) setTimeout(processAllLightboxes, 100);
}).observe(document.body, {
childList: true,
subtree: true
});
document.addEventListener("visibilitychange", () => {
if (!document.hidden) setTimeout(processAllLightboxes, 200);
});
}
function isImageUrl(value) {
if (!value) return false;
let v = value.trim();
if (/^url\(/i.test(v)) {
const inner = v.replace(/^url\(/i, "").replace(/\)$/, "").trim();
if (inner.startsWith("\"") && inner.endsWith("\"") || inner.startsWith("'") && inner.endsWith("'")) v = inner.slice(1, -1).trim();
else v = inner;
}
if (v.startsWith("data:image/")) return true;
if (v.startsWith("blob:")) return true;
if (v.startsWith("//")) v = "https:" + v;
if (/\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)(\?.*)?$/i.test(v)) return true;
try {
const url = new URL(v);
const protocol = url.protocol;
if (protocol === "http:" || protocol === "https:" || protocol.endsWith(":")) {
if (/\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)(\?.*)?$/i.test(url.pathname)) return true;
if (/format=|ext=|type=image|image_type=/i.test(url.search)) return true;
}
} catch {}
return false;
}
function injectEmojiPickerStyles() {
if (typeof document === "undefined") return;
if (document.getElementById("emoji-picker-styles")) return;
const css = `
.emoji-picker-hover-preview{
position:fixed;
pointer-events:none;
display:none;
z-index:1000002;
max-width:320px;
max-height:320px;
overflow:hidden;
border-radius:8px;
box-shadow:0 6px 20px rgba(0,0,0,0.32);
background:#ffffff;
padding:8px;
transition:opacity .12s ease, transform .12s ease;
}
.emoji-picker-hover-preview img.emoji-picker-hover-img{
display:block;
max-width:100%;
max-height:220px;
object-fit:contain;
}
.emoji-picker-hover-preview .emoji-picker-hover-label{
font-size:12px;
color:#222;
margin-top:8px;
text-align:center;
word-break:break-word;
}
/* Dark theme adaptation */
@media (prefers-color-scheme: dark) {
.emoji-picker-hover-preview{
background: rgba(32,33,36,0.94);
box-shadow: 0 6px 20px rgba(0,0,0,0.6);
border: 1px solid rgba(255,255,255,0.04);
}
.emoji-picker-hover-preview .emoji-picker-hover-label{
color: #e6e6e6;
}
}
`;
const style = document.createElement("style");
style.id = "emoji-picker-styles";
style.textContent = css;
document.head.appendChild(style);
}
const __vitePreload = function preload(baseModule, deps, importerUrl) {
let promise = Promise.resolve();
function handlePreloadError(err$2) {
const e$1 = new Event("vite:preloadError", { cancelable: true });
e$1.payload = err$2;
window.dispatchEvent(e$1);
if (!e$1.defaultPrevented) throw err$2;
}
return promise.then((res) => {
for (const item of res || []) {
if (item.status !== "rejected") continue;
handlePreloadError(item.reason);
}
return baseModule().catch(handlePreloadError);
});
};
function injectManagerStyles() {
if (__managerStylesInjected) return;
__managerStylesInjected = true;
document.head.appendChild(createEl("style", {
attrs: { "data-emoji-manager-styles": "1" },
text: `
/* Modal backdrop */
.emoji-manager-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.8);
z-index: 999999;
display: flex;
align-items: center;
justify-content: center;
}
/* Main modal panel */
.emoji-manager-panel {
background: white;
border-radius: 8px;
max-width: 90vw;
max-height: 90vh;
width: 1000px;
height: 600px;
display: grid;
grid-template-columns: 300px 1fr;
grid-template-rows: 1fr auto;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
}
/* Left panel - groups list */
.emoji-manager-left {
background: #f8f9fa;
border-right: 1px solid #e9ecef;
display: flex;
flex-direction: column;
overflow: hidden;
}
.emoji-manager-left-header {
display: flex;
align-items: center;
padding: 16px;
border-bottom: 1px solid #e9ecef;
background: white;
}
.emoji-manager-addgroup-row {
display: flex;
gap: 8px;
padding: 12px;
border-bottom: 1px solid #e9ecef;
}
.emoji-manager-groups-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.emoji-manager-groups-list > div {
padding: 12px;
border-radius: 6px;
cursor: pointer;
margin-bottom: 4px;
transition: background-color 0.2s;
}
.emoji-manager-groups-list > div:hover {
background: #e9ecef;
}
.emoji-manager-groups-list > div:focus {
outline: none;
box-shadow: inset 0 0 0 2px #007bff;
}
/* Right panel - emoji display and editing */
.emoji-manager-right {
background: white;
display: flex;
flex-direction: column;
overflow: hidden;
}
.emoji-manager-right-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid #e9ecef;
}
.emoji-manager-right-main {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.emoji-manager-emojis {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.emoji-manager-card {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
padding: 12px;
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
transition: transform 0.2s, box-shadow 0.2s;
}
.emoji-manager-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.emoji-manager-card-img {
width: 80px;
height: 80px;
object-fit: contain;
border-radius: 6px;
background: white;
}
.emoji-manager-card-name {
font-size: 12px;
color: #495057;
text-align: center;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: 500;
}
.emoji-manager-card-actions {
display: flex;
gap: 6px;
}
/* Add emoji form */
.emoji-manager-add-emoji-form {
padding: 16px;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
display: flex;
gap: 8px;
align-items: center;
}
/* Footer */
.emoji-manager-footer {
grid-column: 1 / -1;
display: flex;
gap: 8px;
justify-content: space-between;
padding: 16px;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
}
/* Editor panel - popup modal */
.emoji-manager-editor-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 24px;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
z-index: 1000000;
min-width: 400px;
}
.emoji-manager-editor-preview {
width: 100px;
height: 100px;
object-fit: contain;
border-radius: 8px;
background: #f8f9fa;
margin: 0 auto 16px;
display: block;
}
/* Hover preview (moved from inline styles) */
.emoji-manager-hover-preview {
position: fixed;
pointer-events: none;
z-index: 1000002;
display: none;
max-width: 300px;
max-height: 300px;
border: 1px solid rgba(0,0,0,0.1);
background: #fff;
padding: 4px;
border-radius: 6px;
box-shadow: 0 6px 18px rgba(0,0,0,0.12);
}
/* Form styling */
.form-control {
width: 100%;
padding: 8px 12px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 14px;
margin-bottom: 8px;
}
.btn {
padding: 8px 16px;
border: 1px solid transparent;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-sm {
padding: 4px 8px;
font-size: 12px;
}
`
}));
}
var __managerStylesInjected;
var init_styles = __esmMin((() => {
init_createEl();
__managerStylesInjected = false;
}));
var manager_exports = __export({ openManagementInterface: () => openManagementInterface });
function createEditorPopup(groupId, index, renderGroups, renderSelectedGroup) {
const group = userscriptState.emojiGroups.find((g) => g.id === groupId);
if (!group) return;
const emo = group.emojis[index];
if (!emo) return;
const backdrop = createEl("div", { style: `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000000;
display: flex;
align-items: center;
justify-content: center;
` });
const editorPanel = createEl("div", { className: "emoji-manager-editor-panel" });
const editorTitle = createEl("h3", {
text: "编辑表情",
className: "emoji-manager-editor-title",
style: "margin: 0 0 16px 0; text-align: center;"
});
const editorPreview = createEl("img", { className: "emoji-manager-editor-preview" });
editorPreview.src = emo.url;
const editorWidthInput = createEl("input", {
className: "form-control",
placeholder: "宽度 (px) 可选",
value: emo.width ? String(emo.width) : ""
});
const editorHeightInput = createEl("input", {
className: "form-control",
placeholder: "高度 (px) 可选",
value: emo.height ? String(emo.height) : ""
});
const editorNameInput = createEl("input", {
className: "form-control",
placeholder: "名称 (alias)",
value: emo.name || ""
});
const editorUrlInput = createEl("input", {
className: "form-control",
placeholder: "表情图片 URL",
value: emo.url || ""
});
const buttonContainer = createEl("div", { style: "display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px;" });
const editorSaveBtn = createEl("button", {
text: "保存修改",
className: "btn btn-primary"
});
const editorCancelBtn = createEl("button", {
text: "取消",
className: "btn"
});
buttonContainer.appendChild(editorCancelBtn);
buttonContainer.appendChild(editorSaveBtn);
editorPanel.appendChild(editorTitle);
editorPanel.appendChild(editorPreview);
editorPanel.appendChild(editorWidthInput);
editorPanel.appendChild(editorHeightInput);
editorPanel.appendChild(editorNameInput);
editorPanel.appendChild(editorUrlInput);
editorPanel.appendChild(buttonContainer);
backdrop.appendChild(editorPanel);
document.body.appendChild(backdrop);
editorUrlInput.addEventListener("input", () => {
editorPreview.src = editorUrlInput.value;
});
editorSaveBtn.addEventListener("click", () => {
const newName = (editorNameInput.value || "").trim();
const newUrl = (editorUrlInput.value || "").trim();
const newWidth = parseInt((editorWidthInput.value || "").trim(), 10);
const newHeight = parseInt((editorHeightInput.value || "").trim(), 10);
if (!newName || !newUrl) {
alert("名称和 URL 均不能为空");
return;
}
emo.name = newName;
emo.url = newUrl;
if (!isNaN(newWidth) && newWidth > 0) emo.width = newWidth;
else delete emo.width;
if (!isNaN(newHeight) && newHeight > 0) emo.height = newHeight;
else delete emo.height;
renderGroups();
renderSelectedGroup();
backdrop.remove();
});
editorCancelBtn.addEventListener("click", () => {
backdrop.remove();
});
backdrop.addEventListener("click", (e) => {
if (e.target === backdrop) backdrop.remove();
});
}
function openManagementInterface() {
injectManagerStyles();
const modal = createEl("div", {
className: "emoji-manager-wrapper",
attrs: {
role: "dialog",
"aria-modal": "true"
}
});
const panel = createEl("div", { className: "emoji-manager-panel" });
const left = createEl("div", { className: "emoji-manager-left" });
const leftHeader = createEl("div", { className: "emoji-manager-left-header" });
const title = createEl("h3", { text: "表情管理器" });
const closeBtn = createEl("button", {
text: "×",
className: "btn",
style: "font-size:20px; background:none; border:none; cursor:pointer;"
});
leftHeader.appendChild(title);
leftHeader.appendChild(closeBtn);
left.appendChild(leftHeader);
const addGroupRow = createEl("div", { className: "emoji-manager-addgroup-row" });
const addGroupInput = createEl("input", {
placeholder: "新分组 id",
className: "form-control"
});
const addGroupBtn = createEl("button", {
text: "添加",
className: "btn"
});
addGroupRow.appendChild(addGroupInput);
addGroupRow.appendChild(addGroupBtn);
left.appendChild(addGroupRow);
const groupsList = createEl("div", { className: "emoji-manager-groups-list" });
left.appendChild(groupsList);
const right = createEl("div", { className: "emoji-manager-right" });
const rightHeader = createEl("div", { className: "emoji-manager-right-header" });
const groupTitle = createEl("h4");
groupTitle.textContent = "";
const deleteGroupBtn = createEl("button", {
text: "删除分组",
className: "btn",
style: "background:#ef4444; color:#fff;"
});
rightHeader.appendChild(groupTitle);
rightHeader.appendChild(deleteGroupBtn);
right.appendChild(rightHeader);
const managerRightMain = createEl("div", { className: "emoji-manager-right-main" });
const emojisContainer = createEl("div", { className: "emoji-manager-emojis" });
managerRightMain.appendChild(emojisContainer);
const addEmojiForm = createEl("div", { className: "emoji-manager-add-emoji-form" });
const emojiUrlInput = createEl("input", {
placeholder: "表情图片 URL",
className: "form-control"
});
const emojiNameInput = createEl("input", {
placeholder: "名称 (alias)",
className: "form-control"
});
const emojiWidthInput = createEl("input", {
placeholder: "宽度 (px) 可选",
className: "form-control"
});
const emojiHeightInput = createEl("input", {
placeholder: "高度 (px) 可选",
className: "form-control"
});
const addEmojiBtn = createEl("button", {
text: "添加表情",
className: "btn btn-primary"
});
addEmojiForm.appendChild(emojiUrlInput);
addEmojiForm.appendChild(emojiNameInput);
addEmojiForm.appendChild(emojiWidthInput);
addEmojiForm.appendChild(emojiHeightInput);
addEmojiForm.appendChild(addEmojiBtn);
managerRightMain.appendChild(addEmojiForm);
right.appendChild(managerRightMain);
const footer = createEl("div", { className: "emoji-manager-footer" });
const exportBtn = createEl("button", {
text: "导出",
className: "btn"
});
const importBtn = createEl("button", {
text: "导入",
className: "btn"
});
const exitBtn = createEl("button", {
text: "退出",
className: "btn"
});
exitBtn.addEventListener("click", () => modal.remove());
const saveBtn = createEl("button", {
text: "保存",
className: "btn btn-primary"
});
const syncBtn = createEl("button", {
text: "同步管理器",
className: "btn"
});
footer.appendChild(syncBtn);
footer.appendChild(exportBtn);
footer.appendChild(importBtn);
footer.appendChild(exitBtn);
footer.appendChild(saveBtn);
panel.appendChild(left);
panel.appendChild(right);
panel.appendChild(footer);
modal.appendChild(panel);
document.body.appendChild(modal);
let selectedGroupId = null;
function renderGroups() {
groupsList.innerHTML = "";
if (!selectedGroupId && userscriptState.emojiGroups.length > 0) selectedGroupId = userscriptState.emojiGroups[0].id;
userscriptState.emojiGroups.forEach((g) => {
const row = createEl("div", {
style: "display:flex; justify-content:space-between; align-items:center; padding:6px; border-radius:4px; cursor:pointer;",
text: `${g.name || g.id} (${(g.emojis || []).length})`,
attrs: {
tabindex: "0",
"data-group-id": g.id
}
});
const selectGroup = () => {
selectedGroupId = g.id;
renderGroups();
renderSelectedGroup();
};
row.addEventListener("click", selectGroup);
row.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
selectGroup();
}
});
if (selectedGroupId === g.id) row.style.background = "#f0f8ff";
groupsList.appendChild(row);
});
}
function showEditorFor(groupId, index) {
createEditorPopup(groupId, index, renderGroups, renderSelectedGroup);
}
function renderSelectedGroup() {
const group = userscriptState.emojiGroups.find((g) => g.id === selectedGroupId) || null;
groupTitle.textContent = group ? group.name || group.id : "";
emojisContainer.innerHTML = "";
if (!group) return;
(Array.isArray(group.emojis) ? group.emojis : []).forEach((emo, idx) => {
const card = createEl("div", { className: "emoji-manager-card" });
const img = createEl("img", {
src: emo.url,
alt: emo.name,
className: "emoji-manager-card-img"
});
if (emo.width) img.style.width = typeof emo.width === "number" ? emo.width + "px" : emo.width;
if (emo.height) img.style.height = typeof emo.height === "number" ? emo.height + "px" : emo.height;
const name = createEl("div", {
text: emo.name,
className: "emoji-manager-card-name"
});
const actions = createEl("div", { className: "emoji-manager-card-actions" });
const edit = createEl("button", {
text: "编辑",
className: "btn btn-sm"
});
edit.addEventListener("click", () => {
showEditorFor(group.id, idx);
});
const del = createEl("button", {
text: "删除",
className: "btn btn-sm"
});
del.addEventListener("click", () => {
group.emojis.splice(idx, 1);
renderGroups();
renderSelectedGroup();
});
actions.appendChild(edit);
actions.appendChild(del);
card.appendChild(img);
card.appendChild(name);
card.appendChild(actions);
emojisContainer.appendChild(card);
bindHoverPreview(img, emo);
});
}
let hoverPreviewEl = null;
function ensureHoverPreview$1() {
if (hoverPreviewEl && document.body.contains(hoverPreviewEl)) return hoverPreviewEl;
hoverPreviewEl = createEl("img", { className: "emoji-manager-hover-preview" });
document.body.appendChild(hoverPreviewEl);
return hoverPreviewEl;
}
function bindHoverPreview(targetImg, emo) {
const preview = ensureHoverPreview$1();
function onEnter(e) {
preview.src = emo.url;
if (emo.width) preview.style.width = typeof emo.width === "number" ? emo.width + "px" : emo.width;
else preview.style.width = "";
if (emo.height) preview.style.height = typeof emo.height === "number" ? emo.height + "px" : emo.height;
else preview.style.height = "";
preview.style.display = "block";
movePreview(e);
}
function movePreview(e) {
const pad = 12;
const vw = window.innerWidth;
const vh = window.innerHeight;
const rect = preview.getBoundingClientRect();
let left$1 = e.clientX + pad;
let top = e.clientY + pad;
if (left$1 + rect.width > vw) left$1 = e.clientX - rect.width - pad;
if (top + rect.height > vh) top = e.clientY - rect.height - pad;
preview.style.left = left$1 + "px";
preview.style.top = top + "px";
}
function onLeave() {
if (preview) preview.style.display = "none";
}
targetImg.addEventListener("mouseenter", onEnter);
targetImg.addEventListener("mousemove", movePreview);
targetImg.addEventListener("mouseleave", onLeave);
}
addGroupBtn.addEventListener("click", () => {
const id = (addGroupInput.value || "").trim();
if (!id) return alert("请输入分组 id");
if (userscriptState.emojiGroups.find((g) => g.id === id)) return alert("分组已存在");
userscriptState.emojiGroups.push({
id,
name: id,
emojis: []
});
addGroupInput.value = "";
const newIdx = userscriptState.emojiGroups.findIndex((g) => g.id === id);
if (newIdx >= 0) selectedGroupId = userscriptState.emojiGroups[newIdx].id;
renderGroups();
renderSelectedGroup();
});
addEmojiBtn.addEventListener("click", () => {
if (!selectedGroupId) return alert("请先选择分组");
const url = (emojiUrlInput.value || "").trim();
const name = (emojiNameInput.value || "").trim();
const widthVal = (emojiWidthInput.value || "").trim();
const heightVal = (emojiHeightInput.value || "").trim();
const width = widthVal ? parseInt(widthVal, 10) : NaN;
const height = heightVal ? parseInt(heightVal, 10) : NaN;
if (!url || !name) return alert("请输入 url 和 名称");
const group = userscriptState.emojiGroups.find((g) => g.id === selectedGroupId);
if (!group) return;
group.emojis = group.emojis || [];
const newEmo = {
url,
name
};
if (!isNaN(width) && width > 0) newEmo.width = width;
if (!isNaN(height) && height > 0) newEmo.height = height;
group.emojis.push(newEmo);
emojiUrlInput.value = "";
emojiNameInput.value = "";
emojiWidthInput.value = "";
emojiHeightInput.value = "";
renderGroups();
renderSelectedGroup();
});
deleteGroupBtn.addEventListener("click", () => {
if (!selectedGroupId) return alert("请先选择分组");
const idx = userscriptState.emojiGroups.findIndex((g) => g.id === selectedGroupId);
if (idx >= 0) {
if (!confirm("确认删除该分组?该操作不可撤销")) return;
userscriptState.emojiGroups.splice(idx, 1);
if (userscriptState.emojiGroups.length > 0) selectedGroupId = userscriptState.emojiGroups[Math.min(idx, userscriptState.emojiGroups.length - 1)].id;
else selectedGroupId = null;
renderGroups();
renderSelectedGroup();
}
});
exportBtn.addEventListener("click", () => {
const data = exportUserscriptData();
navigator.clipboard.writeText(data).then(() => alert("已复制到剪贴板")).catch(() => {
const ta = createEl("textarea", { value: data });
document.body.appendChild(ta);
ta.select();
});
});
importBtn.addEventListener("click", () => {
const ta = createEl("textarea", {
placeholder: "粘贴 JSON 后点击确认",
style: "width:100%;height:200px;margin-top:8px;"
});
const ok = createEl("button", {
text: "确认导入",
style: "padding:6px 8px;margin-top:6px;"
});
const container = createEl("div");
container.appendChild(ta);
container.appendChild(ok);
const importModal = createEl("div", { style: "position:fixed;left:0;top:0;right:0;bottom:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:1000001;" });
const box = createEl("div", { style: "background:#fff;padding:12px;border-radius:6px;width:90%;max-width:700px;" });
box.appendChild(container);
importModal.appendChild(box);
document.body.appendChild(importModal);
ok.addEventListener("click", () => {
try {
const json = ta.value.trim();
if (!json) return;
if (importUserscriptData(json)) {
alert("导入成功,请保存以持久化");
loadDataFromLocalStorage$1();
renderGroups();
renderSelectedGroup();
} else alert("导入失败:格式错误");
} catch (e) {
alert("导入异常:" + e);
}
importModal.remove();
});
});
saveBtn.addEventListener("click", () => {
try {
saveDataToLocalStorage({ emojiGroups: userscriptState.emojiGroups });
alert("已保存");
} catch (e) {
alert("保存失败:" + e);
}
});
syncBtn.addEventListener("click", () => {
try {
if (syncFromManager()) {
alert("同步成功,已导入管理器数据");
loadDataFromLocalStorage$1();
renderGroups();
renderSelectedGroup();
} else alert("同步未成功,未检测到管理器数据");
} catch (e) {
alert("同步异常:" + e);
}
});
closeBtn.addEventListener("click", () => modal.remove());
modal.addEventListener("click", (e) => {
if (e.target === modal) modal.remove();
});
renderGroups();
if (userscriptState.emojiGroups.length > 0) {
selectedGroupId = userscriptState.emojiGroups[0].id;
const first = groupsList.firstChild;
if (first) first.style.background = "#f0f8ff";
renderSelectedGroup();
}
}
function loadDataFromLocalStorage$1() {
console.log("Data reload requested");
}
var init_manager = __esmMin((() => {
init_styles();
init_createEl();
init_userscript_storage();
}));
var settings_exports = __export({ showSettingsModal: () => showSettingsModal });
function showSettingsModal() {
const modal = createEl("div", { style: `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 999999;
display: flex;
align-items: center;
justify-content: center;
` });
const content = createEl("div", {
style: `
background: white;
border-radius: 8px;
padding: 24px;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
position: relative;
`,
innerHTML: `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h2 style="margin: 0; color: #333;">设置</h2>
<button id="closeModal" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #999;">×</button>
</div>
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 8px; color: #555; font-weight: 500;">图片缩放比例: <span id="scaleValue">${userscriptState.settings.imageScale}%</span></label>
<input type="range" id="scaleSlider" min="5" max="150" step="5" value="${userscriptState.settings.imageScale}"
style="width: 100%; margin-bottom: 8px;">
</div>
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 8px; color: #555; font-weight: 500;">输出格式:</label>
<div style="display: flex; gap: 16px;">
<label style="display: flex; align-items: center; color: #666;">
<input type="radio" name="outputFormat" value="markdown" ${userscriptState.settings.outputFormat === "markdown" ? "checked" : ""} style="margin-right: 4px;">
Markdown
</label>
<label style="display: flex; align-items: center; color: #666;">
<input type="radio" name="outputFormat" value="html" ${userscriptState.settings.outputFormat === "html" ? "checked" : ""} style="margin-right: 4px;">
HTML
</label>
</div>
</div>
<div style="margin-bottom: 16px;">
<label style="display: flex; align-items: center; color: #555; font-weight: 500;">
<input type="checkbox" id="showSearchBar" ${userscriptState.settings.showSearchBar ? "checked" : ""} style="margin-right: 8px;">
显示搜索栏
</label>
</div>
<div style="margin-bottom: 16px;">
<label style="display: flex; align-items: center; color: #555; font-weight: 500;">
<input type="checkbox" id="forceMobileMode" ${userscriptState.settings.forceMobileMode ? "checked" : ""} style="margin-right: 8px;">
强制移动模式 (在不兼容检测时也注入移动版布局)
</label>
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end;">
<button id="resetSettings" style="padding: 8px 16px; background: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; cursor: pointer;">重置</button>
<button id="saveSettings" style="padding: 8px 16px; background: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer;">保存</button>
</div>
`
});
modal.appendChild(content);
document.body.appendChild(modal);
const scaleSlider = content.querySelector("#scaleSlider");
const scaleValue = content.querySelector("#scaleValue");
scaleSlider?.addEventListener("input", () => {
if (scaleValue) scaleValue.textContent = scaleSlider.value + "%";
});
content.querySelector("#closeModal")?.addEventListener("click", () => {
modal.remove();
});
content.querySelector("#resetSettings")?.addEventListener("click", async () => {
if (confirm("确定要重置所有设置吗?")) {
userscriptState.settings = {
imageScale: 30,
gridColumns: 4,
outputFormat: "markdown",
forceMobileMode: false,
defaultGroup: "nachoneko",
showSearchBar: true
};
modal.remove();
}
});
content.querySelector("#saveSettings")?.addEventListener("click", () => {
userscriptState.settings.imageScale = parseInt(scaleSlider?.value || "30");
const outputFormat = content.querySelector("input[name=\"outputFormat\"]:checked");
if (outputFormat) userscriptState.settings.outputFormat = outputFormat.value;
const showSearchBar = content.querySelector("#showSearchBar");
if (showSearchBar) userscriptState.settings.showSearchBar = showSearchBar.checked;
const forceMobileEl = content.querySelector("#forceMobileMode");
if (forceMobileEl) userscriptState.settings.forceMobileMode = !!forceMobileEl.checked;
saveDataToLocalStorage({ settings: userscriptState.settings });
try {
const remoteInput = content.querySelector("#remoteConfigUrl");
if (remoteInput && remoteInput.value.trim()) localStorage.setItem("emoji_extension_remote_config_url", remoteInput.value.trim());
} catch (e) {}
alert("设置已保存");
modal.remove();
});
modal.addEventListener("click", (e) => {
if (e.target === modal) modal.remove();
});
}
var init_settings = __esmMin((() => {
init_state();
init_userscript_storage();
init_createEl();
}));
init_state();
init_createEl();
function isMobileView() {
try {
return !!(userscriptState && userscriptState.settings && userscriptState.settings.forceMobileMode);
} catch (e) {
return false;
}
}
function insertEmojiIntoEditor(emoji) {
console.log("[Emoji Extension Userscript] Inserting emoji:", emoji);
const textarea = document.querySelector("textarea.d-editor-input");
const proseMirror = document.querySelector(".ProseMirror.d-editor-input");
if (!textarea && !proseMirror) {
console.error("找不到输入框");
return;
}
const dimensionMatch = emoji.url?.match(/_(\d{3,})x(\d{3,})\./);
let width = "500";
let height = "500";
if (dimensionMatch) {
width = dimensionMatch[1];
height = dimensionMatch[2];
} else if (emoji.width && emoji.height) {
width = emoji.width.toString();
height = emoji.height.toString();
}
const scale = userscriptState.settings?.imageScale || 30;
const outputFormat = userscriptState.settings?.outputFormat || "markdown";
if (textarea) {
let insertText = "";
if (outputFormat === "html") {
const scaledWidth = Math.max(1, Math.round(Number(width) * (scale / 100)));
const scaledHeight = Math.max(1, Math.round(Number(height) * (scale / 100)));
insertText = `<img src="${emoji.url}" title=":${emoji.name}:" class="emoji only-emoji" alt=":${emoji.name}:" loading="lazy" width="${scaledWidth}" height="${scaledHeight}" style="aspect-ratio: ${scaledWidth} / ${scaledHeight};"> `;
} else insertText = ` `;
const selectionStart = textarea.selectionStart;
const selectionEnd = textarea.selectionEnd;
textarea.value = textarea.value.substring(0, selectionStart) + insertText + textarea.value.substring(selectionEnd, textarea.value.length);
textarea.selectionStart = textarea.selectionEnd = selectionStart + insertText.length;
textarea.focus();
const inputEvent = new Event("input", {
bubbles: true,
cancelable: true
});
textarea.dispatchEvent(inputEvent);
} else if (proseMirror) {
const imgWidth = Number(width) || 500;
const scaledWidth = Math.max(1, Math.round(imgWidth * (scale / 100)));
const htmlContent = `<img src="${emoji.url}" alt="${emoji.name}" width="${width}" height="${height}" data-scale="${scale}" style="width: ${scaledWidth}px">`;
try {
const dataTransfer = new DataTransfer();
dataTransfer.setData("text/html", htmlContent);
const pasteEvent = new ClipboardEvent("paste", {
clipboardData: dataTransfer,
bubbles: true
});
proseMirror.dispatchEvent(pasteEvent);
} catch (error) {
try {
document.execCommand("insertHTML", false, htmlContent);
} catch (fallbackError) {
console.error("无法向富文本编辑器中插入表情", fallbackError);
}
}
}
}
var _hoverPreviewEl = null;
function ensureHoverPreview() {
if (_hoverPreviewEl && document.body.contains(_hoverPreviewEl)) return _hoverPreviewEl;
_hoverPreviewEl = createEl("div", {
className: "emoji-picker-hover-preview",
style: "position:fixed;pointer-events:none;display:none;z-index:1000002;max-width:300px;max-height:300px;overflow:hidden;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,0.25);background:#fff;padding:6px;"
});
const img = createEl("img", {
className: "emoji-picker-hover-img",
style: "display:block;max-width:100%;max-height:220px;object-fit:contain;"
});
const label = createEl("div", {
className: "emoji-picker-hover-label",
style: "font-size:12px;color:#333;margin-top:6px;text-align:center;"
});
_hoverPreviewEl.appendChild(img);
_hoverPreviewEl.appendChild(label);
document.body.appendChild(_hoverPreviewEl);
return _hoverPreviewEl;
}
function createMobileEmojiPicker(groups) {
const modal = createEl("div", {
className: "modal d-modal fk-d-menu-modal emoji-picker-content",
attrs: {
"data-identifier": "emoji-picker",
"data-keyboard": "false",
"aria-modal": "true",
role: "dialog"
}
});
const modalContainerDiv = createEl("div", { className: "d-modal__container" });
const modalBody = createEl("div", { className: "d-modal__body" });
modalBody.tabIndex = -1;
const emojiPickerDiv = createEl("div", { className: "emoji-picker" });
const filterContainer = createEl("div", { className: "emoji-picker__filter-container" });
const filterInputContainer = createEl("div", { className: "emoji-picker__filter filter-input-container" });
const filterInput = createEl("input", {
className: "filter-input",
placeholder: "按表情符号名称搜索…",
type: "text"
});
filterInputContainer.appendChild(filterInput);
const closeButton = createEl("button", {
className: "btn no-text btn-icon btn-transparent emoji-picker__close-btn",
type: "button",
innerHTML: `<svg class="fa d-icon d-icon-xmark svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#xmark"></use></svg>`
});
closeButton.addEventListener("click", () => {
const container = modal.closest(".modal-container") || modal;
if (container) container.remove();
});
filterContainer.appendChild(filterInputContainer);
filterContainer.appendChild(closeButton);
const content = createEl("div", { className: "emoji-picker__content" });
const sectionsNav = createEl("div", { className: "emoji-picker__sections-nav" });
const managementButton = createEl("button", {
className: "btn no-text btn-flat emoji-picker__section-btn management-btn",
attrs: {
tabindex: "-1",
style: "border-right: 1px solid #ddd;"
},
innerHTML: "⚙️",
title: "管理表情 - 点击打开完整管理界面",
type: "button"
});
managementButton.addEventListener("click", () => {
__vitePreload(async () => {
const { openManagementInterface: openManagementInterface$1 } = await Promise.resolve().then(() => (init_manager(), manager_exports));
return { openManagementInterface: openManagementInterface$1 };
}, void 0).then(({ openManagementInterface: openManagementInterface$1 }) => {
openManagementInterface$1();
});
});
sectionsNav.appendChild(managementButton);
const settingsButton = createEl("button", {
className: "btn no-text btn-flat emoji-picker__section-btn settings-btn",
innerHTML: "🔧",
title: "设置",
attrs: {
tabindex: "-1",
style: "border-right: 1px solid #ddd;"
},
type: "button"
});
settingsButton.addEventListener("click", () => {
__vitePreload(async () => {
const { showSettingsModal: showSettingsModal$1 } = await Promise.resolve().then(() => (init_settings(), settings_exports));
return { showSettingsModal: showSettingsModal$1 };
}, void 0).then(({ showSettingsModal: showSettingsModal$1 }) => {
showSettingsModal$1();
});
});
sectionsNav.appendChild(settingsButton);
const scrollableContent = createEl("div", { className: "emoji-picker__scrollable-content" });
const sections = createEl("div", {
className: "emoji-picker__sections",
attrs: { role: "button" }
});
let hoverPreviewEl = null;
function ensureHoverPreview$1() {
if (hoverPreviewEl && document.body.contains(hoverPreviewEl)) return hoverPreviewEl;
hoverPreviewEl = createEl("div", {
className: "emoji-picker-hover-preview",
style: "position:fixed;pointer-events:none;display:none;z-index:1000002;max-width:300px;max-height:300px;overflow:hidden;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,0.25);background:#fff;padding:6px;"
});
const img = createEl("img", {
className: "emoji-picker-hover-img",
style: "display:block;max-width:100%;max-height:220px;object-fit:contain;"
});
const label = createEl("div", {
className: "emoji-picker-hover-label",
style: "font-size:12px;color:#333;margin-top:6px;text-align:center;"
});
hoverPreviewEl.appendChild(img);
hoverPreviewEl.appendChild(label);
document.body.appendChild(hoverPreviewEl);
return hoverPreviewEl;
}
groups.forEach((group, index) => {
if (!group?.emojis?.length) return;
const navButton = createEl("button", {
className: `btn no-text btn-flat emoji-picker__section-btn ${index === 0 ? "active" : ""}`,
attrs: {
tabindex: "-1",
"data-section": group.id,
type: "button"
}
});
const iconVal = group.icon || "📁";
if (isImageUrl(iconVal)) {
const img = createEl("img", {
src: iconVal,
alt: group.name || "",
className: "emoji",
style: "width: 18px; height: 18px; object-fit: contain;"
});
navButton.appendChild(img);
} else navButton.textContent = String(iconVal);
navButton.title = group.name;
navButton.addEventListener("click", () => {
sectionsNav.querySelectorAll(".emoji-picker__section-btn").forEach((btn) => btn.classList.remove("active"));
navButton.classList.add("active");
const target = sections.querySelector(`[data-section="${group.id}"]`);
if (target) target.scrollIntoView({
behavior: "smooth",
block: "start"
});
});
sectionsNav.appendChild(navButton);
const section = createEl("div", {
className: "emoji-picker__section",
attrs: {
"data-section": group.id,
role: "region",
"aria-label": group.name
}
});
const titleContainer = createEl("div", { className: "emoji-picker__section-title-container" });
const title = createEl("h2", {
className: "emoji-picker__section-title",
text: group.name
});
titleContainer.appendChild(title);
const sectionEmojis = createEl("div", { className: "emoji-picker__section-emojis" });
group.emojis.forEach((emoji) => {
if (!emoji || typeof emoji !== "object" || !emoji.url || !emoji.name) return;
const img = createEl("img", {
src: emoji.url,
alt: emoji.name,
className: "emoji",
title: `:${emoji.name}:`,
style: "width: 32px; height: 32px; object-fit: contain;",
attrs: {
"data-emoji": emoji.name,
tabindex: "0",
loading: "lazy"
}
});
(function bindHover(imgEl, emo) {
const preview = ensureHoverPreview$1();
const previewImg = preview.querySelector("img");
const previewLabel = preview.querySelector(".emoji-picker-hover-label");
function onEnter(e) {
previewImg.src = emo.url;
previewLabel.textContent = emo.name || "";
preview.style.display = "block";
move(e);
}
function move(e) {
const pad = 12;
const vw = window.innerWidth;
const vh = window.innerHeight;
const rect = preview.getBoundingClientRect();
let left = e.clientX + pad;
let top = e.clientY + pad;
if (left + rect.width > vw) left = e.clientX - rect.width - pad;
if (top + rect.height > vh) top = e.clientY - rect.height - pad;
preview.style.left = left + "px";
preview.style.top = top + "px";
}
function onLeave() {
preview.style.display = "none";
}
imgEl.addEventListener("mouseenter", onEnter);
imgEl.addEventListener("mousemove", move);
imgEl.addEventListener("mouseleave", onLeave);
})(img, emoji);
img.addEventListener("click", () => {
insertEmojiIntoEditor(emoji);
const modalContainer = modal.closest(".modal-container");
if (modalContainer) modalContainer.remove();
else modal.remove();
});
img.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
insertEmojiIntoEditor(emoji);
const modalContainer = modal.closest(".modal-container");
if (modalContainer) modalContainer.remove();
else modal.remove();
}
});
sectionEmojis.appendChild(img);
});
section.appendChild(titleContainer);
section.appendChild(sectionEmojis);
sections.appendChild(section);
});
filterInput.addEventListener("input", (e) => {
const q = (e.target.value || "").toLowerCase();
sections.querySelectorAll("img").forEach((img) => {
const emojiName = (img.dataset.emoji || "").toLowerCase();
img.style.display = q === "" || emojiName.includes(q) ? "" : "none";
});
sections.querySelectorAll(".emoji-picker__section").forEach((section) => {
const visibleEmojis = section.querySelectorAll("img:not([style*=\"display: none\"])");
section.style.display = visibleEmojis.length > 0 ? "" : "none";
});
});
scrollableContent.appendChild(sections);
content.appendChild(sectionsNav);
content.appendChild(scrollableContent);
emojiPickerDiv.appendChild(filterContainer);
emojiPickerDiv.appendChild(content);
modalBody.appendChild(emojiPickerDiv);
modalContainerDiv.appendChild(modalBody);
modal.appendChild(modalContainerDiv);
return modal;
}
function createDesktopEmojiPicker(groups) {
const picker = createEl("div", {
className: "fk-d-menu -animated -expanded",
style: "max-width: 400px; visibility: visible; z-index: 999999;",
attrs: {
"data-identifier": "emoji-picker",
role: "dialog"
}
});
const innerContent = createEl("div", { className: "fk-d-menu__inner-content" });
const emojiPickerDiv = createEl("div", { className: "emoji-picker" });
const filterContainer = createEl("div", { className: "emoji-picker__filter-container" });
const filterDiv = createEl("div", { className: "emoji-picker__filter filter-input-container" });
const searchInput = createEl("input", {
className: "filter-input",
placeholder: "按表情符号名称搜索…",
type: "text"
});
filterDiv.appendChild(searchInput);
filterContainer.appendChild(filterDiv);
const content = createEl("div", { className: "emoji-picker__content" });
const sectionsNav = createEl("div", { className: "emoji-picker__sections-nav" });
const managementButton = createEl("button", {
className: "btn no-text btn-flat emoji-picker__section-btn management-btn",
attrs: {
tabindex: "-1",
style: "border-right: 1px solid #ddd;"
},
type: "button",
innerHTML: "⚙️",
title: "管理表情 - 点击打开完整管理界面"
});
managementButton.addEventListener("click", () => {
__vitePreload(async () => {
const { openManagementInterface: openManagementInterface$1 } = await Promise.resolve().then(() => (init_manager(), manager_exports));
return { openManagementInterface: openManagementInterface$1 };
}, void 0).then(({ openManagementInterface: openManagementInterface$1 }) => {
openManagementInterface$1();
});
});
sectionsNav.appendChild(managementButton);
const settingsButton = createEl("button", {
className: "btn no-text btn-flat emoji-picker__section-btn settings-btn",
attrs: {
tabindex: "-1",
style: "border-right: 1px solid #ddd;"
},
type: "button",
innerHTML: "🔧",
title: "设置"
});
settingsButton.addEventListener("click", () => {
__vitePreload(async () => {
const { showSettingsModal: showSettingsModal$1 } = await Promise.resolve().then(() => (init_settings(), settings_exports));
return { showSettingsModal: showSettingsModal$1 };
}, void 0).then(({ showSettingsModal: showSettingsModal$1 }) => {
showSettingsModal$1();
});
});
sectionsNav.appendChild(settingsButton);
const scrollableContent = createEl("div", { className: "emoji-picker__scrollable-content" });
const sections = createEl("div", {
className: "emoji-picker__sections",
attrs: { role: "button" }
});
groups.forEach((group, index) => {
if (!group?.emojis?.length) return;
const navButton = createEl("button", {
className: `btn no-text btn-flat emoji-picker__section-btn ${index === 0 ? "active" : ""}`,
attrs: {
tabindex: "-1",
"data-section": group.id
},
type: "button"
});
const iconVal = group.icon || "📁";
if (isImageUrl(iconVal)) {
const img = createEl("img", {
src: iconVal,
alt: group.name || "",
className: "emoji-group-icon",
style: "width: 18px; height: 18px; object-fit: contain;"
});
navButton.appendChild(img);
} else navButton.textContent = String(iconVal);
navButton.title = group.name;
navButton.addEventListener("click", () => {
sectionsNav.querySelectorAll(".emoji-picker__section-btn").forEach((btn) => btn.classList.remove("active"));
navButton.classList.add("active");
const target = sections.querySelector(`[data-section="${group.id}"]`);
if (target) target.scrollIntoView({
behavior: "smooth",
block: "start"
});
});
sectionsNav.appendChild(navButton);
const section = createEl("div", {
className: "emoji-picker__section",
attrs: {
"data-section": group.id,
role: "region",
"aria-label": group.name
}
});
const titleContainer = createEl("div", { className: "emoji-picker__section-title-container" });
const title = createEl("h2", {
className: "emoji-picker__section-title",
text: group.name
});
titleContainer.appendChild(title);
const sectionEmojis = createEl("div", { className: "emoji-picker__section-emojis" });
let added = 0;
group.emojis.forEach((emoji) => {
if (!emoji || typeof emoji !== "object" || !emoji.url || !emoji.name) return;
const img = createEl("img", {
width: "32px",
height: "32px",
className: "emoji",
src: emoji.url,
alt: emoji.name,
title: `:${emoji.name}:`,
attrs: {
"data-emoji": emoji.name,
tabindex: "0",
loading: "lazy"
}
});
(function bindHover(imgEl, emo) {
const preview = ensureHoverPreview();
const previewImg = preview.querySelector("img");
const previewLabel = preview.querySelector(".emoji-picker-hover-label");
function onEnter(e) {
previewImg.src = emo.url;
previewLabel.textContent = emo.name || "";
preview.style.display = "block";
move(e);
}
function move(e) {
const pad = 12;
const vw = window.innerWidth;
const vh = window.innerHeight;
const rect = preview.getBoundingClientRect();
let left = e.clientX + pad;
let top = e.clientY + pad;
if (left + rect.width > vw) left = e.clientX - rect.width - pad;
if (top + rect.height > vh) top = e.clientY - rect.height - pad;
preview.style.left = left + "px";
preview.style.top = top + "px";
}
function onLeave() {
preview.style.display = "none";
}
imgEl.addEventListener("mouseenter", onEnter);
imgEl.addEventListener("mousemove", move);
imgEl.addEventListener("mouseleave", onLeave);
})(img, emoji);
img.addEventListener("click", () => {
insertEmojiIntoEditor(emoji);
picker.remove();
});
img.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
insertEmojiIntoEditor(emoji);
picker.remove();
}
});
sectionEmojis.appendChild(img);
added++;
});
if (added === 0) {
const msg = createEl("div", {
text: `${group.name} 组暂无有效表情`,
style: "padding: 20px; text-align: center; color: #999;"
});
sectionEmojis.appendChild(msg);
}
section.appendChild(titleContainer);
section.appendChild(sectionEmojis);
sections.appendChild(section);
});
searchInput.addEventListener("input", (e) => {
const q = (e.target.value || "").toLowerCase();
sections.querySelectorAll("img").forEach((img) => {
const emojiName = img.getAttribute("data-emoji")?.toLowerCase() || "";
img.style.display = q === "" || emojiName.includes(q) ? "" : "none";
});
sections.querySelectorAll(".emoji-picker__section").forEach((section) => {
const visibleEmojis = section.querySelectorAll("img:not([style*=\"none\"])");
const titleContainer = section.querySelector(".emoji-picker__section-title-container");
if (titleContainer) titleContainer.style.display = visibleEmojis.length > 0 ? "" : "none";
});
});
scrollableContent.appendChild(sections);
content.appendChild(sectionsNav);
content.appendChild(scrollableContent);
emojiPickerDiv.appendChild(filterContainer);
emojiPickerDiv.appendChild(content);
innerContent.appendChild(emojiPickerDiv);
picker.appendChild(innerContent);
return picker;
}
async function createEmojiPicker() {
const groups = userscriptState.emojiGroups;
const mobile = isMobileView();
try {
injectEmojiPickerStyles();
} catch (e) {
console.warn("injectEmojiPickerStyles failed", e);
}
if (mobile) return createMobileEmojiPicker(groups);
else return createDesktopEmojiPicker(groups);
}
init_createEl();
var toolbarSelectors = [".d-editor-button-bar[role=\"toolbar\"]", ".chat-composer__inner-container"];
function findAllToolbars() {
const toolbars = [];
for (const selector of toolbarSelectors) {
const elements = document.querySelectorAll(selector);
toolbars.push(...Array.from(elements));
}
return toolbars;
}
var currentPicker = null;
function closeCurrentPicker() {
if (currentPicker) {
currentPicker.remove();
currentPicker = null;
}
}
function injectEmojiButton(toolbar) {
if (toolbar.querySelector(".emoji-extension-button")) return;
const isChatComposer = toolbar.classList.contains("chat-composer__inner-container");
const button = createEl("button", {
className: "btn no-text btn-icon toolbar__button nacho-emoji-picker-button emoji-extension-button",
title: "表情包",
type: "button",
innerHTML: "🐈⬛"
});
if (isChatComposer) {
button.classList.add("fk-d-menu__trigger", "emoji-picker-trigger", "chat-composer-button", "btn-transparent", "-emoji");
button.setAttribute("aria-expanded", "false");
button.setAttribute("data-identifier", "emoji-picker");
button.setAttribute("data-trigger", "");
}
button.addEventListener("click", async (e) => {
e.stopPropagation();
if (currentPicker) {
closeCurrentPicker();
return;
}
currentPicker = await createEmojiPicker();
if (!currentPicker) return;
document.body.appendChild(currentPicker);
const buttonRect = button.getBoundingClientRect();
if (currentPicker.classList.contains("modal") || currentPicker.className.includes("d-modal")) {
currentPicker.style.position = "fixed";
currentPicker.style.top = "0";
currentPicker.style.left = "0";
currentPicker.style.right = "0";
currentPicker.style.bottom = "0";
currentPicker.style.zIndex = "999999";
} else {
currentPicker.style.position = "fixed";
const margin = 8;
const vpWidth = window.innerWidth;
const vpHeight = window.innerHeight;
currentPicker.style.top = buttonRect.bottom + margin + "px";
currentPicker.style.left = buttonRect.left + "px";
const pickerRect = currentPicker.getBoundingClientRect();
const spaceBelow = vpHeight - buttonRect.bottom;
const neededHeight = pickerRect.height + margin;
let top = buttonRect.bottom + margin;
if (spaceBelow < neededHeight) top = Math.max(margin, buttonRect.top - pickerRect.height - margin);
let left = buttonRect.left;
if (left + pickerRect.width + margin > vpWidth) left = Math.max(margin, vpWidth - pickerRect.width - margin);
if (left < margin) left = margin;
currentPicker.style.top = top + "px";
currentPicker.style.left = left + "px";
}
setTimeout(() => {
const handleClick = (e$1) => {
if (currentPicker && !currentPicker.contains(e$1.target) && e$1.target !== button) {
closeCurrentPicker();
document.removeEventListener("click", handleClick);
}
};
document.addEventListener("click", handleClick);
}, 100);
});
try {
if (isChatComposer) {
const existingEmojiTrigger = toolbar.querySelector(".emoji-picker-trigger:not(.emoji-extension-button)");
if (existingEmojiTrigger) toolbar.insertBefore(button, existingEmojiTrigger);
else toolbar.appendChild(button);
} else toolbar.appendChild(button);
} catch (error) {
console.error("[Emoji Extension Userscript] Failed to inject button:", error);
}
}
function attemptInjection() {
const toolbars = findAllToolbars();
let injectedCount = 0;
toolbars.forEach((toolbar) => {
if (!toolbar.querySelector(".emoji-extension-button")) {
console.log("[Emoji Extension Userscript] Toolbar found, injecting button.");
injectEmojiButton(toolbar);
injectedCount++;
}
});
return {
injectedCount,
totalToolbars: toolbars.length
};
}
function startPeriodicInjection() {
setInterval(() => {
findAllToolbars().forEach((toolbar) => {
if (!toolbar.querySelector(".emoji-extension-button")) {
console.log("[Emoji Extension Userscript] New toolbar found, injecting button.");
injectEmojiButton(toolbar);
}
});
}, 3e4);
}
init_userscript_storage();
init_state();
async function initializeUserscriptData() {
const data = await loadDataFromLocalStorageAsync().catch((err) => {
console.warn("[Userscript] loadDataFromLocalStorageAsync failed, falling back to sync loader", err);
return loadDataFromLocalStorage();
});
userscriptState.emojiGroups = data.emojiGroups || [];
userscriptState.settings = data.settings || userscriptState.settings;
}
function shouldInjectEmoji() {
if (document.querySelectorAll("meta[name*=\"discourse\"], meta[content*=\"discourse\"], meta[property*=\"discourse\"]").length > 0) {
console.log("[Emoji Extension Userscript] Discourse detected via meta tags");
return true;
}
const generatorMeta = document.querySelector("meta[name=\"generator\"]");
if (generatorMeta) {
const content = generatorMeta.getAttribute("content")?.toLowerCase() || "";
if (content.includes("discourse") || content.includes("flarum") || content.includes("phpbb")) {
console.log("[Emoji Extension Userscript] Forum platform detected via generator meta");
return true;
}
}
const hostname = window.location.hostname.toLowerCase();
if ([
"linux.do",
"meta.discourse.org",
"pixiv.net"
].some((domain) => hostname.includes(domain))) {
console.log("[Emoji Extension Userscript] Allowed domain detected:", hostname);
return true;
}
if (document.querySelectorAll("textarea.d-editor-input, .ProseMirror.d-editor-input, .composer-input, .reply-area textarea").length > 0) {
console.log("[Emoji Extension Userscript] Discussion editor detected");
return true;
}
console.log("[Emoji Extension Userscript] No compatible platform detected");
return false;
}
async function initializeEmojiFeature(maxAttempts = 10, delay = 1e3) {
console.log("[Emoji Extension Userscript] Initializing...");
await initializeUserscriptData();
initOneClickAdd();
let attempts = 0;
function attemptToolbarInjection() {
attempts++;
const result = attemptInjection();
if (result.injectedCount > 0 || result.totalToolbars > 0) {
console.log(`[Emoji Extension Userscript] Injection successful: ${result.injectedCount} buttons injected into ${result.totalToolbars} toolbars`);
return;
}
if (attempts < maxAttempts) {
console.log(`[Emoji Extension Userscript] Toolbar not found, attempt ${attempts}/${maxAttempts}. Retrying in ${delay / 1e3}s.`);
setTimeout(attemptToolbarInjection, delay);
} else console.error("[Emoji Extension Userscript] Failed to find toolbar after multiple attempts.");
}
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", attemptToolbarInjection);
else attemptToolbarInjection();
startPeriodicInjection();
}
if (shouldInjectEmoji()) {
console.log("[Emoji Extension Userscript] Initializing emoji feature");
initializeEmojiFeature();
} else console.log("[Emoji Extension Userscript] Skipping injection - incompatible platform");
})();
})();