Luogu Alias And Customize Tags

try to take over the world!

  1. // ==UserScript==
  2. // @name Luogu Alias And Customize Tags
  3. // @namespace http://tampermonkey.net/
  4. // @version 2025-01-23 15:17
  5. // @description try to take over the world!
  6. // @author normalpcer
  7. // @match https://www.luogu.com.cn/*
  8. // @match https://www.luogu.com/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=luogu.com.cn
  10. // @grant none
  11. // @license MIT
  12. // ==/UserScript==
  13. /**
  14. * 自定义类名、LocalStorage 项的前缀。
  15. * 为了避免与其他插件重名。
  16. */
  17. const Prefix = "normalpcer-alias-tags-";
  18. const Cooldown = 1000; // 两次修改的冷却时间(毫秒)
  19. const Colors = new Map(Object.entries({
  20. purple: "#9d3dcf",
  21. red: "#fe4c61",
  22. orange: "#f39c11",
  23. green: "#52c41a",
  24. blue: "#3498db",
  25. gray: "#bfbfbf",
  26. }));
  27. /**
  28. * 用于确定一个用户
  29. */
  30. class UserIdentifier {
  31. uid; // 0 为无效项
  32. username;
  33. constructor(uid, username) {
  34. this.uid = uid;
  35. this.username = username;
  36. }
  37. static fromUid(uid) {
  38. console.log(`UserIdentifier::fromUid(${uid})`);
  39. let res = uidToIdentifier.get(uid);
  40. if (res !== undefined) {
  41. return new Promise((resolve, _) => {
  42. resolve(res);
  43. });
  44. }
  45. // 否则,直接通过 API 爬取
  46. const APIBase = "/api/user/search?keyword=";
  47. let api = APIBase + uid.toString();
  48. console.log("api: " + api);
  49. let xml = new XMLHttpRequest();
  50. xml.open("GET", api);
  51. return new Promise((resolve, reject) => {
  52. xml.addEventListener("loadend", () => {
  53. console.log("status: " + xml.status);
  54. console.log("response: " + xml.responseText);
  55. if (xml.status === 200) {
  56. let json = JSON.parse(xml.responseText);
  57. let users = json["users"];
  58. if (users.length !== 1) {
  59. reject();
  60. }
  61. else {
  62. let uid = users[0]["uid"];
  63. let username = users[0]["name"];
  64. let identifier = new UserIdentifier(uid, username);
  65. uidToIdentifier.set(uid, identifier);
  66. usernameToIdentifier.set(username, identifier);
  67. resolve(identifier);
  68. }
  69. }
  70. else {
  71. reject();
  72. }
  73. });
  74. xml.send();
  75. });
  76. }
  77. static fromUsername(username) {
  78. console.log(`UserIdentifier::fromUsername(${username})`);
  79. let res = usernameToIdentifier.get(username);
  80. if (res !== undefined) {
  81. return new Promise((resolve) => {
  82. resolve(res);
  83. });
  84. }
  85. const APIBase = "/api/user/search?keyword=";
  86. let api = APIBase + username;
  87. let xml = new XMLHttpRequest();
  88. xml.open("GET", api);
  89. return new Promise((resolve, reject) => {
  90. xml.addEventListener("loadend", () => {
  91. console.log("response: ", xml.responseText);
  92. if (xml.status === 200) {
  93. let json = JSON.parse(xml.responseText);
  94. let users = json["users"];
  95. if (users.length !== 1) {
  96. reject();
  97. }
  98. else {
  99. let uid = users[0]["uid"];
  100. let username = users[0]["name"];
  101. let identifier = new UserIdentifier(uid, username);
  102. uidToIdentifier.set(uid, identifier);
  103. usernameToIdentifier.set(username, identifier);
  104. resolve(identifier);
  105. }
  106. }
  107. else {
  108. reject();
  109. }
  110. });
  111. xml.send();
  112. });
  113. }
  114. /**
  115. * 通过用户给定的字符串,自动判断类型并创建 UserIdentifier 对象。
  116. * @param s 新创建的 UserIdentifier 对象
  117. */
  118. static fromAny(s) {
  119. // 保证:UID 一定为数字
  120. // 忽略首尾空格,如果是一段完整数字,视为 UID
  121. if (s.trim().match(/^\d+$/)) {
  122. return UserIdentifier.fromUid(parseInt(s));
  123. }
  124. else {
  125. return UserIdentifier.fromUsername(s);
  126. }
  127. }
  128. dump() {
  129. return { uid: this.uid, username: this.username };
  130. }
  131. }
  132. let uidToIdentifier = new Map();
  133. let usernameToIdentifier = new Map();
  134. class UsernameAlias {
  135. id;
  136. newName;
  137. constructor(id, newName) {
  138. this.id = id;
  139. this.newName = newName;
  140. }
  141. /**
  142. * 在当前文档中应用别名。
  143. * 当前采用直接 dfs 全文替换的方式。
  144. */
  145. apply() {
  146. function dfs(p, alias) {
  147. // 进行一些特判。
  148. /**
  149. * 如果当前为私信页面,那么位于顶栏的用户名直接替换会出现问题。
  150. * 在原名的后面用括号标注别名,并且在修改时删除别名
  151. */
  152. if (window.location.href.includes("/chat")) {
  153. if (p.classList.contains("title")) {
  154. let a_list = p.querySelectorAll(`a[href*='/user/${alias.id.uid}']`);
  155. if (a_list.length === 1) {
  156. let a = a_list[0];
  157. if (a.children.length !== 1)
  158. return;
  159. let span = a.children[0];
  160. if (!(span instanceof HTMLSpanElement))
  161. return;
  162. if (span.innerText.includes(alias.id.username)) {
  163. if (span.getElementsByClassName(Prefix + "alias").length !== 0)
  164. return;
  165. // 尝试在里面添加一个 span 标注别名
  166. let alias_span = document.createElement("span");
  167. alias_span.classList.add(Prefix + "alias");
  168. alias_span.innerText = `(${alias.newName})`;
  169. span.appendChild(alias_span);
  170. // 在真实名称修改时删除别名
  171. let observer = new MutationObserver(() => {
  172. span.removeChild(alias_span);
  173. observer.disconnect();
  174. });
  175. observer.observe(span, {
  176. characterData: true,
  177. childList: true,
  178. subtree: true,
  179. attributes: true,
  180. });
  181. }
  182. }
  183. return;
  184. }
  185. }
  186. if (p.children.length == 0) {
  187. // 到达叶子节点,进行替换
  188. if (!p.innerText.includes(alias.id.username)) {
  189. return; // 尽量不做修改
  190. }
  191. p.innerText = p.innerText.replaceAll(alias.id.username, alias.newName);
  192. }
  193. else {
  194. for (let element of p.children) {
  195. if (element instanceof HTMLElement) {
  196. dfs(element, alias);
  197. }
  198. }
  199. }
  200. }
  201. dfs(document.body, this);
  202. }
  203. dump() {
  204. return { uid: this.id.uid, newName: this.newName };
  205. }
  206. }
  207. let aliases = new Map();
  208. let cache = new Map(); // 每个 UID 的缓存
  209. class SettingBoxItem {
  210. }
  211. class SettingBoxItemText {
  212. element = null;
  213. placeholder;
  214. constructor(placeholder) {
  215. this.placeholder = placeholder;
  216. }
  217. createElement() {
  218. if (this.element !== null) {
  219. throw "SettingBoxItemText::createElement(): this.element is not null.";
  220. }
  221. let new_element = document.createElement("input");
  222. new_element.placeholder = this.placeholder;
  223. this.element = new_element;
  224. return new_element;
  225. }
  226. getValue() {
  227. if (this.element instanceof HTMLInputElement) {
  228. return this.element.value;
  229. }
  230. else {
  231. throw "SettingBoxItemText::getValue(): this.element is not HTMLInputElement.";
  232. }
  233. }
  234. }
  235. /**
  236. * 位于主页的设置块
  237. */
  238. class SettingBox {
  239. title;
  240. items = [];
  241. placed = false; // 已经被放置
  242. callback = null; // 确定之后调用的函数
  243. constructor(title) {
  244. this.title = title;
  245. }
  246. /**
  247. * 使用一个新的函数处理用户输入
  248. * @param func 用作处理的函数
  249. */
  250. handle(func = null) {
  251. this.callback = func;
  252. }
  253. /**
  254. * 尝试在当前文档中放置设置块。
  255. * 如果已经存在,则不会做任何事。
  256. */
  257. place() {
  258. if (this.placed)
  259. return;
  260. let parent = document.getElementById(Prefix + "boxes-parent");
  261. if (!(parent instanceof HTMLDivElement))
  262. return;
  263. let new_element = document.createElement("div");
  264. new_element.classList.add("lg-article");
  265. // 标题元素
  266. let title_element = document.createElement("h2");
  267. title_element.innerText = this.title;
  268. // "收起"按钮
  269. let fold_button = document.createElement("span");
  270. fold_button.innerText = "[收起]";
  271. fold_button.style.marginLeft = "0.5em";
  272. fold_button.setAttribute("fold", "0");
  273. title_element.appendChild(fold_button);
  274. new_element.appendChild(title_element);
  275. // 依次创建接下来的询问
  276. let queries = document.createElement("div");
  277. for (let x of this.items) {
  278. queries.appendChild(x.createElement());
  279. }
  280. // “确定”按钮
  281. let confirm_button = document.createElement("input");
  282. confirm_button.type = "button";
  283. confirm_button.value = "确定";
  284. confirm_button.classList.add("am-btn", "am-btn-primary", "am-btn-sm");
  285. if (this.callback !== null) {
  286. let callback = this.callback;
  287. let args = this.items;
  288. confirm_button.onclick = () => callback(args);
  289. }
  290. queries.appendChild(confirm_button);
  291. new_element.appendChild(queries);
  292. fold_button.onclick = () => {
  293. if (fold_button.getAttribute("fold") === "0") {
  294. fold_button.innerText = "[展开]";
  295. fold_button.setAttribute("fold", "1");
  296. queries.style.display = "none";
  297. }
  298. else {
  299. fold_button.innerText = "[收起]";
  300. fold_button.setAttribute("fold", "0");
  301. queries.style.display = "block";
  302. }
  303. };
  304. parent.insertBefore(new_element, parent.children[0]); // 插入到开头
  305. this.placed = true;
  306. }
  307. }
  308. /**
  309. * 用户自定义标签
  310. */
  311. class UserTag {
  312. id;
  313. tag;
  314. constructor(id, tag) {
  315. this.id = id;
  316. this.tag = tag;
  317. }
  318. /**
  319. * 应用一个标签
  320. */
  321. apply() {
  322. // 寻找所有用户名出现的位置
  323. // 对于页面中的所有超链接,如果链接内容含有 "/user/uid",且为叶子节点,则认为这是一个用户名
  324. let feature = `/user/${this.id.uid}`;
  325. let selector = `a[href*='${feature}']`;
  326. if (window.location.href.includes(feature)) {
  327. selector += ", .user-name > span";
  328. }
  329. let links = document.querySelectorAll(selector);
  330. for (let link of links) {
  331. if (!(link instanceof HTMLElement)) {
  332. console.log("UserTag::apply(): link is not HTMLElement.");
  333. continue;
  334. }
  335. // 已经放置过标签
  336. if (link.parentElement?.getElementsByClassName(Prefix + "customized-tag").length !== 0) {
  337. // console.log("UserTag::apply(): already placed tag.");
  338. continue;
  339. }
  340. if (link.children.length === 1 && link.children[0] instanceof HTMLSpanElement) {
  341. // 特别地,仅有一个 span 是允许的
  342. link = link.children[0];
  343. }
  344. else if (link.children.length !== 0) {
  345. // 否则,要求 link 为叶子节点
  346. // console.log("UserTag::apply(): link is not a leaf node.");
  347. continue;
  348. }
  349. if (!(link instanceof HTMLElement))
  350. continue; // 让 Typescript 认为 link 是 HTMLElement
  351. // console.log(link);
  352. // 获取用户名颜色信息
  353. // - 如果存在颜色属性,直接使用
  354. // - 否则,尝试通过 class 推断颜色
  355. let existsColorStyle = false;
  356. let color = link.style.color;
  357. let colorHex = "";
  358. let colorName = ""; // 通过 class 推断的颜色名
  359. if (color !== "") {
  360. existsColorStyle = true;
  361. // 尝试解析十六进制颜色或者 rgb 颜色
  362. if (color.startsWith("#")) {
  363. colorHex = color;
  364. }
  365. else if (color.startsWith("rgb")) {
  366. let rgb = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
  367. if (rgb !== null) {
  368. // 十进制转为十六进制
  369. const f = (x) => parseInt(x).toString(16).padStart(2, "0");
  370. colorHex = "#" + f(rgb[1]) + f(rgb[2]) + f(rgb[3]);
  371. }
  372. else {
  373. throw "UserTag::apply(): cannot parse color " + color;
  374. }
  375. }
  376. else {
  377. throw "UserTag::apply(): cannot parse color " + color;
  378. }
  379. }
  380. else {
  381. // 尝试从类名推断
  382. let classList = link.classList;
  383. for (let x of classList) {
  384. if (x.startsWith("lg-fg-")) {
  385. colorName = x.substring(6);
  386. break;
  387. }
  388. }
  389. }
  390. if (!existsColorStyle && colorName === "") {
  391. // 尝试使用缓存中的颜色
  392. if (cache.has(this.id.uid)) {
  393. let data = cache.get(this.id.uid)?.get("color");
  394. console.log("data", data);
  395. if (data !== undefined && typeof data === "string") {
  396. colorHex = data;
  397. existsColorStyle = true;
  398. }
  399. }
  400. }
  401. // 完全无法推断,使用缺省值灰色
  402. if (!existsColorStyle && colorName === "") {
  403. let color = Colors.get("gray");
  404. if (color !== undefined) {
  405. colorHex = color;
  406. }
  407. else {
  408. throw "UserTag::apply(): cannot find color gray.";
  409. }
  410. }
  411. console.log(`tag ${this.tag} for ${this.id.uid}. colorHex = ${colorHex}, colorName = ${colorName}`);
  412. // 生成标签
  413. let new_element = document.createElement("span");
  414. new_element.classList.add("lg-bg-" + colorName);
  415. new_element.classList.add("am-badge");
  416. new_element.classList.add("am-radius");
  417. new_element.classList.add(Prefix + "customized-tag");
  418. new_element.innerText = this.tag;
  419. if (!existsColorStyle) {
  420. let color = Colors.get(colorName);
  421. if (color !== undefined) {
  422. colorHex = color;
  423. }
  424. else {
  425. throw "UserTag::apply(): cannot find color " + colorName;
  426. }
  427. }
  428. new_element.style.setProperty("background", colorHex, "important");
  429. new_element.style.setProperty("border-color", colorHex, "important");
  430. new_element.style.setProperty("color", "#fff", "important");
  431. // 特别地,如果 innerText 不以空格结尾,添加 0.3em 的 margin-left
  432. if (!link.innerText.endsWith(" ")) {
  433. new_element.style.marginLeft = "0.3em";
  434. }
  435. // 插入到文档中
  436. if (!(link instanceof HTMLAnchorElement)) {
  437. if (link.parentElement instanceof HTMLAnchorElement) {
  438. link = link.parentElement;
  439. }
  440. }
  441. if (!(link instanceof HTMLElement)) {
  442. throw "UserTag::apply(): link is not HTMLElement before insertion.";
  443. }
  444. let parent = link.parentElement;
  445. if (parent === null) {
  446. throw "UserTag::apply(): cannot find parent.";
  447. }
  448. // 在 link 之后
  449. if (parent.lastChild === link) {
  450. parent.appendChild(new_element);
  451. }
  452. else {
  453. parent.insertBefore(new_element, link.nextSibling);
  454. }
  455. // 在原始元素被修改时删除标签
  456. // 仍然是为了适配私信界面
  457. let observer = new MutationObserver(() => {
  458. observer.disconnect();
  459. new_element.remove();
  460. });
  461. observer.observe(link, {
  462. childList: true,
  463. characterData: true,
  464. subtree: true,
  465. attributes: true,
  466. });
  467. // 在缓存中保存颜色信息
  468. if (!cache.has(this.id.uid))
  469. cache.set(this.id.uid, new Map());
  470. cache.get(this.id.uid)?.set("color", colorHex);
  471. saveCache();
  472. }
  473. }
  474. dump() {
  475. return { uid: this.id.uid, tag: this.tag };
  476. }
  477. }
  478. let tags = new Map();
  479. /**
  480. * 从 localStorage 加载/存储数据
  481. */
  482. const StorageKeyName = Prefix + "alias_tag_data";
  483. const StorageCacheKeyName = Prefix + "alias_tag_cache";
  484. function load() {
  485. let json = localStorage.getItem(StorageKeyName);
  486. if (json !== null) {
  487. let data = JSON.parse(json);
  488. let _identifiers = data.identifiers;
  489. if (_identifiers instanceof Array) {
  490. for (let x of _identifiers) {
  491. let uid = x.uid;
  492. let username = x.username;
  493. // 判断 uid 为数字,username 为字符串
  494. if (typeof uid === "number" && typeof username === "string") {
  495. let identifier = new UserIdentifier(uid, username);
  496. uidToIdentifier.set(uid, identifier);
  497. usernameToIdentifier.set(username, identifier);
  498. }
  499. }
  500. }
  501. let _aliases = data.aliases;
  502. if (_aliases instanceof Array) {
  503. for (let x of _aliases) {
  504. let uid = x.uid;
  505. let newName = x.newName;
  506. if (typeof uid === "number" && typeof newName === "string") {
  507. let identifier = uidToIdentifier.get(uid);
  508. if (identifier !== undefined) {
  509. aliases.set(identifier, new UsernameAlias(identifier, newName));
  510. }
  511. }
  512. }
  513. }
  514. let _tags = data.tags;
  515. if (_tags instanceof Array) {
  516. for (let x of _tags) {
  517. let uid = x.uid;
  518. let tag = x.tag;
  519. if (typeof uid === "number" && typeof tag === "string") {
  520. let identifier = uidToIdentifier.get(uid);
  521. if (identifier !== undefined) {
  522. tags.set(identifier, new UserTag(identifier, tag));
  523. }
  524. }
  525. }
  526. }
  527. }
  528. let json_cache = localStorage.getItem(StorageCacheKeyName);
  529. if (json_cache !== null) {
  530. let _cache = JSON.parse(json_cache);
  531. if (_cache instanceof Array) {
  532. for (let item of _cache) {
  533. if (item instanceof Array && item.length === 2) {
  534. let [uid, data] = item;
  535. if (typeof uid === "number" && typeof data === "object") {
  536. let data_map = new Map();
  537. for (let [key, value] of Object.entries(data)) {
  538. if (typeof key === "string") {
  539. data_map.set(key, value);
  540. }
  541. }
  542. cache.set(uid, data_map);
  543. }
  544. }
  545. }
  546. }
  547. }
  548. }
  549. function save() {
  550. let data = {
  551. identifiers: Array.from(uidToIdentifier.values()).map((x) => x.dump()),
  552. aliases: Array.from(aliases.values()).map((x) => x.dump()),
  553. tags: Array.from(tags.values()).map((x) => x.dump()),
  554. };
  555. let json = JSON.stringify(data);
  556. localStorage.setItem(StorageKeyName, json);
  557. }
  558. function saveCache() {
  559. let cache_data = Array.from(cache.entries()).map(([uid, data]) => [
  560. uid,
  561. Object.fromEntries(data.entries()),
  562. ]);
  563. let json_cache = JSON.stringify(cache_data);
  564. localStorage.setItem(StorageCacheKeyName, json_cache);
  565. }
  566. (function () {
  567. "use strict";
  568. load();
  569. //
  570. // Your code here...
  571. // “添加别名”设置块
  572. let alias_box = new SettingBox("添加别名");
  573. alias_box.items.push(new SettingBoxItemText("UID/用户名"));
  574. alias_box.items.push(new SettingBoxItemText("别名"));
  575. alias_box.handle((arr) => {
  576. let uid_or_name = arr[0].getValue();
  577. let alias = arr[1].getValue();
  578. console.log(`${uid_or_name} -> ${alias}?`);
  579. UserIdentifier.fromAny(uid_or_name).then((identifier) => {
  580. console.log(`${identifier.uid} ${identifier.username} -> ${alias}`);
  581. aliases.set(identifier, new UsernameAlias(identifier, alias));
  582. alert(`为 ${identifier.username} (${identifier.uid}) 添加别名 ${alias}`);
  583. save();
  584. run();
  585. });
  586. });
  587. // “添加标签”设置块
  588. let tag_box = new SettingBox("添加标签");
  589. tag_box.items.push(new SettingBoxItemText("UID/用户名"));
  590. tag_box.items.push(new SettingBoxItemText("标签"));
  591. tag_box.handle((arr) => {
  592. let uid_or_name = arr[0].getValue();
  593. let tag = arr[1].getValue();
  594. UserIdentifier.fromAny(uid_or_name).then((identifier) => {
  595. console.log(`${identifier.uid} ${identifier.username} -> tag ${tag}`);
  596. tags.set(identifier, new UserTag(identifier, tag));
  597. alert(`为 ${identifier.username} (${identifier.uid}) 添加标签 ${tag}`);
  598. save();
  599. run();
  600. });
  601. save();
  602. });
  603. // “还原用户”设置块
  604. let restore_box = new SettingBox("还原用户");
  605. restore_box.items.push(new SettingBoxItemText("UID/用户名"));
  606. restore_box.handle((arr) => {
  607. let uid_or_name = arr[0].getValue();
  608. UserIdentifier.fromAny(uid_or_name).then((identifier) => {
  609. let deleted_item = [];
  610. if (aliases.has(identifier)) {
  611. aliases.delete(identifier);
  612. deleted_item.push("别名");
  613. }
  614. if (tags.has(identifier)) {
  615. tags.delete(identifier);
  616. deleted_item.push("标签");
  617. }
  618. if (deleted_item.length > 0) {
  619. alert(`已删除 ${identifier.username} (${identifier.uid}) ${deleted_item.join("和")}(刷新网页生效)`);
  620. }
  621. save();
  622. });
  623. });
  624. console.log("Luogu Alias And Customize Tags");
  625. // let prev_time = Date.now();
  626. function run() {
  627. // if (Date.now() - prev_time < Cooldown) return;
  628. try {
  629. restore_box.place();
  630. tag_box.place();
  631. alias_box.place();
  632. }
  633. catch (_) { }
  634. for (let [_, alias] of aliases) {
  635. alias.apply();
  636. }
  637. for (let [_, tag] of tags) {
  638. tag.apply();
  639. }
  640. }
  641. window.onload = () => {
  642. // 创建 boxes-parent
  643. function create_boxes_parent() {
  644. let boxes_grand_parent = document.querySelectorAll(".am-g .am-u-lg-3");
  645. if (boxes_grand_parent.length !== 1)
  646. throw "cannot place boxes-parent";
  647. let boxes_parent = document.createElement("div");
  648. boxes_parent.id = Prefix + "boxes-parent";
  649. boxes_grand_parent[0].insertBefore(boxes_parent, boxes_grand_parent[0].firstChild);
  650. }
  651. try {
  652. create_boxes_parent();
  653. }
  654. catch (err) {
  655. console.log("create_boxes_parent: ", err);
  656. }
  657. // 加入 style 标签
  658. let new_style = document.createElement("style");
  659. new_style.innerHTML = `
  660. span.${Prefix}customized-tag {
  661. display: inline-block;
  662. color: #fff;
  663. padding: 0.25em 0.625em;
  664. font-size: min(0.8em, 1.3rem);
  665. font-weight: 800;
  666. /* margin-left: 0.3em; */
  667. border-radius: 2px;
  668. }`;
  669. // console.log(new_style);
  670. new_style.id = Prefix + "customized-tags-style";
  671. document.head.appendChild(new_style);
  672. /*
  673. const observer = new MutationObserver(run);
  674. observer.observe(document.body, {
  675. childList: true,
  676. subtree: true,
  677. characterData: true,
  678. attributes: true,
  679. });
  680. setTimeout(run, Cooldown);
  681. */
  682. setInterval(run, Cooldown);
  683. };
  684. })();

QingJ © 2025

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