您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds player filters to various userlists in Torn City
// ==UserScript== // @name Player Filters // @namespace dev.kwack.torn.player-filters // @version 0.0.3 // @description Adds player filters to various userlists in Torn City // @author Kwack [2190604] // @match https://www.torn.com/* // @icon // @grant none // ==/UserScript== // THIS SCRIPT IS STILL BEING TESTED, do NOT expect all to work perfectly. // If you find any issues, please report them to me on Torn or Discord. (() => { const STATUS_ENUM = { OKAY: "OKAY", HOSPITAL: "HOSPITAL", JAIL: "JAIL", FALLEN: "FALLEN", FEDERAL: "FEDERAL", TRAVELING: "TRAVELING", ABROAD: "ABROAD", UNKNOWN: "UNKNOWN", }; const ONLINE_STATUS_ENUM = { ONLINE: "ONLINE", OFFLINE: "OFFLINE", IDLE: "IDLE", UNKNOWN: "UNKNOWN", }; const FILTERS = [ { check: () => document.location.pathname === "/blacklist.php" || document.location.pathname === "/friendlist.php", table: () => $("div.content-wrapper > div.blacklist > ul.user-info-blacklist-wrap")[0], insertFilters: (f) => f.insertBefore($("div.content-wrapper > div.blacklist > hr")), rows: (t) => t.children, filters: { name: { type: "text", fn: (r) => r.find("a.user.name span.honor-text:not(.honor-text-svg)").text(), }, id: { type: "text", fn: (r) => r .find("a.user.name") .attr("href") .match(/\?XID=([\d]+)/)[1], }, level: { type: "min-max", min: 1, max: 100, fn: (r) => r.find(".level")[0].lastChild.textContent.trim(), }, status: { type: "select", options: STATUS_ENUM, fn: (r) => STATUS_ENUM[r.find("div.status > span:last-child").text().trim().toUpperCase()] ?? STATUS_ENUM.UNKNOWN, }, online: { type: "select", options: ONLINE_STATUS_ENUM, fn: (r) => ONLINE_STATUS_ENUM[ r .find("ul#iconTray > .iconShow") .attr("title") .match(/<b>([\w]+)<\/b>/)[1] .toUpperCase() ] ?? ONLINE_STATUS_ENUM.UNKNOWN, }, }, }, { check: () => document.location.pathname === "/index.php" && $(document.body).data("abroad") && new URLSearchParams(document.location.search).get("page") === "people", table: () => $("div.content-wrapper > div.travel-people > ul.users-list")[0], rows: (t) => t.children, insertFilters: (f) => f.insertAfter($("div.content-wrapper > div.info-msg-cont").last()), filters: { name: { type: "text", fn: (r) => r.find("a.user.name span.honor-text:not(.honor-text-svg)").text(), }, id: { type: "text", fn: (r) => r .find("a.user.name") .attr("href") .match(/\?XID=([\d]+)/)[1], }, level: { type: "min-max", min: 1, max: 100, fn: (r) => r.find(".level")[0].lastChild.textContent.trim(), }, status: { type: "select", options: Object.entries(STATUS_ENUM) .filter(([k]) => k !== "ABROAD" && k !== "TRAVELING" && k !== "JAIL") .reduce((a, [k, v]) => ((a[k] = v), a), {}), fn: (r) => STATUS_ENUM[r.find("span.status > span:last-child").text().trim().toUpperCase()] ?? STATUS_ENUM.UNKNOWN, }, online: { type: "select", options: ONLINE_STATUS_ENUM, fn: (r) => ONLINE_STATUS_ENUM[ r .find("ul#iconTray > .iconShow") .attr("title") .match(/<b>([\w]+)<\/b>/)[1] .toUpperCase() ] ?? ONLINE_STATUS_ENUM.UNKNOWN, }, }, }, { check: () => document.location.pathname === "/bounties.php", table: () => $( "div.content-wrapper > div.newspaper-wrap div.bounties-wrap > div.bounties-cont > ul.bounties-list" )[0], insertFilters: (f) => f.insertBefore($("div.content-wrapper > div.newspaper-wrap div.bounties-wrap")), rows: (t) => [...t.children].filter((c) => c.getAttribute("data-id")), filters: { name: { type: "text", fn: (r) => r.find("ul.item div.target > a").text(), }, id: { type: "text", fn: (r) => r .find("ul.item div.target > a") .attr("href") .match(/\?XID=([\d]+)/)[1], }, level: { type: "min-max", min: 1, max: 100, fn: (r) => r.find("ul.item div.level")[0].lastChild.textContent.trim(), }, status: { type: "select", options: STATUS_ENUM, fn: (r) => STATUS_ENUM[r.find("ul.item div.status").children().last().text().toUpperCase()] ?? STATUS_ENUM.UNKNOWN, }, }, }, { check: () => document.location.pathname === "/page.php" && new URLSearchParams(document.location.search).get("sid").toLowerCase() === "userlist", table: () => $("div.content-wrapper > div.userlist-wrapper > ul.user-info-list-wrap")[0], rows: (t) => t.children, insertFilters: (f) => f.insertAfter($("div.content-wrapper > div.content-title")), filters: { name: { type: "text", fn: (r) => r.find("a.user.name span.honor-text:not(.honor-text-svg)").text().trim(), }, id: { type: "text", fn: (r) => r .find("a.user.name") .attr("href") .match(/\?XID=([\d]+)/)[1], }, level: { type: "min-max", min: 1, max: 100, fn: (r) => r.find(".level").children().last().text().trim(), }, status: { type: "select", // There's no way to differentiate between traveling and abroad, so all are set to traveling. options: Object.entries(STATUS_ENUM) .filter(([k]) => k !== "ABROAD") .reduce((a, [k, v]) => ((a[k] = v), a), {}), fn: (r) => { const icons = r.find("div.level-icons-wrap > .user-icons ul#iconTray > li").toArray(); for (const i of icons) { const iconNumber = i.id?.match(/^icon([\d]+)_/)?.[1]; switch (iconNumber) { case "15": return STATUS_ENUM.HOSPITAL; case "16": return STATUS_ENUM.JAIL; case "70": return STATUS_ENUM.FEDERAL; case "71": return STATUS_ENUM.TRAVELING; case "77": return STATUS_ENUM.FALLEN; } } return STATUS_ENUM.OKAY; }, online: { type: "select", options: ONLINE_STATUS_ENUM, fn: (r) => ONLINE_STATUS_ENUM[ r .find("li > div:not(.level-icons-wrap) > ul#iconTray > li") .text() .match(/<b>([\w]+)<\/b>/)[1] .toUpperCase() ] ?? ONLINE_STATUS_ENUM.UNKNOWN, }, }, }, }, { check: () => { if (document.location.pathname !== "/factions.php") return false; const params = new URLSearchParams(document.location.search); if (params.get("step") === "profile") return true; if (params.get("step") === "your" && document.location.hash.includes("tab=info")) return true; return false; }, }, ]; function init() { // Finds first filter where check is valid const filter = FILTERS.find((f) => f.check()); if (!filter) return; // No filter found let f = createFilter(filter); new MutationObserver(() => { if (document.contains(f[0])) return; if (!filter.table()) return; showAllRows(filter); f = createFilter(filter); }).observe(document.body, { childList: true, subtree: true }); injectStyle(); } function createFilter(filterOptions) { const filters = $("<div/>", { id: "kw--filter-container", style: "display: flex;justify-content: space-between;background: linear-gradient(to bottom, #ff149311, #ff149344);" + "padding: 10px;border-radius: 10px; margin: 10px 0;flex-wrap: wrap;gap: 10px;", }); Object.entries(filterOptions.filters).forEach(([name, { type, min, max, options, fn }]) => { const filter = $("<div/>", { class: "kw--filter", style: "display: flex; flex-direction: column; gap: 0.25rem;", }).appendTo(filters); filter.append( $("<label/>", { text: name + ":", style: "font-weight: bolder; font-size: 0.9rem", }) ); switch (type) { case "text": filter.append( $("<input/>", { type: "text", class: "kw--filter-text kw--filter-row", }) .data("filter", { name, type, fn }) .on("input", () => handleFilterChange(filterOptions)) ); break; case "min-max": filter.append( $("<input/>", { type: "number", class: "kw--filter-min kw--filter-row", min, max, style: "min-width: 50px;", }) .attr("placeholder", "min") .data("filter", { name, type, is: "min", fn }) .on("input", () => handleFilterChange(filterOptions)) ); filter.append( $("<input/>", { type: "number", class: "kw--filter-max kw--filter-row", min, max, style: "min-width: 50px;", }) .attr("placeholder", "max") .data("filter", { name, type, is: "max", fn }) .on("input", () => handleFilterChange(filterOptions)) ); break; case "select": const select = $("<select/>", { class: "kw--filter-select kw--filter-row", }) .data("filter", { name, type, fn }) .on("change", () => handleFilterChange(filterOptions)) .append( $("<option/>", { value: "", text: "ANY", }) ) .appendTo(filter); Object.entries(options) .filter(([key, value]) => key !== "UNKNOWN" || value !== "UNKNOWN") .forEach(([key, value]) => { select.append( $("<option/>", { value: key, text: value, }) ); }); break; } }); filterOptions.insertFilters(filters); return filters; } function handleFilterChange(filterOptions) { const table = filterOptions.table(); if (!table) return console.error("[kw--player-filters]: Table could not be found"); const rows = filterOptions.rows(table); const filters = $(".kw--filter-row") .toArray() .map((f) => ({ f: $(f).data("filter"), e: $(f) })); [...rows].forEach((r) => { const row = $(r); const data = filters.map(({ f, e }) => { const value = f.fn(row); const filterVal = e.val(); if (!filterVal || !value) return true; try { switch (f.type) { case "text": return value.toLowerCase().includes(filterVal.toLowerCase()); case "min-max": if (f.is === "min") return parseInt(value) >= parseInt(filterVal); if (f.is === "max") return parseInt(value) <= parseInt(filterVal); return true; case "select": return value === filterVal; } } catch (e) { console.error(e); console.debug({ f, row, value }); return true; } }); if (data.every((d) => d)) row.show(); else row.hide(); }); } function showAllRows(pageData) { const table = pageData.table(); if (!table) return console.error("[kw--player-filters]: Table could not be found"); const rows = pageData.rows(table); [...rows].forEach((r) => $(r).show()); } init(); function injectStyle() { const style = ` #kw--filter-container input { border: 1px solid var(--input-border-color, #ccc); border-radius: 5px; font-family: Arial, serif; color: var(--input-color, #000); background: var(--input-background-color, #fff); padding: 9px 10px; } #kw--filter-container select { height: 34px; line-height: 34px; color: #444; border: 1px solid var(--default-panel-divider-inner-side-color, #fff); border-radius: 5px; background: linear-gradient(to bottom, #e4e4e4, #f2f2f2); } body.dark-mode #kw--filter-container select { color: #ddd; background: #000; border-color: #444; } `; if (typeof GM_addStyle !== "undefined") return GM_addStyle(style); $(document.head).append($("<style/>", { text: style })); } })();