Greasy Fork镜像 支持简体中文。

Player Filters

Adds player filters to various userlists in Torn City

  1. // ==UserScript==
  2. // @name Player Filters
  3. // @namespace dev.kwack.torn.player-filters
  4. // @version 0.0.3
  5. // @description Adds player filters to various userlists in Torn City
  6. // @author Kwack [2190604]
  7. // @match https://www.torn.com/*
  8. // @icon
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. // THIS SCRIPT IS STILL BEING TESTED, do NOT expect all to work perfectly.
  13. // If you find any issues, please report them to me on Torn or Discord.
  14.  
  15. (() => {
  16. const STATUS_ENUM = {
  17. OKAY: "OKAY",
  18. HOSPITAL: "HOSPITAL",
  19. JAIL: "JAIL",
  20. FALLEN: "FALLEN",
  21. FEDERAL: "FEDERAL",
  22. TRAVELING: "TRAVELING",
  23. ABROAD: "ABROAD",
  24. UNKNOWN: "UNKNOWN",
  25. };
  26.  
  27. const ONLINE_STATUS_ENUM = {
  28. ONLINE: "ONLINE",
  29. OFFLINE: "OFFLINE",
  30. IDLE: "IDLE",
  31. UNKNOWN: "UNKNOWN",
  32. };
  33.  
  34. const FILTERS = [
  35. {
  36. check: () =>
  37. document.location.pathname === "/blacklist.php" || document.location.pathname === "/friendlist.php",
  38. table: () => $("div.content-wrapper > div.blacklist > ul.user-info-blacklist-wrap")[0],
  39. insertFilters: (f) => f.insertBefore($("div.content-wrapper > div.blacklist > hr")),
  40. rows: (t) => t.children,
  41. filters: {
  42. name: {
  43. type: "text",
  44. fn: (r) => r.find("a.user.name span.honor-text:not(.honor-text-svg)").text(),
  45. },
  46. id: {
  47. type: "text",
  48. fn: (r) =>
  49. r
  50. .find("a.user.name")
  51. .attr("href")
  52. .match(/\?XID=([\d]+)/)[1],
  53. },
  54. level: {
  55. type: "min-max",
  56. min: 1,
  57. max: 100,
  58. fn: (r) => r.find(".level")[0].lastChild.textContent.trim(),
  59. },
  60. status: {
  61. type: "select",
  62. options: STATUS_ENUM,
  63. fn: (r) =>
  64. STATUS_ENUM[r.find("div.status > span:last-child").text().trim().toUpperCase()] ??
  65. STATUS_ENUM.UNKNOWN,
  66. },
  67. online: {
  68. type: "select",
  69. options: ONLINE_STATUS_ENUM,
  70. fn: (r) =>
  71. ONLINE_STATUS_ENUM[
  72. r
  73. .find("ul#iconTray > .iconShow")
  74. .attr("title")
  75. .match(/<b>([\w]+)<\/b>/)[1]
  76. .toUpperCase()
  77. ] ?? ONLINE_STATUS_ENUM.UNKNOWN,
  78. },
  79. },
  80. },
  81. {
  82. check: () =>
  83. document.location.pathname === "/index.php" &&
  84. $(document.body).data("abroad") &&
  85. new URLSearchParams(document.location.search).get("page") === "people",
  86. table: () => $("div.content-wrapper > div.travel-people > ul.users-list")[0],
  87. rows: (t) => t.children,
  88. insertFilters: (f) => f.insertAfter($("div.content-wrapper > div.info-msg-cont").last()),
  89. filters: {
  90. name: {
  91. type: "text",
  92. fn: (r) => r.find("a.user.name span.honor-text:not(.honor-text-svg)").text(),
  93. },
  94. id: {
  95. type: "text",
  96. fn: (r) =>
  97. r
  98. .find("a.user.name")
  99. .attr("href")
  100. .match(/\?XID=([\d]+)/)[1],
  101. },
  102. level: {
  103. type: "min-max",
  104. min: 1,
  105. max: 100,
  106. fn: (r) => r.find(".level")[0].lastChild.textContent.trim(),
  107. },
  108. status: {
  109. type: "select",
  110. options: Object.entries(STATUS_ENUM)
  111. .filter(([k]) => k !== "ABROAD" && k !== "TRAVELING" && k !== "JAIL")
  112. .reduce((a, [k, v]) => ((a[k] = v), a), {}),
  113. fn: (r) =>
  114. STATUS_ENUM[r.find("span.status > span:last-child").text().trim().toUpperCase()] ??
  115. STATUS_ENUM.UNKNOWN,
  116. },
  117. online: {
  118. type: "select",
  119. options: ONLINE_STATUS_ENUM,
  120. fn: (r) =>
  121. ONLINE_STATUS_ENUM[
  122. r
  123. .find("ul#iconTray > .iconShow")
  124. .attr("title")
  125. .match(/<b>([\w]+)<\/b>/)[1]
  126. .toUpperCase()
  127. ] ?? ONLINE_STATUS_ENUM.UNKNOWN,
  128. },
  129. },
  130. },
  131. {
  132. check: () => document.location.pathname === "/bounties.php",
  133. table: () =>
  134. $(
  135. "div.content-wrapper > div.newspaper-wrap div.bounties-wrap > div.bounties-cont > ul.bounties-list"
  136. )[0],
  137. insertFilters: (f) => f.insertBefore($("div.content-wrapper > div.newspaper-wrap div.bounties-wrap")),
  138. rows: (t) => [...t.children].filter((c) => c.getAttribute("data-id")),
  139. filters: {
  140. name: {
  141. type: "text",
  142. fn: (r) => r.find("ul.item div.target > a").text(),
  143. },
  144. id: {
  145. type: "text",
  146. fn: (r) =>
  147. r
  148. .find("ul.item div.target > a")
  149. .attr("href")
  150. .match(/\?XID=([\d]+)/)[1],
  151. },
  152. level: {
  153. type: "min-max",
  154. min: 1,
  155. max: 100,
  156. fn: (r) => r.find("ul.item div.level")[0].lastChild.textContent.trim(),
  157. },
  158. status: {
  159. type: "select",
  160. options: STATUS_ENUM,
  161. fn: (r) =>
  162. STATUS_ENUM[r.find("ul.item div.status").children().last().text().toUpperCase()] ??
  163. STATUS_ENUM.UNKNOWN,
  164. },
  165. },
  166. },
  167. {
  168. check: () =>
  169. document.location.pathname === "/page.php" &&
  170. new URLSearchParams(document.location.search).get("sid").toLowerCase() === "userlist",
  171. table: () => $("div.content-wrapper > div.userlist-wrapper > ul.user-info-list-wrap")[0],
  172. rows: (t) => t.children,
  173. insertFilters: (f) => f.insertAfter($("div.content-wrapper > div.content-title")),
  174. filters: {
  175. name: {
  176. type: "text",
  177. fn: (r) => r.find("a.user.name span.honor-text:not(.honor-text-svg)").text().trim(),
  178. },
  179. id: {
  180. type: "text",
  181. fn: (r) =>
  182. r
  183. .find("a.user.name")
  184. .attr("href")
  185. .match(/\?XID=([\d]+)/)[1],
  186. },
  187. level: {
  188. type: "min-max",
  189. min: 1,
  190. max: 100,
  191. fn: (r) => r.find(".level").children().last().text().trim(),
  192. },
  193. status: {
  194. type: "select",
  195. // There's no way to differentiate between traveling and abroad, so all are set to traveling.
  196. options: Object.entries(STATUS_ENUM)
  197. .filter(([k]) => k !== "ABROAD")
  198. .reduce((a, [k, v]) => ((a[k] = v), a), {}),
  199. fn: (r) => {
  200. const icons = r.find("div.level-icons-wrap > .user-icons ul#iconTray > li").toArray();
  201. for (const i of icons) {
  202. const iconNumber = i.id?.match(/^icon([\d]+)_/)?.[1];
  203. switch (iconNumber) {
  204. case "15":
  205. return STATUS_ENUM.HOSPITAL;
  206. case "16":
  207. return STATUS_ENUM.JAIL;
  208. case "70":
  209. return STATUS_ENUM.FEDERAL;
  210. case "71":
  211. return STATUS_ENUM.TRAVELING;
  212. case "77":
  213. return STATUS_ENUM.FALLEN;
  214. }
  215. }
  216. return STATUS_ENUM.OKAY;
  217. },
  218. online: {
  219. type: "select",
  220. options: ONLINE_STATUS_ENUM,
  221. fn: (r) =>
  222. ONLINE_STATUS_ENUM[
  223. r
  224. .find("li > div:not(.level-icons-wrap) > ul#iconTray > li")
  225. .text()
  226. .match(/<b>([\w]+)<\/b>/)[1]
  227. .toUpperCase()
  228. ] ?? ONLINE_STATUS_ENUM.UNKNOWN,
  229. },
  230. },
  231. },
  232. },
  233. {
  234. check: () => {
  235. if (document.location.pathname !== "/factions.php") return false;
  236. const params = new URLSearchParams(document.location.search);
  237. if (params.get("step") === "profile") return true;
  238. if (params.get("step") === "your" && document.location.hash.includes("tab=info")) return true;
  239. return false;
  240. },
  241. },
  242. ];
  243.  
  244. function init() {
  245. // Finds first filter where check is valid
  246. const filter = FILTERS.find((f) => f.check());
  247. if (!filter) return; // No filter found
  248. let f = createFilter(filter);
  249. new MutationObserver(() => {
  250. if (document.contains(f[0])) return;
  251. if (!filter.table()) return;
  252. showAllRows(filter);
  253. f = createFilter(filter);
  254. }).observe(document.body, { childList: true, subtree: true });
  255. injectStyle();
  256. }
  257.  
  258. function createFilter(filterOptions) {
  259. const filters = $("<div/>", {
  260. id: "kw--filter-container",
  261. style:
  262. "display: flex;justify-content: space-between;background: linear-gradient(to bottom, #ff149311, #ff149344);" +
  263. "padding: 10px;border-radius: 10px; margin: 10px 0;flex-wrap: wrap;gap: 10px;",
  264. });
  265. Object.entries(filterOptions.filters).forEach(([name, { type, min, max, options, fn }]) => {
  266. const filter = $("<div/>", {
  267. class: "kw--filter",
  268. style: "display: flex; flex-direction: column; gap: 0.25rem;",
  269. }).appendTo(filters);
  270. filter.append(
  271. $("<label/>", {
  272. text: name + ":",
  273. style: "font-weight: bolder; font-size: 0.9rem",
  274. })
  275. );
  276. switch (type) {
  277. case "text":
  278. filter.append(
  279. $("<input/>", {
  280. type: "text",
  281. class: "kw--filter-text kw--filter-row",
  282. })
  283. .data("filter", { name, type, fn })
  284. .on("input", () => handleFilterChange(filterOptions))
  285. );
  286. break;
  287. case "min-max":
  288. filter.append(
  289. $("<input/>", {
  290. type: "number",
  291. class: "kw--filter-min kw--filter-row",
  292. min,
  293. max,
  294. style: "min-width: 50px;",
  295. })
  296. .attr("placeholder", "min")
  297. .data("filter", { name, type, is: "min", fn })
  298. .on("input", () => handleFilterChange(filterOptions))
  299. );
  300. filter.append(
  301. $("<input/>", {
  302. type: "number",
  303. class: "kw--filter-max kw--filter-row",
  304. min,
  305. max,
  306. style: "min-width: 50px;",
  307. })
  308. .attr("placeholder", "max")
  309. .data("filter", { name, type, is: "max", fn })
  310. .on("input", () => handleFilterChange(filterOptions))
  311. );
  312. break;
  313. case "select":
  314. const select = $("<select/>", {
  315. class: "kw--filter-select kw--filter-row",
  316. })
  317. .data("filter", { name, type, fn })
  318. .on("change", () => handleFilterChange(filterOptions))
  319. .append(
  320. $("<option/>", {
  321. value: "",
  322. text: "ANY",
  323. })
  324. )
  325. .appendTo(filter);
  326. Object.entries(options)
  327. .filter(([key, value]) => key !== "UNKNOWN" || value !== "UNKNOWN")
  328. .forEach(([key, value]) => {
  329. select.append(
  330. $("<option/>", {
  331. value: key,
  332. text: value,
  333. })
  334. );
  335. });
  336. break;
  337. }
  338. });
  339. filterOptions.insertFilters(filters);
  340. return filters;
  341. }
  342.  
  343. function handleFilterChange(filterOptions) {
  344. const table = filterOptions.table();
  345. if (!table) return console.error("[kw--player-filters]: Table could not be found");
  346. const rows = filterOptions.rows(table);
  347. const filters = $(".kw--filter-row")
  348. .toArray()
  349. .map((f) => ({ f: $(f).data("filter"), e: $(f) }));
  350. [...rows].forEach((r) => {
  351. const row = $(r);
  352. const data = filters.map(({ f, e }) => {
  353. const value = f.fn(row);
  354. const filterVal = e.val();
  355. if (!filterVal || !value) return true;
  356. try {
  357. switch (f.type) {
  358. case "text":
  359. return value.toLowerCase().includes(filterVal.toLowerCase());
  360. case "min-max":
  361. if (f.is === "min") return parseInt(value) >= parseInt(filterVal);
  362. if (f.is === "max") return parseInt(value) <= parseInt(filterVal);
  363. return true;
  364. case "select":
  365. return value === filterVal;
  366. }
  367. } catch (e) {
  368. console.error(e);
  369. console.debug({ f, row, value });
  370. return true;
  371. }
  372. });
  373. if (data.every((d) => d)) row.show();
  374. else row.hide();
  375. });
  376. }
  377.  
  378. function showAllRows(pageData) {
  379. const table = pageData.table();
  380. if (!table) return console.error("[kw--player-filters]: Table could not be found");
  381. const rows = pageData.rows(table);
  382. [...rows].forEach((r) => $(r).show());
  383. }
  384.  
  385. init();
  386.  
  387. function injectStyle() {
  388. const style = `
  389. #kw--filter-container input {
  390. border: 1px solid var(--input-border-color, #ccc);
  391. border-radius: 5px;
  392. font-family: Arial, serif;
  393. color: var(--input-color, #000);
  394. background: var(--input-background-color, #fff);
  395. padding: 9px 10px;
  396. }
  397.  
  398. #kw--filter-container select {
  399. height: 34px;
  400. line-height: 34px;
  401. color: #444;
  402. border: 1px solid var(--default-panel-divider-inner-side-color, #fff);
  403. border-radius: 5px;
  404. background: linear-gradient(to bottom, #e4e4e4, #f2f2f2);
  405. }
  406.  
  407. body.dark-mode #kw--filter-container select {
  408. color: #ddd;
  409. background: #000;
  410. border-color: #444;
  411. }
  412. `;
  413. if (typeof GM_addStyle !== "undefined") return GM_addStyle(style);
  414. $(document.head).append($("<style/>", { text: style }));
  415. }
  416. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址