// ==UserScript== // @name NGA Smiles Manager // @namespace https://greasyfork.org/users/263018 // @version 1.3.0 // @author snyssss // @description 表情管理器,支持快速添加表情包,自动同步表情包,隐藏系统表情,显示最近表情 // @match *://bbs.nga.cn/* // @match *://ngabbs.com/* // @match *://nga.178.com/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_addValueChangeListener // @grant GM_registerMenuCommand // @noframes // @downloadURL https://update.greasyfork.icu/scripts/449822/NGA%20Smiles%20Manager.user.js // @updateURL https://update.greasyfork.icu/scripts/449822/NGA%20Smiles%20Manager.meta.js // ==/UserScript== ((ui, poster, smiles, basePath) => { if (!ui) return; if (!poster) return; if (!smiles) return; if (!basePath) return; // KEY const USER_AGENT_KEY = "USER_AGENT_KEY"; // User Agent const USER_AGENT = (() => { const data = GM_getValue(USER_AGENT_KEY) || "Nga_Official"; GM_registerMenuCommand(`修改UA:${data}`, () => { const value = prompt("修改UA", data); if (value) { GM_setValue(USER_AGENT_KEY, value); location.reload(); } }); return data; })(); // 简单的统一请求 const request = (url, config = {}) => fetch(url, { headers: { "X-User-Agent": USER_AGENT, }, ...config, }); // 数据操作 const manager = (() => { const KEY = `NGA_SMILES_MANAGER`; const RECENT_KEY = `NGA_SMILES_RECENT`; const data = {}; const fetchData = (pid) => new Promise((resolve, reject) => { const api = `/read.php?pid=${pid}`; request(api) .then((res) => res.blob()) .then((blob) => { const reader = new FileReader(); reader.onload = async () => { const parser = new DOMParser(); const doc = parser.parseFromString(reader.result, "text/html"); const verify = doc.querySelector("#m_posts"); if (verify) { const subject = doc.querySelector("#postsubject0").innerHTML; const content = doc.querySelector("#postcontent0").innerHTML; const items = content.match(/(?<=\[img\])(.+?)(?=\[\/img\])/g); if (items.length) { resolve({ name: subject, items, }); } else { reject("图楼内容有误"); } } else { reject(doc.title); } }; reader.readAsText(blob, "GBK"); }) .catch(() => { reject("服务器异常"); }); }); const assign = (next) => { Object.getOwnPropertyNames(data).forEach((property) => { delete data[property]; }); Object.getOwnPropertyNames(next).forEach((property) => { data[property] = next[property]; }); }; const save = () => { GM_setValue(KEY, data); }; const load = () => { assign( GM_getValue(KEY) || { [0]: { syncInterval: 3600, hiddenSmiles: [], showRecent: 0, }, } ); }; const settings = () => { if (Object.keys(data).length < 1) { load(); } return data[0]; }; const updateSettings = (values) => { const entity = settings(); Object.getOwnPropertyNames(values).forEach((property) => { entity[property] = values[property]; }); save(); }; const list = () => { if (Object.keys(data).length < 1) { load(); } return Object.keys(data) .filter((key) => key > 0) .reduce((root, key) => { return [...root, data[key]]; }, []); }; const get = (pid) => { return list().find((item) => item.pid === pid); }; const set = (pid, values) => { const entity = get(pid); if (entity) { Object.getOwnPropertyNames(values).forEach((property) => { entity[property] = values[property]; }); } else { const index = Math.max(...Object.keys(data), 0) + 1; data[index] = { pid, name: `#${pid}`, error: "", enabled: true, syncDate: null, ...values, }; } save(); }; const sync = async (pid) => { const { syncInterval } = settings(); const syncDate = new Date().getTime(); const entity = get(pid); if ( syncInterval > 0 && entity && entity.syncDate + syncInterval * 1000 > syncDate ) { return false; } try { const { name, items } = await fetchData(pid); set(pid, { name: name || `#${pid}`, error: "", syncDate, }); GM_setValue(pid, items); } catch (error) { set(pid, { error, syncDate, }); return false; } return true; }; const add = async (url) => { const params = new URLSearchParams(url.substring(url.indexOf("?"))); const pid = params.get("pid"); if (pid === null) { alert("图楼地址有误"); return false; } await sync(pid); return true; }; const remove = (pid) => { GM_deleteValue(pid); Object.keys(data).forEach((key) => { if (data[key].pid === pid) { delete data[key]; } }); save(); }; const listRecent = () => { return GM_getValue(RECENT_KEY) || []; }; const pushRecent = (value) => { const { showRecent } = settings(); const list = listRecent(); GM_setValue( RECENT_KEY, [value, ...list.filter((item) => item !== value)].slice(0, showRecent) ); }; GM_addValueChangeListener(KEY, function (_, prev, next) { assign(next); }); return { add, set, sync, remove, list, listRecent, pushRecent, settings, updateSettings, }; })(); // STYLE GM_addStyle(` .s-user-info-container:not(:hover) .ah { display: none !important; } .s-table-wrapper { height: calc((2em + 10px) * 11 + 3px); overflow-y: auto; } .s-table { margin: 0; } .s-table th, .s-table td { position: relative; white-space: nowrap; } .s-table th { position: sticky; top: 2px; z-index: 1; } .s-table input:not([type]), .s-table input[type="text"] { margin: 0; box-sizing: border-box; height: 100%; width: 100%; } .s-input-wrapper { position: absolute; top: 6px; right: 6px; bottom: 6px; left: 6px; } .s-text-ellipsis { display: flex; } .s-text-ellipsis > * { flex: 1; width: 1px; overflow: hidden; text-overflow: ellipsis; } .s-button-group { margin: -.1em -.2em; } `); // UI const u = (() => { const modules = {}; const tabContainer = (() => { const c = document.createElement("div"); c.className = "w100"; c.innerHTML = `
`; return c; })(); const tabPanelContainer = (() => { const c = document.createElement("div"); c.style = "width: 800px;"; return c; })(); const content = (() => { const c = document.createElement("div"); c.append(tabContainer); c.append(tabPanelContainer); return c; })(); const addModule = (() => { const tc = tabContainer.getElementsByTagName("tr")[0]; const cc = tabPanelContainer; return (module) => { const tabBox = document.createElement("td"); tabBox.innerHTML = `${module.name}`; const tab = tabBox.childNodes[0]; const toggle = () => { Object.values(modules).forEach((item) => { if (item.tab === tab) { item.tab.className = "nobr"; item.content.style = "display: block"; item.refresh(); } else { item.tab.className = "nobr silver"; item.content.style = "display: none"; } }); }; tc.append(tabBox); cc.append(module.content); tab.onclick = toggle; modules[module.name] = { ...module, tab, toggle, }; return modules[module.name]; }; })(); return { content, modules, addModule, }; })(); // 列表 (() => { const content = (() => { const c = document.createElement("div"); c.style = "display: none"; c.innerHTML = `
标题 自定义标题 异常信息 同步时间 是否启用 操作
`; return c; })(); const refresh = (() => { const container = content.getElementsByTagName("tbody")[0]; const func = () => { container.innerHTML = ""; Object.values(manager.list()).forEach((item) => { const { pid, name, label, error, enabled, syncDate } = item; const tc = document.createElement("tr"); tc.className = `row${ (container.querySelectorAll("TR").length % 2) + 1 }`; tc.innerHTML = ` ${name}
${error} ${ui.time2dis(syncDate / 1000)}
`; const labelInput = tc.querySelector(`INPUT[type="text"]`); if (labelInput) { const save = () => { manager.set(pid, { label: labelInput.value, }); }; labelInput.onblur = save; } const enabledElement = tc.querySelector(`INPUT[type="checkbox"]`); if (enabledElement) { const save = () => { manager.set(pid, { enabled: enabledElement.checked ? 1 : 0, }); }; enabledElement.onchange = save; } const actions = tc.getElementsByTagName("button"); actions[0].onclick = async () => { await manager.sync(pid); refresh(); }; actions[1].onclick = () => { if (confirm("是否确认?")) { manager.remove(pid); refresh(); } }; container.appendChild(tc); }); { const tc = document.createElement("tr"); tc.className = `row${ (container.querySelectorAll("TR").length % 2) + 1 }`; tc.innerHTML = `
`; const inputElement = tc.querySelector("INPUT"); const actions = tc.getElementsByTagName("button"); actions[0].onclick = async () => { if (inputElement.value) { if (await manager.add(inputElement.value)) { refresh(); } } }; container.appendChild(tc); } }; return func; })(); u.addModule({ name: "列表", content, refresh, }); })(); // 系统 (() => { const content = (() => { const c = document.createElement("div"); c.style = "display: none"; c.innerHTML = `
标题 是否启用
`; return c; })(); const refresh = (() => { const container = content.getElementsByTagName("tbody")[0]; const func = () => { container.innerHTML = ""; const hiddenSmiles = manager.settings().hiddenSmiles || []; Object.values(smiles).forEach((item) => { const { _______name: name } = item; if (name) { const tc = document.createElement("tr"); tc.className = `row${ (container.querySelectorAll("TR").length % 2) + 1 }`; tc.innerHTML = ` ${name}
`; const enabledElement = tc.querySelector(`INPUT[type="checkbox"]`); const save = () => { manager.updateSettings({ hiddenSmiles: hiddenSmiles .filter((item) => item !== name) .concat(enabledElement.checked ? [] : [name]), }); refresh(); }; enabledElement.onchange = save; container.appendChild(tc); } }); }; return func; })(); u.addModule({ name: "系统", content, refresh, }); })(); // 通用设置 (() => { const content = (() => { const c = document.createElement("div"); c.style = "display: none"; return c; })(); const refresh = (() => { const container = content; const func = () => { container.innerHTML = ""; // 自动同步 { const syncInterval = manager.settings().syncInterval || 0; const tc = document.createElement("div"); tc.innerHTML += `
自动同步设置
`; [ { label: "1小时", value: 3600, }, { label: "1天", value: 3600 * 24, }, { label: "从不", value: 0, }, ].forEach(({ label, value }) => { const ele = document.createElement("SPAN"); ele.innerHTML += ` `; const items = ele.querySelector("input"); items.onchange = () => { if (items.checked) { manager.updateSettings({ syncInterval: value, }); } }; tc.querySelectorAll("div")[1].append(ele); }); container.appendChild(tc); } // 显示最近表情 { const showRecent = manager.settings().showRecent || 0; const tc = document.createElement("div"); tc.innerHTML += `
显示最近表情
`; [ { label: "0", value: 0, }, { label: "10", value: 10, }, { label: "20", value: 20, }, { label: "50", value: 50, }, ].forEach(({ label, value }) => { const ele = document.createElement("SPAN"); ele.innerHTML += ` `; const items = ele.querySelector("input"); items.onchange = () => { if (items.checked) { manager.updateSettings({ showRecent: value, }); } }; tc.querySelectorAll("div")[1].append(ele); }); container.appendChild(tc); } }; return func; })(); u.addModule({ name: "设置", content, refresh, }); })(); // 增加菜单项 (() => { const title = "表情管理"; let window; ui.mainMenu.addItemOnTheFly(title, null, () => { if (window === undefined) { window = ui.createCommmonWindow(); } u.modules["列表"].toggle(); window._.addContent(null); window._.addTitle(title); window._.addContent(u.content); window._.show(); }); })(); // 判断是否为系统表情 const isSystemSmile = (value) => { const result = value.match(/\[s:(.{1,10}?)\]/); if (result) { const [group, item] = parseInt(result[1], 10) ? [0, result[1]] : result[1].split(":"); if (smiles[group || 0] && smiles[group || 0][item]) { return `${basePath}/post/smile/${smiles[group || 0][item]}`; } } return null; }; // 加载表情 const loadSmile = (content, list) => { const { correctAttachUrl } = ui; content.innerHTML = ``; list.forEach((item) => { const smile = document.createElement("IMG"); const path = isSystemSmile(item); if (path) { smile.src = path; smile.onclick = () => { poster.selectSmilesw._.hide(); poster.addText(item); }; } else { smile.src = item.indexOf("http") < 0 ? correctAttachUrl(item) : item; smile.style = "max-height: 200px"; smile.onclick = () => { poster.selectSmilesw._.hide(); poster.addText(`[img]${item}[/img]`); manager.pushRecent(item); }; } content.appendChild(smile); }); }; // 加载表情 const loadSmiles = (loaded) => { if (loaded) return; const tabs = poster.selectSmilesw._.__c.firstElementChild; const contents = poster.selectSmilesw._.__c.lastElementChild; const hiddenSmiles = manager.settings().hiddenSmiles || []; [...tabs.querySelectorAll("button.block_txt_big")].forEach((item) => { const name = item.innerHTML; if (hiddenSmiles.includes(name)) { item.style.display = "none"; } }); manager.list().forEach((item) => { const { pid, name, label, enabled } = item; if (enabled) { const tab = document.createElement("BUTTON"); const content = document.createElement("DIV"); tab.className = "block_txt_big"; tab.innerText = label || name; tab.onclick = async () => { tabs.firstChild.innerHTML = ``; contents.childNodes.forEach((item) => { if (item !== content) { item.style.display = "none"; } else { item.style.display = ""; } }); if (content.childNodes.length === 0) { await manager.sync(pid); const list = GM_getValue(pid) || []; loadSmile(content, list); } }; tabs.appendChild(tab); contents.appendChild(content); } }); }; // 加载最近表情 const loadRecent = () => { const list = manager.listRecent(); if (list.length) { const contents = poster.selectSmilesw._.__c.lastElementChild; const recentElementId = `smile_recent`; const recentElement = contents.querySelector(`[id="smile_recent"]`) || document.createElement("DIV"); if (!recentElement.id) { recentElement.id = recentElementId; contents.appendChild(recentElement); } contents.childNodes.forEach((item) => { if (item !== recentElement) { item.style.display = "none"; } else { item.style.display = ""; } }); loadSmile(recentElement, list); } }; // 扩展菜单 const enhanceMenu = () => { // 标识 const key = "SMILES_MANAGER_IMPORT"; // 生成菜单 if (ui.postBtn) { ui.postBtn.d[key] = { n2: "导入表情", n3: "导入表情", on: async (_, { pid }) => { const result = await manager.sync(pid); if (result) { alert("导入成功"); } }, ck: ({ pid }) => { return pid > 0; }, }; // 写入系统菜单 ui.postBtn.all["扩展"] = ui.postBtn.all["扩展"] || []; if (ui.postBtn.all["扩展"].indexOf(key) < 0) { ui.postBtn.all["扩展"].push(key); } } }; // 加载脚本 (() => { const hookFunction = (object, functionName, callback) => { ((originalFunction) => { object[functionName] = function () { const returnValue = originalFunction.apply(this, arguments); callback.apply(this, [returnValue, originalFunction, arguments]); return returnValue; }; })(object[functionName]); }; hookFunction(poster, "selectSmiles", (returnValue) => { loadSmiles(returnValue); loadRecent(); }); hookFunction(poster, "addText", (returnValue, _, arguments) => { const path = isSystemSmile(arguments[0]); if (path) { manager.pushRecent(arguments[0]); } }); hookFunction(ui, "eval", enhanceMenu); enhanceMenu(); })(); })(commonui, postfunc, ubbcode.smiles, __IMGPATH);