// ==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
// ==/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 = `
<div class="right_" style="margin-bottom: 5px;">
<table class="stdbtn" cellspacing="0">
<tbody>
<tr></tr>
</tbody>
</table>
</div>
<div class="clear"></div>
`;
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 = `<a href="javascript:void(0)" class="nobr silver">${module.name}</a>`;
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 = `
<div class="s-table-wrapper">
<table class="s-table forumbox">
<thead>
<tr class="block_txt_c0">
<th class="c1">标题</th>
<th class="c2">自定义标题</th>
<th class="c3">异常信息</th>
<th class="c4" width="1">同步时间</th>
<th class="c5" width="1">是否启用</th>
<th class="c6" width="1">操作</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
`;
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 = `
<td class="c1">
<a href="/read.php?pid=${pid}" class="b nobr">${name}</a>
</td>
<td class="c2">
<div class="s-input-wrapper">
<input type="text" value="${label || ""}" maxlength="20" />
</div>
</td>
<td class="c3">
<span class="nobr">${error}</span>
</td>
<td class="c4">
<span class="nobr">${ui.time2dis(syncDate / 1000)}</span>
</td>
<td class="c5">
<div style="text-align: center;">
<input type="checkbox" ${
enabled ? `checked="checked"` : ""
} />
</div>
</td>
<td class="c6">
<div class="s-button-group">
<button>同步</button>
<button>删除</button>
</div>
</td>
`;
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 = `
<td class="c1" colspan="4">
<div class="s-input-wrapper">
<input value="" />
</div>
</td>
<td class="c5">
<div class="s-button-group">
<button>添加</button>
</div>
</td>
`;
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 = `
<div class="s-table-wrapper">
<table class="s-table forumbox">
<thead>
<tr class="block_txt_c0">
<th class="c1">标题</th>
<th class="c2" width="1">是否启用</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
`;
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 = `
<td class="c1">
<span class="nobr">${name}</span>
</td>
<td class="c2">
<div style="text-align: center;">
<input type="checkbox" ${
hiddenSmiles.includes(name) ? "" : `checked="checked"`
} />
</div>
</td>
`;
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 += `
<div>自动同步设置</div>
<div></div>
`;
[
{
label: "1小时",
value: 3600,
},
{
label: "1天",
value: 3600 * 24,
},
{
label: "从不",
value: 0,
},
].forEach(({ label, value }) => {
const ele = document.createElement("SPAN");
ele.innerHTML += `
<label style="cursor: pointer;">
<input type="radio" name="syncInterval" ${
syncInterval === value && "checked"
}>${label}
</label>
`;
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 += `
<br/>
<div>显示最近表情</div>
<div></div>
`;
[
{
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 += `
<label style="cursor: pointer;">
<input type="radio" name="showRecent" ${
showRecent === value && "checked"
}>${label}
</label>
`;
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);