// ==UserScript==
// @name Linux do 表情扩展 (Emoji Extension) lite
// @namespace https://github.com/stevessr/bug-v3
// @version 1.0.6
// @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,
enableFloatingPreview: 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,
enableFloatingPreview: 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,
enableFloatingPreview: 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;
}
}
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;
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";
}));
var userscriptState;
var init_state = __esmMin((() => {
userscriptState = {
emojiGroups: [],
settings: {
imageScale: 30,
gridColumns: 4,
outputFormat: "markdown",
forceMobileMode: false,
defaultGroup: "nachoneko",
showSearchBar: true,
enableFloatingPreview: true
},
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;
}
return el;
}
var init_createEl = __esmMin((() => {}));
init_createEl();
init_state();
init_userscript_storage();
function notify(message, type = "info", timeout = 4e3) {
try {
let container = document.getElementById("emoji-ext-toast-container");
if (!container) {
container = document.createElement("div");
container.id = "emoji-ext-toast-container";
container.style.position = "fixed";
container.style.right = "12px";
container.style.bottom = "12px";
container.style.zIndex = "2147483647";
container.style.display = "flex";
container.style.flexDirection = "column";
container.style.gap = "8px";
document.body.appendChild(container);
}
const el = document.createElement("div");
el.textContent = message;
el.style.padding = "8px 12px";
el.style.borderRadius = "6px";
el.style.boxShadow = "0 2px 8px rgba(0,0,0,0.12)";
el.style.color = "#ffffff";
el.style.fontSize = "13px";
el.style.maxWidth = "320px";
el.style.wordBreak = "break-word";
if (type === "success") el.style.background = "#16a34a";
else if (type === "error") el.style.background = "#dc2626";
else el.style.background = "#0369a1";
container.appendChild(el);
const id = setTimeout(() => {
el.remove();
clearTimeout(id);
}, timeout);
return () => {
el.remove();
clearTimeout(id);
};
} catch (e) {
try {
alert(message);
} catch (_e) {}
return () => {};
}
}
async function postTimings(topicId, timings) {
function readCsrfToken() {
try {
const meta = document.querySelector("meta[name=\"csrf-token\"]");
if (meta && meta.content) return meta.content;
const input = document.querySelector("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://linux.do/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;
return await fetch(url, {
method: "POST",
body: params.toString(),
credentials: "same-origin",
headers
});
}
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;
}
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
};
}
async function autoReadAll(topicId) {
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 = document.querySelector("[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 = [];
for (let n = 1; n <= total; n++) postNumbers.push(n);
const BATCH_SIZE = 7;
for (let i = 0; i < postNumbers.length; i += BATCH_SIZE) {
const batch = postNumbers.slice(i, i + BATCH_SIZE);
const timings = {};
for (const pn of batch) timings[pn] = 1e3;
try {
await postTimings(tid, timings);
notify(`已标记 ${Object.keys(timings).length} 个帖子为已读(发送)`, "success");
} catch (e) {
notify("发送阅读标记失败: " + (e && e.message ? e.message : String(e)), "error");
}
const delay = 500 + Math.floor(Math.random() * 1e3);
await sleep(delay);
}
notify("自动阅读完成", "success");
} catch (e) {
notify("自动阅读异常: " + (e && e.message ? e.message : String(e)), "error");
}
}
window.autoReadAllReplies = autoReadAll;
function insertIntoEditor$1(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$1(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 getBuildPlatform() {
try {
return "original";
} catch {
return "original";
}
}
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() {
const buildPlatform = getBuildPlatform();
if (buildPlatform === "original") return detectRuntimePlatform();
return buildPlatform;
}
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"];
switch (platform) {
case "mobile": return [
...baseSelectors,
".mobile-composer-toolbar",
".chat-composer-mobile",
"[data-mobile-toolbar]",
".discourse-mobile .d-editor-button-bar"
];
case "pc": return [
...baseSelectors,
".desktop-composer-toolbar",
".chat-composer-desktop",
"[data-desktop-toolbar]",
".discourse-desktop .d-editor-button-bar"
];
default: return baseSelectors;
}
}
function logPlatformInfo() {
const buildPlatform = getBuildPlatform();
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 injectGlobalThemeStyles() {
if (themeStylesInjected || typeof document === "undefined") return;
themeStylesInjected = true;
const style = document.createElement("style");
style.id = "emoji-extension-theme-globals";
style.textContent = `
/* 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);
}
}
`;
document.head.appendChild(style);
}
var themeStylesInjected;
var init_themeSupport = __esmMin((() => {
themeStylesInjected = false;
}));
init_themeSupport();
function injectEmojiPickerStyles() {
if (typeof document === "undefined") return;
if (document.getElementById("emoji-picker-styles")) return;
injectGlobalThemeStyles();
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: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;
}
`;
const style = document.createElement("style");
style.id = "emoji-picker-styles";
style.textContent = css;
document.head.appendChild(style);
}
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;
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;
/* Prevent extremely large images from breaking the layout by limiting their
rendered size relative to the card. Use both absolute and percentage-based
constraints so user-provided pixel sizes (from edit form) still work but
will not overflow the card or modal. */
max-width: 90%;
max-height: 60vh; /* allow tall images but cap at viewport height */
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;
/* editor preview should be bounded to avoid huge remote images
while still allowing percentage-based scaling */
max-width: 100%;
max-height: 40vh;
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;
/* For hover previews allow a generous but bounded size relative to viewport
to avoid covering entire UI or pushing content off-screen. */
max-width: 30vw;
max-height: 40vh;
width: auto;
height: auto;
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 = /* @__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"
});
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();
}));
function showGroupEditorModal() {
injectGlobalThemeStyles();
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: var(--emoji-modal-bg);
color: var(--emoji-modal-text);
border-radius: 8px;
padding: 24px;
max-width: 700px;
max-height: 80vh;
overflow-y: auto;
position: relative;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
` });
content.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="margin: 0; color: var(--emoji-modal-text);">表情分组编辑器</h2>
<button id="closeModal" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #999;">×</button>
</div>
<div style="margin-bottom: 20px; padding: 16px; background: var(--emoji-modal-button-bg); border-radius: 6px; border: 1px solid var(--emoji-modal-border);">
<div style="font-weight: 500; color: var(--emoji-modal-label); margin-bottom: 8px;">编辑说明</div>
<div style="font-size: 14px; color: var(--emoji-modal-text); opacity: 0.8; line-height: 1.4;">
• 点击分组名称或图标进行编辑<br>
• 图标支持 emoji 字符或单个字符<br>
• 修改会立即保存到本地存储<br>
• 可以调整分组的显示顺序
</div>
</div>
<div id="groupsList" style="display: flex; flex-direction: column; gap: 12px;">
${userscriptState.emojiGroups.map((group, index) => `
<div class="group-item" data-group-id="${group.id}" data-index="${index}" style="
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: var(--emoji-modal-button-bg);
border: 1px solid var(--emoji-modal-border);
border-radius: 6px;
transition: all 0.2s;
">
<div class="drag-handle" style="
cursor: grab;
color: var(--emoji-modal-text);
opacity: 0.5;
font-size: 16px;
user-select: none;
" title="拖拽调整顺序">⋮⋮</div>
<div class="group-icon-editor" style="
min-width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: var(--emoji-modal-bg);
border: 1px dashed var(--emoji-modal-border);
border-radius: 4px;
cursor: pointer;
font-size: 18px;
user-select: none;
" data-group-id="${group.id}" title="点击编辑图标">
${group.icon || "📁"}
</div>
<div style="flex: 1; display: flex; flex-direction: column; gap: 4px;">
<input class="group-name-editor"
type="text"
value="${group.name || "Unnamed Group"}"
data-group-id="${group.id}"
style="
background: var(--emoji-modal-bg);
color: var(--emoji-modal-text);
border: 1px solid var(--emoji-modal-border);
border-radius: 4px;
padding: 8px 12px;
font-size: 14px;
font-weight: 500;
"
placeholder="分组名称">
<div style="font-size: 12px; color: var(--emoji-modal-text); opacity: 0.6;">
ID: ${group.id} | 表情数: ${group.emojis ? group.emojis.length : 0}
</div>
</div>
<div style="display: flex; flex-direction: column; gap: 4px; align-items: center;">
<button class="move-up" data-index="${index}" style="
background: var(--emoji-modal-button-bg);
border: 1px solid var(--emoji-modal-border);
border-radius: 3px;
padding: 4px 8px;
cursor: pointer;
font-size: 12px;
color: var(--emoji-modal-text);
" ${index === 0 ? "disabled" : ""}>↑</button>
<button class="move-down" data-index="${index}" style="
background: var(--emoji-modal-button-bg);
border: 1px solid var(--emoji-modal-border);
border-radius: 3px;
padding: 4px 8px;
cursor: pointer;
font-size: 12px;
color: var(--emoji-modal-text);
" ${index === userscriptState.emojiGroups.length - 1 ? "disabled" : ""}>↓</button>
</div>
</div>
`).join("")}
</div>
<div style="margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--emoji-modal-border); display: flex; gap: 8px; justify-content: flex-end;">
<button id="addNewGroup" style="padding: 8px 16px; background: var(--emoji-modal-primary-bg); color: white; border: none; border-radius: 4px; cursor: pointer;">新建分组</button>
<button id="saveAllChanges" style="padding: 8px 16px; background: var(--emoji-modal-primary-bg); color: white; border: none; border-radius: 4px; cursor: pointer;">保存所有更改</button>
</div>
`;
modal.appendChild(content);
document.body.appendChild(modal);
const style = document.createElement("style");
style.textContent = `
.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;
}
`;
document.head.appendChild(style);
content.querySelector("#closeModal")?.addEventListener("click", () => {
modal.remove();
style.remove();
});
modal.addEventListener("click", (e) => {
if (e.target === modal) {
modal.remove();
style.remove();
}
});
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$1(`分组 "${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$1(`分组图标已更新为: ${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();
style.remove();
showTemporaryMessage$1("分组顺序已调整");
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();
style.remove();
showTemporaryMessage$1("分组顺序已调整");
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();
style.remove();
showTemporaryMessage$1(`新分组 "${groupName.trim()}" 已创建`);
setTimeout(() => showGroupEditorModal(), 300);
}
});
content.querySelector("#saveAllChanges")?.addEventListener("click", () => {
saveDataToLocalStorage({ emojiGroups: userscriptState.emojiGroups });
showTemporaryMessage$1("所有更改已保存到本地存储");
});
}
function showTemporaryMessage$1(message) {
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 = document.createElement("style");
style.id = "tempMessageStyles";
style.textContent = `
@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(() => {
messageEl.remove();
}, 2e3);
}
var init_groupEditor = __esmMin((() => {
init_state();
init_userscript_storage();
init_createEl();
init_themeSupport();
}));
function showPopularEmojisModal() {
injectGlobalThemeStyles();
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: var(--emoji-modal-bg);
color: var(--emoji-modal-text);
border-radius: 8px;
padding: 24px;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
position: relative;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
` });
const popularEmojis = getPopularEmojis(50);
content.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h2 style="margin: 0; color: var(--emoji-modal-text);">常用表情 (${popularEmojis.length})</h2>
<div style="display: flex; gap: 8px; align-items: center;">
<button id="clearStats" style="padding: 6px 12px; background: #ff4444; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">清空统计</button>
<button id="closeModal" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #999;">×</button>
</div>
</div>
<div style="margin-bottom: 16px; padding: 12px; background: var(--emoji-modal-button-bg); border-radius: 6px; border: 1px solid var(--emoji-modal-border);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span style="font-weight: 500; color: var(--emoji-modal-label);">表情按使用次数排序</span>
<span style="font-size: 12px; color: var(--emoji-modal-text); opacity: 0.7;">点击表情直接使用</span>
</div>
<div style="font-size: 12px; color: var(--emoji-modal-text); opacity: 0.6;">
总使用次数: ${popularEmojis.reduce((sum, emoji) => sum + emoji.count, 0)}
</div>
</div>
<div id="popularEmojiGrid" style="
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 8px;
max-height: 400px;
overflow-y: auto;
">
${popularEmojis.length === 0 ? "<div style=\"grid-column: 1/-1; text-align: center; padding: 40px; color: var(--emoji-modal-text); opacity: 0.7;\">还没有使用过表情<br><small>开始使用表情后,这里会显示常用的表情</small></div>" : popularEmojis.map((emoji) => `
<div class="popular-emoji-item" data-name="${emoji.name}" data-url="${emoji.url}" style="
display: flex;
flex-direction: column;
align-items: center;
padding: 8px;
border: 1px solid var(--emoji-modal-border);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
background: var(--emoji-modal-button-bg);
">
<img src="${emoji.url}" alt="${emoji.name}" style="
width: 40px;
height: 40px;
object-fit: contain;
margin-bottom: 4px;
">
<div style="
font-size: 11px;
font-weight: 500;
color: var(--emoji-modal-text);
text-align: center;
word-break: break-all;
line-height: 1.2;
margin-bottom: 2px;
">${emoji.name}</div>
<div style="
font-size: 10px;
color: var(--emoji-modal-text);
opacity: 0.6;
text-align: center;
">使用${emoji.count}次</div>
</div>
`).join("")}
</div>
${popularEmojis.length > 0 ? `
<div style="margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--emoji-modal-border); font-size: 12px; color: var(--emoji-modal-text); opacity: 0.6; text-align: center;">
统计数据保存在本地,清空统计将重置所有使用记录
</div>
` : ""}
`;
modal.appendChild(content);
document.body.appendChild(modal);
const style = document.createElement("style");
style.textContent = `
.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);
}
`;
document.head.appendChild(style);
content.querySelector("#closeModal")?.addEventListener("click", () => {
modal.remove();
style.remove();
});
content.querySelector("#clearStats")?.addEventListener("click", () => {
if (confirm("确定要清空所有表情使用统计吗?此操作不可撤销。")) {
clearEmojiUsageStats();
modal.remove();
style.remove();
showTemporaryMessage("表情使用统计已清空");
setTimeout(() => showPopularEmojisModal(), 300);
}
});
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();
style.remove();
showTemporaryMessage(`已使用表情: ${name}`);
}
});
});
modal.addEventListener("click", (e) => {
if (e.target === modal) {
modal.remove();
style.remove();
}
});
}
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 = `<img src="${url}" alt="${name}" style="width: ${userscriptState.settings.imageScale}px; height: ${userscriptState.settings.imageScale}px;">`;
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 = `<img src="${url}" alt="${name}" style="width: ${userscriptState.settings.imageScale}px; height: ${userscriptState.settings.imageScale}px;">`;
const textarea = lastTextArea;
textarea.value += emojiText;
textarea.dispatchEvent(new Event("input", { bubbles: true }));
}
}
}
}
function showTemporaryMessage(message) {
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 = document.createElement("style");
style.id = "tempMessageStyles";
style.textContent = `
@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(() => {
messageEl.remove();
}, 2e3);
}
var init_popularEmojis = __esmMin((() => {
init_state();
init_userscript_storage();
init_createEl();
init_themeSupport();
}));
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%;
background: rgba(0, 0, 0, 0.8);
z-index: 999999;
display: flex;
align-items: center;
justify-content: center;
` });
const content = createEl("div", {
style: `
background: var(--emoji-modal-bg);
color: var(--emoji-modal-text);
border-radius: 8px;
padding: 24px;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
position: relative;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
`,
innerHTML: `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h2 style="margin: 0; color: var(--emoji-modal-text);">设置</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: var(--emoji-modal-label); 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: var(--emoji-modal-label); font-weight: 500;">输出格式:</label>
<div style="display: flex; gap: 16px;">
<label style="display: flex; align-items: center; color: var(--emoji-modal-text);">
<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: var(--emoji-modal-text);">
<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: var(--emoji-modal-label); 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: var(--emoji-modal-label); font-weight: 500;">
<input type="checkbox" id="enableFloatingPreview" ${userscriptState.settings.enableFloatingPreview ? "checked" : ""} style="margin-right: 8px;">
启用悬浮预览功能
</label>
</div>
<div style="margin-bottom: 16px;">
<label style="display: flex; align-items: center; color: var(--emoji-modal-label); font-weight: 500;">
<input type="checkbox" id="forceMobileMode" ${userscriptState.settings.forceMobileMode ? "checked" : ""} style="margin-right: 8px;">
强制移动模式 (在不兼容检测时也注入移动版布局)
</label>
</div>
<div style="margin-bottom: 16px; padding: 12px; background: var(--emoji-modal-button-bg); border-radius: 6px; border: 1px solid var(--emoji-modal-border);">
<div style="font-weight: 500; color: var(--emoji-modal-label); margin-bottom: 8px;">高级功能</div>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<button id="openGroupEditor" style="
padding: 6px 12px;
background: var(--emoji-modal-primary-bg);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
">编辑分组</button>
<button id="openPopularEmojis" style="
padding: 6px 12px;
background: var(--emoji-modal-primary-bg);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
">常用表情</button>
</div>
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end;">
<button id="resetSettings" style="padding: 8px 16px; background: var(--emoji-modal-button-bg); color: var(--emoji-modal-text); border: 1px solid var(--emoji-modal-border); border-radius: 4px; cursor: pointer;">重置</button>
<button id="saveSettings" style="padding: 8px 16px; background: var(--emoji-modal-primary-bg); 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,
enableFloatingPreview: 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 enableFloatingPreview = content.querySelector("#enableFloatingPreview");
if (enableFloatingPreview) userscriptState.settings.enableFloatingPreview = enableFloatingPreview.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();
});
modal.addEventListener("click", (e) => {
if (e.target === modal) modal.remove();
});
}
var init_settings = __esmMin((() => {
init_state();
init_userscript_storage();
init_createEl();
init_themeSupport();
init_groupEditor();
init_popularEmojis();
}));
init_state();
init_userscript_storage();
init_createEl();
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 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) {
if (!userscriptState.settings?.enableFloatingPreview) return;
const preview = ensureHoverPreview$1();
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);
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) {
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();
var QUICK_INSERTS = [
"info",
"tip",
"faq",
"question",
"note",
"abstract",
"todo",
"success",
"warning",
"failure",
"danger",
"bug",
"example",
"quote"
];
var ICON_MAP = {
info: "ℹ️",
tip: "💡",
faq: "❓",
question: "🤔",
note: "📝",
abstract: "📋",
todo: "☑️",
success: "🎉",
warning: "⚠️",
failure: "❌",
danger: "☠️",
bug: "🐛",
example: "🔎",
quote: "💬"
};
function insertIntoEditor(text) {
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 createQuickInsertMenu() {
const menu = document.createElement("div");
menu.className = "fk-d-menu toolbar-menu__options-content toolbar-popup-menu-options -animated -expanded";
const inner = document.createElement("div");
inner.className = "fk-d-menu__inner-content";
const list = document.createElement("ul");
list.className = "dropdown-menu";
QUICK_INSERTS.forEach((key) => {
const li = document.createElement("li");
li.className = "dropdown-menu__item";
const btn = document.createElement("button");
btn.className = "btn btn-icon-text";
btn.type = "button";
const displayLabel = key.charAt(0).toUpperCase() + key.slice(1);
btn.title = displayLabel;
btn.addEventListener("click", () => {
if (menu.parentElement) menu.parentElement.removeChild(menu);
insertIntoEditor(`>[!${key}]`);
});
const emojiSpan = document.createElement("span");
emojiSpan.textContent = ICON_MAP[key] || "✳️";
const labelWrap = document.createElement("span");
labelWrap.className = "d-button-label";
const labelText = document.createElement("span");
labelText.className = "d-button-label__text";
labelText.textContent = displayLabel;
labelWrap.appendChild(labelText);
btn.appendChild(emojiSpan);
btn.appendChild(labelWrap);
li.appendChild(btn);
list.appendChild(li);
});
inner.appendChild(list);
menu.appendChild(inner);
return menu;
}
function findAllToolbars() {
const toolbars = [];
const selectors = getPlatformToolbarSelectors();
for (const selector of selectors) {
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: "🐈⬛"
});
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);
}
}
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_createEl();
init_themeSupport();
var floatingButton = null;
var isButtonVisible = false;
var FLOATING_BUTTON_STYLES = `
.emoji-extension-floating-button {
position: fixed !important;
bottom: 20px !important;
right: 20px !important;
width: 56px !important;
height: 56px !important;
border-radius: 50% !important;
background: linear-gradient(135deg, var(--emoji-button-gradient-start) 0%, var(--emoji-button-gradient-end) 100%) !important;
border: none !important;
box-shadow: 0 4px 12px var(--emoji-button-shadow) !important;
cursor: pointer !important;
z-index: 999999 !important;
font-size: 24px !important;
color: white !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
transition: all 0.3s ease !important;
opacity: 0.9 !important;
line-height: 1 !important;
}
.emoji-extension-floating-button:hover {
transform: scale(1.1) !important;
opacity: 1 !important;
box-shadow: 0 6px 16px var(--emoji-button-hover-shadow) !important;
}
.emoji-extension-floating-button:active {
transform: scale(0.95) !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 {
bottom: 15px !important;
right: 15px !important;
width: 48px !important;
height: 48px !important;
font-size: 20px !important;
}
}
`;
function injectStyles() {
if (document.getElementById("emoji-extension-floating-button-styles")) return;
injectGlobalThemeStyles();
const style = createEl("style", {
attrs: { id: "emoji-extension-floating-button-styles" },
text: FLOATING_BUTTON_STYLES
});
document.head.appendChild(style);
}
function createFloatingButton() {
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 {
const result = attemptInjection();
if (result.injectedCount > 0) {
button.innerHTML = "✅";
button.style.background = "linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%)";
setTimeout(() => {
button.innerHTML = "🐈⬛";
button.style.background = "linear-gradient(135deg, #667eea 0%, #764ba2 100%)";
button.style.transform = "scale(1)";
}, 1500);
console.log(`[Emoji Extension Userscript] Manual injection successful: ${result.injectedCount} buttons injected into ${result.totalToolbars} toolbars`);
} else {
button.innerHTML = "❌";
button.style.background = "linear-gradient(135deg, #ff6b6b 0%, #ffa8a8 100%)";
setTimeout(() => {
button.innerHTML = "🐈⬛";
button.style.background = "linear-gradient(135deg, #667eea 0%, #764ba2 100%)";
button.style.transform = "scale(1)";
}, 1500);
console.log("[Emoji Extension Userscript] Manual injection failed: No compatible toolbars found");
}
} catch (error) {
button.innerHTML = "⚠️";
button.style.background = "linear-gradient(135deg, #ff6b6b 0%, #ffa8a8 100%)";
setTimeout(() => {
button.innerHTML = "🐈⬛";
button.style.background = "linear-gradient(135deg, #667eea 0%, #764ba2 100%)";
button.style.transform = "scale(1)";
}, 1500);
console.error("[Emoji Extension Userscript] Manual injection error:", error);
}
});
return button;
}
function showFloatingButton() {
if (floatingButton) return;
injectStyles();
floatingButton = createFloatingButton();
document.body.appendChild(floatingButton);
isButtonVisible = true;
console.log("[Emoji Extension Userscript] Floating manual injection button shown");
}
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() {
const existingButtons = document.querySelectorAll(".emoji-extension-button");
if (existingButtons.length === 0 && !isButtonVisible) setTimeout(() => {
autoShowFloatingButton();
}, 2e3);
else if (existingButtons.length > 0 && isButtonVisible) hideFloatingButton();
}
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...");
logPlatformInfo();
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.");
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 (shouldInjectEmoji()) {
console.log("[Emoji Extension Userscript] Initializing emoji feature");
initializeEmojiFeature();
} else console.log("[Emoji Extension Userscript] Skipping injection - incompatible platform");
})();
})();