隐藏不需要的用户信息,或显示额外的用户信息,包括被点赞和粉丝数量,以及坛龄、离线天数、发帖数量、属地、曾用名、游戏档案、刀塔段位
// ==UserScript== // @name NGA UserInfo Enhance // @name:zh-CN NGA 用户信息增强 // @namespace https://greasyfork.org/users/263018 // @version 2.0.9 // @author snyssss // @license MIT // @description 隐藏不需要的用户信息,或显示额外的用户信息,包括被点赞和粉丝数量,以及坛龄、离线天数、发帖数量、属地、曾用名、游戏档案、刀塔段位 // @description:zh-CN 隐藏不需要的用户信息,或显示额外的用户信息,包括被点赞和粉丝数量,以及坛龄、离线天数、发帖数量、属地、曾用名、游戏档案、刀塔段位 // @match *://bbs.nga.cn/* // @match *://ngabbs.com/* // @match *://nga.178.com/* // @require https://update.greasyfork.org/scripts/486070/1414880/NGA%20Library.js // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant unsafeWindow // @run-at document-start // @noframes // ==/UserScript== (() => { // 声明泥潭主模块 let commonui; // 声明缓存和 API let cache, api; // 系统标签 const SYSTEM_LABEL_MAP = { 头像: "", 头衔: "", 声望: "", #望: "", 级别: "", 注册: "", 发帖: "泥潭默认仅版主可见,普通用户可在增强里打开", 财富: "", 徽章: "", 版面: "", 备注: "", 签名: "", }; // 自定义标签 const CUSTOM_LABEL_MAP = { 点赞: "需要占用额外的资源", 粉丝: "需要占用额外的资源", 坛龄: "", 离线: "", 发帖: "", 属地: "需要占用额外的资源", 曾用名: "需要占用额外的资源", 游戏档案: "需要占用额外的资源,目前支持 Steam、PSN、NS、原神、深空之眼", 刀塔段位: "需要占用额外的资源,需要可以访问 Opendota 和 Stratz<br/>免费接口为每天 2000 次,每分钟 60 次", }; // STYLE GM_addStyle(` .s-table-wrapper { max-height: 80vh; 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; } .s-user-enhance [s-user-enhance-visible="true"].usercol::after { content: ' · '; } .s-user-enhance [s-user-enhance-visible="false"] { display: none; } `); /** * UI */ class UI { /** * 标签 */ static label = "用户信息增强"; /** * 弹出窗 */ window; /** * 视图元素 */ views = {}; /** * 初始化 */ constructor() { this.init(); } /** * 初始化,创建基础视图,初始化通用设置 */ init() { const tabs = this.createTabs({ className: "right_", }); const content = this.createElement("DIV", [], { style: "width: 600px;", }); const container = this.createElement("DIV", [tabs, content]); this.views = { tabs, content, container, }; } /** * 创建元素 * @param {String} tagName 标签 * @param {HTMLElement | HTMLElement[] | String} content 内容,元素或者 innerHTML * @param {*} properties 额外属性 * @returns {HTMLElement} 元素 */ createElement(tagName, content, properties = {}) { const element = document.createElement(tagName); // 写入内容 if (typeof content === "string") { element.innerHTML = content; } else { if (Array.isArray(content) === false) { content = [content]; } content.forEach((item) => { if (item === null) { return; } if (typeof item === "string") { element.append(item); return; } element.appendChild(item); }); } // 对 A 标签的额外处理 if (tagName.toUpperCase() === "A") { if (Object.hasOwn(properties, "href") === false) { properties.href = "javascript: void(0);"; } } // 附加属性 Object.entries(properties).forEach(([key, value]) => { element[key] = value; }); return element; } /** * 创建按钮 * @param {String} text 文字 * @param {Function} onclick 点击事件 * @param {*} properties 额外属性 */ createButton(text, onclick, properties = {}) { return this.createElement("BUTTON", text, { ...properties, onclick, }); } /** * 创建按钮组 * @param {Array} buttons 按钮集合 */ createButtonGroup(...buttons) { return this.createElement("DIV", buttons, { className: "s-button-group", }); } /** * 创建表格 * @param {Array} headers 表头集合 * @param {*} properties 额外属性 * @returns {HTMLElement} 元素和相关函数 */ createTable(headers, properties = {}) { const rows = []; const ths = headers.map((item, index) => this.createElement("TH", item.label, { ...item, className: `c${index + 1}`, }) ); const tr = ths.length > 0 ? this.createElement("TR", ths, { className: "block_txt_c0", }) : null; const thead = tr !== null ? this.createElement("THEAD", tr) : null; const tbody = this.createElement("TBODY", []); const table = this.createElement("TABLE", [thead, tbody], { ...properties, className: "s-table forumbox", }); const wrapper = this.createElement("DIV", table, { className: "s-table-wrapper", }); const intersectionObserver = new IntersectionObserver((entries) => { if (entries[0].intersectionRatio <= 0) return; const list = rows.splice(0, 10); if (list.length === 0) { return; } intersectionObserver.disconnect(); tbody.append(...list); intersectionObserver.observe(tbody.lastElementChild); }); const add = (...columns) => { const tds = columns.map((column, index) => { if (ths[index]) { const { center, ellipsis } = ths[index]; const properties = {}; if (center) { properties.style = "text-align: center;"; } if (ellipsis) { properties.className = "s-text-ellipsis"; } column = this.createElement("DIV", column, properties); } return this.createElement("TD", column, { className: `c${index + 1}`, }); }); const tr = this.createElement("TR", tds, { className: `row${(rows.length % 2) + 1}`, }); intersectionObserver.disconnect(); rows.push(tr); intersectionObserver.observe(tbody.lastElementChild || tbody); }; const update = (e, ...columns) => { const row = e.target.closest("TR"); if (row) { const tds = row.querySelectorAll("TD"); columns.map((column, index) => { if (ths[index]) { const { center, ellipsis } = ths[index]; const properties = {}; if (center) { properties.style = "text-align: center;"; } if (ellipsis) { properties.className = "s-text-ellipsis"; } column = this.createElement("DIV", column, properties); } if (tds[index]) { tds[index].innerHTML = ""; tds[index].append(column); } }); } }; const remove = (e) => { const row = e.target.closest("TR"); if (row) { tbody.removeChild(row); } }; const clear = () => { rows.splice(0); intersectionObserver.disconnect(); tbody.innerHTML = ""; }; Object.assign(wrapper, { add, update, remove, clear, }); return wrapper; } /** * 创建标签组 * @param {*} properties 额外属性 */ createTabs(properties = {}) { const tabs = this.createElement( "DIV", `<table class="stdbtn" cellspacing="0"> <tbody> <tr></tr> </tbody> </table>`, properties ); return this.createElement( "DIV", [ tabs, this.createElement("DIV", [], { className: "clear", }), ], { style: "display: none; margin-bottom: 5px;", } ); } /** * 创建标签 * @param {Element} tabs 标签组 * @param {String} label 标签名称 * @param {Number} order 标签顺序,重复则跳过 * @param {*} properties 额外属性 */ createTab(tabs, label, order, properties = {}) { const group = tabs.querySelector("TR"); const items = [...group.childNodes]; if (items.find((item) => item.order === order)) { return; } if (items.length > 0) { tabs.style.removeProperty("display"); } const tab = this.createElement("A", label, { ...properties, className: "nobr silver", onclick: () => { if (tab.className === "nobr") { return; } group.querySelectorAll("A").forEach((item) => { if (item === tab) { item.className = "nobr"; } else { item.className = "nobr silver"; } }); if (properties.onclick) { properties.onclick(); } }, }); const wrapper = this.createElement("TD", tab, { order, }); const anchor = items.find((item) => item.order > order); group.insertBefore(wrapper, anchor || null); return wrapper; } /** * 创建对话框 * @param {HTMLElement | null} anchor 要绑定的元素,如果为空,直接弹出 * @param {String} title 对话框的标题 * @param {HTMLElement} content 对话框的内容 */ createDialog(anchor, title, content) { let window; const show = () => { if (window === undefined) { window = commonui.createCommmonWindow(); } window._.addContent(null); window._.addTitle(title); window._.addContent(content); window._.show(); }; if (anchor) { anchor.onclick = show; } else { show(); } return window; } /** * 渲染视图 */ renderView() { // 创建或打开弹出窗 if (this.window === undefined) { this.window = this.createDialog( this.views.anchor, this.constructor.label, this.views.container ); } else { this.window._.show(); } // 启用第一个模块 this.views.tabs.querySelector("A").click(); } /** * 渲染 */ render() { this.renderView(); } } /** * 基础模块 */ class Module { /** * 模块名称 */ static name; /** * 模块标签 */ static label; /** * 顺序 */ static order; /** * UI */ ui; /** * 视图元素 */ views = {}; /** * 初始化并绑定UI,注册 UI * @param {UI} ui UI */ constructor(ui) { this.ui = ui; this.init(); } /** * 获取列表 */ get list() { return GM_getValue(this.constructor.name, []); } /** * 写入列表 */ set list(value) { GM_setValue(this.constructor.name, value); } /** * 切换启用状态 * @param {String} label 标签 */ toggle(label) { const list = this.list; if (this.list.includes(label)) { this.list = list.filter((i) => i !== label); } else { this.list = list.concat(label); } rerender(); } /** * 初始化,创建基础视图和组件 */ init() { if (this.views.container) { this.destroy(); } const { ui } = this; const container = ui.createElement("DIV", []); this.views = { container, }; this.initComponents(); } /** * 初始化组件 */ initComponents() {} /** * 销毁 */ destroy() { Object.values(this.views).forEach((view) => { if (view.parentNode) { view.parentNode.removeChild(view); } }); this.views = {}; } /** * 渲染 * @param {HTMLElement} container 容器 */ render(container) { container.innerHTML = ""; container.appendChild(this.views.container); } } /** * 系统模块 */ class SystemModule extends Module { /** * 模块名称 */ static name = "system"; /** * 模块标签 */ static label = "系统"; /** * 顺序 */ static order = 10; /** * 表格列 * @returns {Array} 表格列集合 */ columns() { return [ { label: "标题" }, { label: "注释" }, { label: "是否启用", center: true, width: 1 }, ]; } /** * 表格项 * @param {String} label 标签 * @param {String} description 注释 * @returns {Array} 表格项集合 */ column(label, description) { const { ui, list } = this; // 标题 const labelElement = ui.createElement("SPAN", label, { className: "nobr", }); // 注释 const descriptionElement = ui.createElement("SPAN", description, { className: "nobr", }); // 是否启用 const enabled = ui.createElement("INPUT", [], { type: "checkbox", checked: list.includes(label) === false, onchange: () => { this.toggle(label); }, }); return [labelElement, descriptionElement, enabled]; } /** * 初始化组件 */ initComponents() { super.initComponents(); const { tabs, content } = this.ui.views; const table = this.ui.createTable(this.columns()); const tab = this.ui.createTab( tabs, this.constructor.label, this.constructor.order, { onclick: () => { this.render(content); }, } ); Object.assign(this.views, { tab, table, }); this.views.container.appendChild(table); } /** * 渲染 * @param {HTMLElement} container 容器 */ render(container) { super.render(container); const { table } = this.views; if (table) { const { add, clear } = table; clear(); Object.entries(SYSTEM_LABEL_MAP).forEach(([label, description]) => { const column = this.column(label, description); add(...column); }); } } } /** * 自定义模块 */ class CustomModule extends Module { /** * 模块名称 */ static name = "custom"; /** * 模块标签 */ static label = "增强"; /** * 顺序 */ static order = 20; /** * 表格列 * @returns {Array} 表格列集合 */ columns() { return [ { label: "标题" }, { label: "注释" }, { label: "是否启用", center: true, width: 1 }, ]; } /** * 表格项 * @param {String} label 标签 * @param {String} description 注释 * @returns {Array} 表格项集合 */ column(label, description) { const { ui, list } = this; // 标题 const labelElement = ui.createElement("SPAN", label, { className: "nobr", }); // 注释 const descriptionElement = ui.createElement("SPAN", description, { className: "nobr", }); // 是否启用 const enabled = ui.createElement("INPUT", [], { type: "checkbox", checked: list.includes(label), onchange: () => { this.toggle(label); }, }); return [labelElement, descriptionElement, enabled]; } /** * 初始化组件 */ initComponents() { super.initComponents(); const { tabs, content } = this.ui.views; const table = this.ui.createTable(this.columns()); const tab = this.ui.createTab( tabs, this.constructor.label, this.constructor.order, { onclick: () => { this.render(content); }, } ); Object.assign(this.views, { tab, table, }); this.views.container.appendChild(table); } /** * 渲染 * @param {HTMLElement} container 容器 */ render(container) { super.render(container); const { table } = this.views; if (table) { const { add, clear } = table; clear(); Object.entries(CUSTOM_LABEL_MAP).forEach(([label, description]) => { const column = this.column(label, description); add(...column); }); } } } /** * 处理 commonui 模块 * @param {*} value commonui */ const handleCommonui = (value) => { // 绑定主模块 commonui = value; // 拦截 postDisp 事件,这是泥潭的楼层渲染 Tools.interceptProperty(commonui, "postDisp", { afterSet: () => { rerender(); }, afterGet: (_, args) => { rerender(...args); }, }); }; /** * 注册脚本菜单 */ const registerMenu = () => { let ui; GM_registerMenuCommand(`设置`, () => { if (commonui && commonui.mainMenuItems) { if (ui === undefined) { ui = new UI(); new SystemModule(ui); new CustomModule(ui); } ui.render(); } }); }; /** * 重新渲染 * @param {Number | undefined} index 重新渲染的楼层,为空时重新渲染全部 */ const rerender = (index) => { if (commonui === undefined || commonui.postArg === undefined) { return; } if (index === undefined) { Object.keys(commonui.postArg.data).forEach((item) => { rerender(item); }); return; } const argid = parseInt(index, 10); if (Number.isNaN(argid) || argid < 0) { return; } // TODO 需要优化 const system = GM_getValue("system", []); const custom = GM_getValue("custom", []); const item = commonui.postArg.data[argid]; const lite = item.lite; const uid = parseInt(item.pAid, 10) || 0; const posterInfo = lite ? item.uInfoC.closest("tr").querySelector(".posterInfoLine") : item.uInfoC; const container = item.pC.closest(".postbox"); // 主容器样式 container.classList.add("s-user-enhance"); // 头像 { const element = posterInfo.querySelector(".avatar"); if (element) { element.setAttribute( "s-user-enhance-visible", system.includes("头像") === false ); } } // 头衔 { const element = posterInfo.querySelector("[name='honor']"); if (element) { element.setAttribute( "s-user-enhance-visible", system.includes("头衔") === false ); } } // 声望进度条 { const element = posterInfo.querySelector(".r_container"); if (element) { element.setAttribute( "s-user-enhance-visible", system.includes("声望") === false ); } } // 声望、#望、级别、注册、发帖、财富 { const elements = lite ? posterInfo.querySelectorAll(".usercol") : posterInfo.querySelectorAll(".stat NOBR"); [...elements].forEach((element) => { if (lite) { ["声望", "#望", "级别", "注册", "发帖", "财富"].forEach((label) => { if (element.innerText.indexOf(label) >= 0) { element.innerHTML = element.innerHTML.replace(" · ", ""); element.setAttribute( "s-user-enhance-visible", system.includes(label) === false ); } }); } else { const container = element.closest("DIV"); container.style = "float: left; min-width: 50%;"; ["声望", "#望", "级别", "注册", "发帖", "财富"].forEach((label) => { if (element.innerText.indexOf(label) >= 0) { container.setAttribute( "s-user-enhance-visible", system.includes(label) === false ); } }); } }); } // 徽章 { const anchor = posterInfo.querySelector("[name='medal']"); if (anchor) { const br = anchor.nextElementSibling; const text = (() => { const previous = anchor.previousElementSibling || anchor.previousSibling; if (previous.nodeName === "SPAN") { return previous; } const span = document.createElement("SPAN"); span.appendChild(previous); insertBefore(span, anchor); return span; })(); const visible = system.includes("徽章") === false; if (lite) { text.innerHTML = text.innerHTML.replace(" · ", ""); anchor .closest(".usercol") .setAttribute("s-user-enhance-visible", visible); } else { [text, anchor, br].forEach((element) => { element.setAttribute("s-user-enhance-visible", visible); }); } } } // 版面 { const anchor = posterInfo.querySelector("[name='site']"); if (anchor) { const container = anchor.closest("SPAN"); const br = container.nextElementSibling; const visible = system.includes("版面") === false; if (lite) { anchor .closest(".usercol") .setAttribute("s-user-enhance-visible", visible); } else { [container, br].forEach((element) => { if (element) { element.setAttribute("s-user-enhance-visible", visible); } }); } } } // 备注 { const elements = [ ...posterInfo.querySelectorAll("SPAN[title^='公开备注']"), ...posterInfo.querySelectorAll("SPAN[title^='版主可见']"), ]; [...elements].forEach((element) => { const container = element.closest("SPAN"); container.setAttribute( "s-user-enhance-visible", system.includes("备注") === false ); }); } // 签名 { const signC = item.signC; if (signC) { signC.setAttribute( "s-user-enhance-visible", system.includes("签名") === false ); } } if (uid <= 0) { return; } // 粉丝 { const element = (() => { const anchor = posterInfo.querySelector( "[name='s-user-enhance-follows']" ); if (anchor) { return anchor; } const span = document.createElement("SPAN"); span.setAttribute("name", `s-user-enhance-follows`); span.className = "small_colored_text_btn stxt block_txt_c2 vertmod"; span.style.cursor = "default"; span.style.margin = "0 0 0 4px"; span.innerHTML = ` <span class="white"> <span style="font-family: comm_glyphs; -webkit-font-smoothing: antialiased; line-height: 1em;">★</span> <span name="s-user-enhance-follows-value"></span> </span>`; const uid = posterInfo.querySelector("[name='uid']"); insertAfter(span, uid); return span; })(); const value = element.querySelector( "[name='s-user-enhance-follows-value']" ); const visible = custom.includes("粉丝"); if (visible) { api.getUserInfo(uid).then(({ follow_by_num }) => { value.innerHTML = follow_by_num || 0; element.setAttribute("s-user-enhance-visible", true); }); } element.setAttribute("s-user-enhance-visible", false); } // 点赞 { const element = (() => { const anchor = posterInfo.querySelector( "[name='s-user-enhance-likes']" ); if (anchor) { return anchor; } const span = document.createElement("SPAN"); span.setAttribute("name", `s-user-enhance-likes`); span.className = "small_colored_text_btn stxt block_txt_c2 vertmod"; span.style.cursor = "default"; span.style.margin = "0 0 0 4px"; span.innerHTML = ` <span class="white"> <span style="font-family: comm_glyphs; -webkit-font-smoothing: antialiased; line-height: 1em;">⯅</span> <span name="s-user-enhance-likes-value"></span> </span>`; const uid = posterInfo.querySelector("[name='uid']"); insertAfter(span, uid); return span; })(); const value = element.querySelector( "[name='s-user-enhance-likes-value']" ); const visible = custom.includes("点赞"); if (visible) { api.getUserInfo(uid).then(({ more_info }) => { const likes = Object.values(more_info || {}).find( (item) => item.type === 8 ); value.innerHTML = likes ? likes.data : 0; element.setAttribute("s-user-enhance-visible", true); }); } element.setAttribute("s-user-enhance-visible", false); } // 坛龄 { const element = (() => { const anchor = posterInfo.querySelector( "[name='s-user-enhance-regdays']" ); if (anchor) { return anchor; } if (lite) { const span = document.createElement("SPAN"); span.setAttribute("name", `s-user-enhance-regdays`); span.className = "usercol nobr"; span.innerHTML = `坛龄 <span class="userval" name="s-user-enhance-regdays-value"></span>`; const lastChild = [...posterInfo.querySelectorAll(".usercol")].pop(); insertAfter(span, lastChild); return span; } const div = document.createElement("DIV"); div.setAttribute("name", `s-user-enhance-regdays`); div.style = "float: left; min-width: 50%"; div.innerHTML = ` <nobr> <span>坛龄: <span class="userval numericl" name="s-user-enhance-regdays-value"></span></span> </nobr>`; const lastChild = posterInfo.querySelector('.stat DIV[class="clear"]'); insertBefore(div, lastChild); return div; })(); const value = element.querySelector( "[name='s-user-enhance-regdays-value']" ); const visible = custom.includes("坛龄"); if (visible) { const { regdate } = commonui.userInfo.users[uid]; const { years, months, days } = Tools.dateDiff( new Date(regdate * 1000) ); value.title = ``; value.innerHTML = ``; [ [years, "年"], [months, "月"], [days, "天"], ].forEach(([item, unit]) => { if (item > 0) { value.title += `${item}${unit}`; if (value.innerHTML.length === 0) { value.innerHTML = `${item}${unit}`; } } }); if (value.innerHTML.length === 0) { value.innerHTML = `0天`; } } element.setAttribute("s-user-enhance-visible", visible); } // 离线 { const element = (() => { const anchor = posterInfo.querySelector( "[name='s-user-enhance-offdays']" ); if (anchor) { return anchor; } if (lite) { const span = document.createElement("SPAN"); span.setAttribute("name", `s-user-enhance-offdays`); span.className = "usercol nobr"; span.innerHTML = `离线 <span class="userval" name="s-user-enhance-offdays-value"></span>`; const lastChild = [...posterInfo.querySelectorAll(".usercol")].pop(); insertAfter(span, lastChild); return span; } const div = document.createElement("DIV"); div.setAttribute("name", `s-user-enhance-offdays`); div.style = "float: left; min-width: 50%"; div.innerHTML = ` <nobr> <span>离线: <span class="userval numericl" name="s-user-enhance-offdays-value"></span></span> </nobr>`; const lastChild = posterInfo.querySelector('.stat DIV[class="clear"]'); insertBefore(div, lastChild); return div; })(); const value = element.querySelector( "[name='s-user-enhance-offdays-value']" ); const visible = custom.includes("离线"); if (visible) { const thisvisit = commonui.userInfo.users[uid].thisvisit; const postTime = item.postTime; const time = Math.max(thisvisit, postTime) * 1000; const diff = new Date() - new Date(time); const start = new Date(2000, 0, 1); const end = new Date(); end.setTime(start.getTime() + diff); const { years, months, days } = Tools.dateDiff(start, end); value.title = ``; value.innerHTML = ``; [ [years, "年"], [months, "月"], [days, "天"], ].forEach(([item, unit]) => { if (item > 0) { value.title += `${item}${unit}`; if (value.innerHTML.length === 0) { value.innerHTML = `${item}${unit}`; } } }); } else { value.innerHTML = ``; } element.setAttribute( "s-user-enhance-visible", value.innerHTML.length > 0 ); } // 发帖 { const element = (() => { const anchor = posterInfo.querySelector( "[name='s-user-enhance-postnum']" ); if (anchor) { return anchor; } if (lite) { const span = document.createElement("SPAN"); span.setAttribute("name", `s-user-enhance-postnum`); span.className = "usercol nobr"; span.innerHTML = `发帖 <span class="userval" name="s-user-enhance-postnum-value"></span>`; const lastChild = [...posterInfo.querySelectorAll(".usercol")].pop(); insertAfter(span, lastChild); return span; } const div = document.createElement("DIV"); div.setAttribute("name", `s-user-enhance-postnum`); div.style = "float: left; min-width: 50%"; div.innerHTML = ` <nobr> <span>发帖: <span class="userval numericl" name="s-user-enhance-postnum-value"></span></span> </nobr>`; const lastChild = posterInfo.querySelector('.stat DIV[class="clear"]'); insertBefore(div, lastChild); return div; })(); const value = element.querySelector( "[name='s-user-enhance-postnum-value']" ); const visible = custom.includes("发帖"); if (visible) { const { postnum, regdate } = commonui.userInfo.users[uid]; const days = Math.ceil((Date.now() / 1000 - regdate) / (24 * 60 * 60)); const postnumPerDay = postnum / days; value.title = `日均: ${postnumPerDay.toFixed(1)}`; value.innerHTML = postnum; } element.setAttribute("s-user-enhance-visible", visible); } // 属地 { const element = (() => { const anchor = posterInfo.querySelector( "[name='s-user-enhance-ipLoc']" ); if (anchor) { return anchor; } if (lite) { const span = document.createElement("SPAN"); span.setAttribute("name", `s-user-enhance-ipLoc`); span.className = "usercol nobr"; span.innerHTML = `<span class="userval" name="s-user-enhance-ipLoc-value"></span>`; const lastChild = [...posterInfo.querySelectorAll(".usercol")].pop(); insertAfter(span, lastChild); return span; } const div = document.createElement("DIV"); div.setAttribute("name", `s-user-enhance-ipLoc`); div.style = "float: left; min-width: 50%"; div.innerHTML = `<span name="s-user-enhance-ipLoc-value"></span>`; const lastChild = posterInfo.querySelector('.stat DIV[class="clear"]'); insertBefore(div, lastChild); return div; })(); const value = element.querySelector( "[name='s-user-enhance-ipLoc-value']" ); const visible = custom.includes("属地"); if (visible) { api.getIpLocations(uid).then((data) => { if (data.length) { value.innerHTML = `${lite ? "属地 " : "属地: "}${data .map( ({ ipLoc, timestamp }) => `<span class="userval" title="${ timestamp ? commonui.time2dis(timestamp / 1000) : "" }">${ipLoc}</span>` ) .join(", ")}`; element.setAttribute("s-user-enhance-visible", true); } }); } element.setAttribute("s-user-enhance-visible", false); } // 曾用名 { const element = (() => { const anchor = posterInfo.querySelector( "[name='s-user-enhance-oldname']" ); if (anchor) { return anchor; } if (lite) { const span = document.createElement("SPAN"); span.setAttribute("name", `s-user-enhance-oldname`); span.className = "usercol nobr"; span.innerHTML = `<span class="userval" name="s-user-enhance-oldname-value"></span>`; const lastChild = [...posterInfo.querySelectorAll(".usercol")].pop(); insertAfter(span, lastChild); return span; } const div = document.createElement("DIV"); div.setAttribute("name", `s-user-enhance-oldname`); div.style = "float: left; width: 100%"; div.innerHTML = `<span name="s-user-enhance-oldname-value"></span>`; const lastChild = posterInfo.querySelector('.stat DIV[class="clear"]'); insertBefore(div, lastChild); return div; })(); const value = element.querySelector( "[name='s-user-enhance-oldname-value']" ); const visible = custom.includes("曾用名"); if (visible) { api.getUsernameChanged(uid).then((data) => { const values = Object.values(data || {}); if (values.length) { value.innerHTML = `${lite ? "曾用名 " : "曾用名: "}${values .map( ({ username, time }) => `<span class="userval" title="${commonui.time2dis( time )}">${username}</span>` ) .join(", ")}`; element.setAttribute("s-user-enhance-visible", true); } }); } element.setAttribute("s-user-enhance-visible", false); } // 游戏档案 { const element = (() => { const anchor = posterInfo.querySelector( "[name='s-user-enhance-games']" ); if (anchor) { return anchor; } const div = document.createElement("DIV"); div.setAttribute("name", `s-user-enhance-games`); div.style = "margin: 0 -2px;"; div.innerHTML = ``; if (lite) { const lastChild = [...posterInfo.querySelectorAll(".usercol")].pop(); insertAfter(div, lastChild); } else { const lastChild = posterInfo.querySelector(".stat").lastChild; insertBefore(div, lastChild); } return div; })(); const visible = custom.includes("游戏档案"); if (visible) { element.innerHTML = ``; api.getUserGameInfo(uid).then((info) => { // Steam if (info.steam) { const { steam_user_id, steam_user_name } = info.steam; const steam = (() => { if (steam_user_id) { const element = document.createElement("A"); element.href = `https://steamcommunity.com/profiles/${steam_user_id}`; element.style = ` background-image: url(${unsafeWindow.__IMG_BASE}/misc/fid414/headline/icon03.png); background-repeat: no-repeat; background-position: 50% 50%; background-size: contain; width: 20px; height: 20px; display: inline-block; cursor: pointer; outline: none;`; element.title = `${steam_user_name}[${steam_user_id}]`; return element; } return null; })(); if (steam) { steam.style.margin = "2px"; element.appendChild(steam); } element.setAttribute("s-user-enhance-visible", true); } // PSN if (info.psn) { const { psn_user_id, psn_user_name } = info.psn; const psn = (() => { if (psn_user_name) { const element = document.createElement("A"); element.href = `https://psnprofiles.com/${psn_user_name}`; element.style = ` background-image: url(${unsafeWindow.__IMG_BASE}/misc/fid414/headline/icon05.png); background-repeat: no-repeat; background-position: 50% 50%; background-size: contain; width: 20px; height: 20px; display: inline-block; cursor: pointer; outline: none;`; element.title = `${psn_user_name}[${psn_user_id}]`; return element; } return null; })(); if (psn) { psn.style.margin = "2px"; element.appendChild(psn); } element.setAttribute("s-user-enhance-visible", true); } // NS if (info.nintendo) { const { user_info } = info.nintendo; const nintendo = (() => { if (user_info) { const { ns_nickname, ns_friendcode } = user_info.user; const element = document.createElement("A"); element.style = ` background-image: url(${unsafeWindow.__IMG_BASE}/misc/fid414/headline/icon01.png); background-repeat: no-repeat; background-position: 50% 50%; background-size: contain; width: 20px; height: 20px; display: inline-block; cursor: pointer; outline: none;`; element.title = `${ns_nickname}[${ ns_friendcode === "SW-XXXX-XXXX-XXXX" ? "-" : ns_friendcode }]`; return element; } return null; })(); if (nintendo) { nintendo.style.margin = "2px"; element.appendChild(nintendo); } element.setAttribute("s-user-enhance-visible", true); } // 刀塔 if (info.steam) { const { steam_user_id } = info.steam; const stratz = (() => { if (steam_user_id && unsafeWindow.__CURRENT_GFID === 321) { const shortID = Number.isSafeInteger(steam_user_id) ? steam_user_id : Number(steam_user_id.substr(-16, 16)) - 6561197960265728; const element = document.createElement("A"); element.href = `https://stratz.com/players/${shortID}`; element.style = ` background-image: url(${unsafeWindow.__IMG_BASE}/proxy/cache_attach/ficon/321u.png); background-repeat: no-repeat; background-position: 50% 50%; background-size: contain; width: 20px; height: 20px; display: inline-block; cursor: pointer; outline: none;`; element.title = shortID; return element; } return null; })(); if (stratz) { stratz.style.margin = "2px"; element.appendChild(stratz); } element.setAttribute("s-user-enhance-visible", true); } // 原神 if (info.genshin) { const { userInfo } = info.genshin; const genshin = (() => { if (userInfo.ys_id) { const element = document.createElement("A"); element.style = ` background-image: url(${unsafeWindow.__IMG_BASE}/proxy/cache_attach/ficon/650u.png); background-repeat: no-repeat; background-position: 50% 50%; background-size: contain; width: 20px; height: 20px; display: inline-block; cursor: pointer; outline: none;`; element.title = `${userInfo.nickname}[${userInfo.ys_id}]`; return element; } return null; })(); if (genshin) { genshin.style.margin = "2px"; element.appendChild(genshin); } element.setAttribute("s-user-enhance-visible", true); } // 深空之眼 if (info.skzy) { const { skzy_uid, nick_name } = info.skzy; const skzy = (() => { if (skzy_uid) { const element = document.createElement("A"); element.style = ` background-image: url(${unsafeWindow.__IMG_BASE}/proxy/cache_attach/ficon/848u.png); background-repeat: no-repeat; background-position: 50% 50%; background-size: contain; width: 20px; height: 20px; display: inline-block; cursor: pointer; outline: none;`; element.title = `${nick_name}[${skzy_uid}]`; return element; } return null; })(); if (skzy) { skzy.style.margin = "2px"; element.appendChild(skzy); } element.setAttribute("s-user-enhance-visible", true); } }); } element.setAttribute("s-user-enhance-visible", false); } // 刀塔段位 { const element = (() => { const anchor = posterInfo.querySelector( "[name='s-user-enhance-dota-rank']" ); if (anchor) { return anchor; } const div = document.createElement("DIV"); div.setAttribute("name", `s-user-enhance-dota-rank`); div.style = "margin: 2px 0"; div.innerHTML = ``; if (lite) { return null; } const lastChild = posterInfo.querySelector(".stat"); insertAfter(div, lastChild); return div; })(); if (element) { const visible = custom.includes("刀塔段位"); if (visible) { element.innerHTML = ``; api.getSteamInfo(uid).then(async ({ steam_user_id }) => { if (steam_user_id) { const shortID = Number.isSafeInteger(steam_user_id) ? steam_user_id : Number(steam_user_id.substr(-16, 16)) - 6561197960265728; // TODO 代码优化 // 简单的缓存,同一个人每天只请求一次 const data = (await cache.get("DotaRank")) || {}; const info = await new Promise((resolve) => { if (data[shortID]) { const { timestamp } = data[shortID]; const date = new Date(timestamp); const isToday = Tools.dateIsToday(date); if (isToday) { resolve(data[shortID]); return; } delete data[shortID]; } fetch(`https://api.opendota.com/api/players/${shortID}`) .then((res) => res.json()) .then((res) => { if (res) { data[shortID] = { ...res, timestamp: new Date().getTime(), }; cache.put("DotaRank", data); resolve(res); return; } resolve(null); }) .catch(() => { resolve(null); }); }); if (info.profile) { const { rank_tier, leaderboard_rank } = info; const medals = [ "先锋", "卫士", "中#", "统帅", "传奇", "万古流芳", "超凡入圣", "冠绝一世", ]; const medal = Math.floor(rank_tier / 10); const star = rank_tier % 10; element.innerHTML = ` <div style=" width: 64px; height: 64px; display: inline-flex; -webkit-box-pack: center; justify-content: center; -webkit-box-align: center; align-items: center; position: relative; font-size: 10px; overflow: hidden; " title="${ medals[medal - 1] ? `${medals[medal - 1]}[${leaderboard_rank || star}]` : "" }"> <svg viewBox="0 0 256 256" style="max-width: 256px; max-height: 256px"> <image href="https://cdn.stratz.com/images/dota2/seasonal_rank/medal_${medal}.png" height="100%" width="100%"></image> ${ star > 0 ? `<image href="https://cdn.stratz.com/images/dota2/seasonal_rank/star_${star}.png" height="100%" width="100%"></image>` : "" } </svg> ${ leaderboard_rank ? `<div style=" background-color: rgba(0, 0, 0, 0.7); border-radius: 4px; color: rgba(255, 255, 255, 0.8); padding: 0.2em 0.3em 0.3em; position: absolute; line-height: normal; bottom: 0; ">${leaderboard_rank}</div>` : "" } </div>`; element.setAttribute("s-user-enhance-visible", true); } } }); } element.setAttribute("s-user-enhance-visible", false); } } }; /** * 插入至元素之前 * @param {HTMLElement} element 新元素 * @param {HTMLElement} target 目标元素 */ const insertBefore = (element, target) => { const parentNode = target.parentNode; parentNode.insertBefore(element, target); }; /** * 插入至元素之后 * @param {HTMLElement} element 新元素 * @param {HTMLElement} target 目标元素 */ const insertAfter = (element, target) => { const parentNode = target.parentNode; if (parentNode.lastChild === target) { parentNode.appendChild(element); return; } parentNode.insertBefore(element, target.nextSibling); }; // 主函数 (async () => { // 初始化缓存和 API 并绑定 const libs = initCacheAndAPI(); cache = libs.cache; api = libs.api; // 注册脚本菜单 registerMenu(); // 处理 commonui 模块 if (unsafeWindow.commonui) { handleCommonui(unsafeWindow.commonui); return; } Tools.interceptProperty(unsafeWindow, "commonui", { afterSet: (value) => { handleCommonui(value); }, }); })(); })();