USTC 助手

为 USTC 学生定制的各类实用功能:验证码识别,自动登录,睿客网性能优化以及更多。

安装此脚本
作者推荐脚本

您可能也喜欢评课社区反屏蔽

安装此脚本
  1. // ==UserScript==
  2. // @name USTC Helper
  3. // @name:zh-CN USTC 助手
  4. // @license gpl-3.0
  5. // @namespace http://tampermonkey.net/
  6. // @version 1.3.9
  7. // @description Various useful functions for USTC students: verification code recognition, auto login, rec performance improvement and more.
  8. // @description:zh-CN 为 USTC 学生定制的各类实用功能:验证码识别,自动登录,睿客网性能优化以及更多。
  9. // @author PRO
  10. // @match https://mail.ustc.edu.cn/*
  11. // @match https://id.ustc.edu.cn/*
  12. // @match https://rec.ustc.edu.cn/*
  13. // @match https://recapi.ustc.edu.cn/identity/other_login?*
  14. // @match https://www.bb.ustc.edu.cn/*
  15. // @match https://jw.ustc.edu.cn/*
  16. // @match https://young.ustc.edu.cn/login*
  17. // @match https://young.ustc.edu.cn/nginx_auth/*
  18. // @match https://wvpn.ustc.edu.cn/*
  19. // @match https://icourse.club/*
  20. // @icon https://id.ustc.edu.cn/gate/linkid/api/image/download/login_favicon.png
  21. // @grant unsafeWindow
  22. // @grant GM_setValue
  23. // @grant GM_getValue
  24. // @grant GM_deleteValue
  25. // @grant GM_registerMenuCommand
  26. // @grant GM_unregisterMenuCommand
  27. // @grant GM_addValueChangeListener
  28. // @require https://github.com/PRO-2684/GM_config/releases/download/v1.2.1/config.min.js#md5=525526b8f0b6b8606cedf08c651163c2
  29. // ==/UserScript==
  30.  
  31. (function () {
  32. 'use strict';
  33. const window = unsafeWindow;
  34. const log = console.log.bind(console, "[USTC Helper]");
  35. const configDesc = {
  36. $default: {
  37. autoClose: false,
  38. },
  39. id: {
  40. name: "Unified Authentication",
  41. type: "folder",
  42. items: {
  43. enabled: { name: "Enabled", title: "Whether to enable USTC Helper for Unified Authentication", type: "bool", value: true },
  44. }
  45. },
  46. mail: {
  47. name: "USTC Mail",
  48. type: "folder",
  49. items: {
  50. enabled: { name: "Enabled", title: "Whether to enable USTC Helper for USTC Mail", type: "bool", value: true },
  51. focus: { name: "Focus", title: "Automatically focuses on login button", type: "bool", value: true },
  52. remove_watermark: { name: "Remove watermark", title: "Remove the annoying watermark", type: "bool", value: true },
  53. remove_background: { name: "Remove background", title: "Remove the background image", type: "bool", value: true },
  54. }
  55. },
  56. rec: {
  57. name: "Rec",
  58. type: "folder",
  59. items: {
  60. enabled: { name: "Enabled", title: "Whether to enable USTC Helper for Rec", type: "bool", value: true },
  61. autologin: { name: "Auto login", title: "Automatically clicks login button", type: "bool", value: true },
  62. opencurrent: { name: "Open in current tab", title: "Set some links to be opened in current tab (Significantly improves performance)", type: "bool", value: true },
  63. }
  64. },
  65. bb: {
  66. name: "BB System",
  67. type: "folder",
  68. items: {
  69. enabled: { name: "Enabled", title: "Whether to enable USTC Helper for BB System", type: "bool", value: true },
  70. autoauth: { name: "Auto authenticate", title: "Automatically authenticate when accessing outside school net", type: "bool", value: true },
  71. autologin: { name: "Auto login", title: "Automatically clicks login button", type: "bool", value: true },
  72. showhwstatus: { name: "Show homework status", title: "Query all homework status (may consume some network traffic)", type: "bool", value: true },
  73. }
  74. },
  75. jw: {
  76. name: "Education Administration System",
  77. type: "folder",
  78. items: {
  79. enabled: { name: "Enabled", title: "Whether to enable USTC Helper for Education Administration System", type: "bool", value: true },
  80. login: {
  81. name: "Login",
  82. type: "enum",
  83. value: 1,
  84. options: ["none", "focus", "click"],
  85. title: "What to do to the login button"
  86. },
  87. shortcut: { name: "Shortcut", title: "Enable shortcut support", type: "bool", value: true },
  88. score_mask: { name: "Score mask", title: "Allows you to hide/reveal your scores with dblclick", type: "bool", value: true },
  89. detailed_time: { name: "Detailed time", title: "Show start/end time of each class", type: "bool", value: true },
  90. css: { name: "CSS improve", title: "Minor CSS improvements", type: "bool", value: true },
  91. privacy: { name: "Privacy", title: "Hides your personal information", type: "bool" },
  92. sum: { name: "Sum", title: "Show the sum of credit and period at course table", type: "bool", value: true },
  93. }
  94. },
  95. young: {
  96. name: "Second Classroom",
  97. type: "folder",
  98. items: {
  99. enabled: { name: "Enabled", title: "Whether to enable USTC Helper for Second Classroom", type: "bool", value: true },
  100. auto_auth: { name: "Auto authenticate", title: "Automatically authenticate when accessing outside school net", type: "bool", value: true },
  101. default_tab: {
  102. name: "Default tab",
  103. value: "/myproject/SignUp",
  104. title: "The tab to be opened on entering"
  105. },
  106. auto_tab: { name: "Auto tab", title: "Auto navigate to frequently-used submenu", type: "bool", value: true },
  107. no_datascreen: { name: "No data screen", title: "Remove annoying data screen image", type: "bool", value: true },
  108. shortcut: { name: "Shortcut", title: "Enable shortcut support", type: "bool", value: true }
  109. }
  110. },
  111. wvpn: {
  112. name: "Web VPN",
  113. type: "folder",
  114. items: {
  115. enabled: { name: "Enabled", title: "Whether to enable USTC Helper for Web VPN", type: "bool", value: true },
  116. custom_collection: { name: "Custom collection", title: "Allows you to fully customize your collection", type: "bool", value: true },
  117. }
  118. },
  119. icourse: {
  120. name: "Icourse",
  121. type: "folder",
  122. items: {
  123. enabled: { name: "Enabled", title: "Whether to enable USTC Helper for Icourse", type: "bool", value: true },
  124. filelist: { name: "File list", title: "Show all uploaded files and name them properly", type: "bool", value: true },
  125. linklist: { name: "Link list", title: "Show all links posted in the review section", type: "bool", value: true },
  126. css: { name: "CSS improve", title: "Minor CSS improvements", type: "bool", value: true },
  127. native_top: { name: "Native top", title: "Use native method to scroll to top", type: "bool", value: true },
  128. shortcut: { name: "Shortcut", title: "Enable shortcut support", type: "bool", value: true },
  129. }
  130. }
  131. };
  132.  
  133. const $ = document.querySelector.bind(document);
  134. const $$ = document.querySelectorAll.bind(document);
  135. async function timer(callback, interval = 500, times = 16) {
  136. return new Promise((resolve, reject) => {
  137. const timer = window.setInterval(() => {
  138. if (times-- === 0) {
  139. window.clearInterval(timer);
  140. resolve(false);
  141. } else if (callback()) {
  142. window.clearInterval(timer);
  143. resolve(true);
  144. }
  145. }, interval);
  146. });
  147. }
  148. function setupDynamicStyles(host, config, styles) {
  149. function injectCSS(name) {
  150. const css = document.head.appendChild(document.createElement("style"));
  151. css.id = `ustc-helper-${host}-${name}`;
  152. css.textContent = styles[name];
  153. }
  154. function toggleCSS(name, enabled) {
  155. const css = $(`#ustc-helper-${host}-${name}`);
  156. if (css) {
  157. css.disabled = !enabled;
  158. } else if (enabled) {
  159. injectCSS(name);
  160. }
  161. }
  162. for (const name in styles) {
  163. toggleCSS(name, config.proxy[`${host}.${name}`]);
  164. }
  165. config.addEventListener("set", e => {
  166. if (e.detail.prop.startsWith(`${host}.`)) {
  167. const name = e.detail.prop.split(".")[1];
  168. if (name in styles) {
  169. toggleCSS(name, e.detail.after);
  170. }
  171. }
  172. });
  173. }
  174. /**
  175. * Setup shortcuts for switching tabs and closing tabs
  176. * @param {Element} el The element to receive scroll wheel events
  177. * @param {Object} actions The actions for switching & closing tabs
  178. * @param {Function} actions.select The function to switch to a tab at given index, starting from 0
  179. * @param {Function} actions.close The function to close a tab at given index, starting from 0
  180. * @param {Function} actions.count The funtion to determine total number of tabs
  181. * @param {Function} actions.current The funtion to determine current index of the tab
  182. * @param {Function} [actions.special] The funtion to handle key ``` ` ```
  183. */
  184. function setupShortcuts(el, actions) {
  185. function delta(n) {
  186. const count = actions.count();
  187. const current = actions.current();
  188. actions.select((current + n + count) % count);
  189. }
  190. document.addEventListener("keydown", (e) => {
  191. const active = document.activeElement;
  192. if (active.nodeName === "INPUT" || active.nodeName === "TEXTAREA") return;
  193. const count = actions.count();
  194. const current = actions.current();
  195. switch (e.key) {
  196. case "ArrowLeft":
  197. delta(-1);
  198. break;
  199. case "ArrowRight":
  200. delta(1);
  201. break;
  202. case "x":
  203. actions.close(current);
  204. break;
  205. case "`":
  206. actions?.special?.(); // Optional
  207. default:
  208. if (e.key.length == 1) {
  209. const idx = Number(e.key);
  210. if (!isNaN(idx) && 0 < idx && idx <= count) {
  211. actions.select(idx - 1);
  212. }
  213. }
  214. break;
  215. }
  216. });
  217. setupScroll(el, delta);
  218. }
  219. /**
  220. * Setup shortcuts for scroll wheel
  221. * @param {Element} el The element to be scrolled
  222. * @param {Function} delta The delta function
  223. */
  224. function setupScroll(el, delta) {
  225. el.addEventListener("wheel", (e) => {
  226. e.preventDefault();
  227. if (e.deltaY < 0) {
  228. delta(-1);
  229. } else if (e.deltaY > 0) {
  230. delta(1);
  231. }
  232. });
  233. }
  234.  
  235. const config = new GM_config(configDesc);
  236. switch (window.location.host) {
  237. case 'mail.ustc.edu.cn': {
  238. config.down("mail");
  239. if (!config.get("mail.enabled")) {
  240. console.info("[USTC Helper] 'mail' feature disabled.");
  241. break;
  242. }
  243. if (config.get("mail.focus")) {
  244. timer(() => {
  245. const btn = $(".formLogin .submit");
  246. if (btn) {
  247. btn.focus();
  248. return true;
  249. } else {
  250. return false;
  251. }
  252. }).then((result) => {
  253. console.info(result ? "[USTC Helper] Login button focused." : "[USTC Helper] Login button not found!");
  254. });
  255. }
  256. const mail_css = {
  257. "remove_watermark": "div.watermark-wrap { display: none; }",
  258. "remove_background": ".lymain .lybg { display: none; }"
  259. }
  260. setupDynamicStyles("mail", config, mail_css);
  261. break;
  262. }
  263. case 'id.ustc.edu.cn': {
  264. config.down("id");
  265. break;
  266. }
  267. case 'rec.ustc.edu.cn': {
  268. config.down("rec");
  269. if (!config.get("rec.enabled")) {
  270. console.info("[USTC Helper] 'rec' feature disabled.");
  271. break;
  272. }
  273. if (config.get("rec.opencurrent")) {
  274. window.webpackJsonp.push_ = window.webpackJsonp.push;
  275. window.webpackJsonp.push = (val) => {
  276. if (val[0][0] !== "chunk-5ae262a1")
  277. return window.webpackJsonp.push_(val);
  278. else { // Following script is adapted from https://rec.ustc.edu.cn/js/chunk-5ae262a1.b84e1461.js
  279. val[1]["2c03"] = function (t, e, s) {
  280. "use strict";
  281. (function (t) {
  282. s("55dd");
  283. var r = s("a67e");
  284. e["a"] = {
  285. name: "GroupLister",
  286. components: {
  287. GroupCreate: function () {
  288. return Promise.all([s.e("chunk-390136ce"), s.e("chunk-662e27b9")]).then(s.bind(null, "18fa"))
  289. },
  290. GroupAdd: function () {
  291. return s.e("chunk-5b916374").then(s.bind(null, "c1c7"))
  292. },
  293. GroupEdit: function () {
  294. return Promise.all([s.e("chunk-390136ce"), s.e("chunk-0daeb591")]).then(s.bind(null, "1fa6"))
  295. }
  296. },
  297. data: function () {
  298. return {
  299. status: {
  300. GroupCreateStatus: !1,
  301. GroupAddStatus: !1,
  302. GroupEditStatus: !1
  303. },
  304. loading: !1,
  305. nothing: !1,
  306. group: {},
  307. sortBy: {},
  308. headers: [{
  309. id: 1,
  310. title: "群名称",
  311. class: "groupname",
  312. sort: "asc",
  313. showSort: !0,
  314. field: "group_name"
  315. }, {
  316. id: 2,
  317. title: "群号",
  318. class: "groupid",
  319. sort: "des",
  320. showSort: !1,
  321. field: "group_number"
  322. }, {
  323. id: 3,
  324. title: "成员",
  325. class: "groupuser",
  326. sort: "des",
  327. showSort: !1,
  328. field: "group_memeber_count"
  329. }, {
  330. id: 5,
  331. title: "分享",
  332. class: "groupshare",
  333. sort: "des",
  334. showSort: !1,
  335. field: "group_share_file_count"
  336. }, {
  337. id: 6,
  338. title: "操作",
  339. class: "groupmenu",
  340. sort: "",
  341. showSort: !1
  342. }]
  343. }
  344. },
  345. created: function () {
  346. this.sortBy = this.headers[0],
  347. this.getGroups()
  348. },
  349. computed: {
  350. userInfo: function () {
  351. return this.$store.state.user.userInfo
  352. }
  353. },
  354. watch: {
  355. $route: function () {
  356. this.getGroups()
  357. }
  358. },
  359. filters: {
  360. identityNameFilter: function (t) {
  361. var e;
  362. switch (t) {
  363. case "owner":
  364. e = "群主";
  365. break;
  366. case "admin":
  367. e = "管理员";
  368. break;
  369. case "user":
  370. e = "成员";
  371. break;
  372. default:
  373. break
  374. }
  375. return e
  376. }
  377. },
  378. methods: {
  379. createGroup: function () {
  380. t("#newgroup").modal("show")
  381. },
  382. addGroup: function () {
  383. t("#addgroup").modal("show")
  384. },
  385. invite: function (t) {
  386. var e = this.$router.resolve({
  387. name: "group",
  388. params: {
  389. groupNumber: t.group_number
  390. }
  391. });
  392. this.$confirm({
  393. showYesBtn: !1,
  394. showCopyBtn: !0,
  395. copyBtnText: "复制文字",
  396. title: "邀请入群",
  397. type: "confirm",
  398. content: "打开链接进入群组主页即可申请加入群组:".concat(t.group_name, ",群组主页链接:").concat(window.location.origin).concat(e.href)
  399. }).then((function () { }
  400. )).catch((function () { }
  401. ))
  402. },
  403. goToGroupCloud: function (t, e) {
  404. if (["owner", "admin", "user"].indexOf(t.group_member_identity) < 0)
  405. return this.$message({
  406. type: "warning",
  407. message: "您不是组群成员,无法进入群盘"
  408. }),
  409. !1;
  410. this.$store.commit("setSetting", {
  411. from: !0,
  412. drive: "groupdisk",
  413. tab: e,
  414. group: t
  415. }),
  416. this.$router.push({
  417. name: "groupDisk",
  418. params: {
  419. groupNumber: t.group_number
  420. }
  421. })
  422. },
  423. isShowMenu: function (t) {
  424. return ["owner", "admin", "user"].indexOf(t.group_member_identity) > -1
  425. },
  426. isEditGroup: function (t) {
  427. return ["owner", "admin"].indexOf(t.group_member_identity) > -1
  428. },
  429. goToGroup: function (t) {
  430. var e = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : "group";
  431. if ("wait" === t.group_is_review)
  432. return this.$message({
  433. type: "warning",
  434. message: "群组待审核,不允许操作!"
  435. }),
  436. !1;
  437. if ("refuse" === t.group_is_review)
  438. return this.$message({
  439. type: "warning",
  440. message: "群组审核未通过,不允许操作!"
  441. }),
  442. !1;
  443. // Instead of opening in new tab, we prefer to use vue's solution
  444. // Modifiy start
  445. this.$router.replace({
  446. name: e,
  447. params: {
  448. groupNumber: t.group_number
  449. }
  450. });
  451. // Modify end
  452. },
  453. goToGroupHome: function (t) {
  454. this.$store.commit("SET_GROUP_SHOWDESC", !1),
  455. this.$router.push({
  456. name: "group",
  457. params: {
  458. groupNumber: t
  459. }
  460. })
  461. },
  462. handleEditGroup: function (e) {
  463. var s = this;
  464. Object(r["g"])(e.group_number).then((function (t) {
  465. s.group = t.entity
  466. }
  467. )).catch((function (t) {
  468. s.$message({
  469. type: "error",
  470. message: t
  471. })
  472. }
  473. )),
  474. t("#editgroup").modal("show")
  475. },
  476. groupRefresh: function () {
  477. this.getGroups()
  478. },
  479. sortGroup: function (t) {
  480. if (6 === t)
  481. return !1;
  482. var e = this;
  483. this.headers.map((function (s) {
  484. return s.id === t ? (s.showSort = !0,
  485. s.sort = "des" === s.sort ? "asc" : "des",
  486. e.sortBy = s,
  487. s) : (s.showSort = !1,
  488. s.sort = "des",
  489. s)
  490. }
  491. )),
  492. this.sortGroupBy()
  493. },
  494. getGroups: function () {
  495. var t = this;
  496. this.groups = [],
  497. this.loading = !0,
  498. this.nothing = !1,
  499. Object(r["r"])({}).then((function (e) {
  500. if (200 === e.status_code)
  501. if (t.loading = !1,
  502. t.groups = e.entity.datas,
  503. e.entity.total > 0) {
  504. var s = 0;
  505. e.entity.datas.map((function (t) {
  506. "user" != t.group_member_identity && t.group_pending_member_count > 0 && (s += t.group_pending_member_count)
  507. }
  508. )),
  509. t.$store.commit("setRequestNums", s),
  510. t.sortGroupBy(!0)
  511. } else
  512. t.nothing = !0;
  513. else
  514. t.$message({
  515. type: "error",
  516. message: e.message
  517. })
  518. }
  519. )).catch((function (e) {
  520. t.$message({
  521. type: "error",
  522. message: e
  523. })
  524. }
  525. ))
  526. },
  527. sortGroupBy: function () {
  528. var t = this
  529. , e = arguments.length > 0 && void 0 !== arguments[0] && arguments[0];
  530. this.groups.sort((function (s, r) {
  531. var o;
  532. return o = e ? r.group_is_review.localeCompare(s.group_is_review) : "group_name" === t.sortBy.field ? s[t.sortBy.field].localeCompare(r[t.sortBy.field]) : s[t.sortBy.field] - r[t.sortBy.field],
  533. o = "asc" === t.sortBy.sort ? o : -o,
  534. o
  535. }
  536. ))
  537. },
  538. groupCancel: function (t) {
  539. var e = this
  540. , s = "adopt" === t.group_is_review ? "解散" : "删除";
  541. this.$confirm({
  542. type: "confirm",
  543. content: "".concat(s, "群后,所有关于本群组的信息都将被删除且无法恢复,确定").concat(s, "【").concat(t.group_name, "】吗?"),
  544. showCancleBtn: !0,
  545. showYesBtn: !0,
  546. custom: []
  547. }).then((function () {
  548. Object(r["u"])({
  549. groups_list: [t.group_number]
  550. }).then((function (t) {
  551. 200 === t.status_code ? (e.$message({
  552. type: "success",
  553. message: t.message
  554. }),
  555. e.getGroups()) : e.$message({
  556. type: "error",
  557. message: t.message
  558. })
  559. }
  560. )).catch((function (t) {
  561. e.$message({
  562. type: "error",
  563. message: t
  564. })
  565. }
  566. ))
  567. }
  568. )).catch((function () { }
  569. ))
  570. },
  571. groupQuit: function (t) {
  572. var e = this;
  573. this.$confirm({
  574. type: "confirm",
  575. content: "确定退出该群组吗?",
  576. showCancleBtn: !0,
  577. showYesBtn: !0,
  578. custom: []
  579. }).then((function () {
  580. Object(r["v"])({
  581. group_number: t,
  582. action: "quit",
  583. members_list: [e.userInfo.user_number]
  584. }).then((function (t) {
  585. 200 === t.status_code ? (e.$message({
  586. type: "success",
  587. message: t.message
  588. }),
  589. e.getGroups()) : e.$message({
  590. type: "error",
  591. message: t.message
  592. })
  593. }
  594. )).catch((function (t) {
  595. e.$message({
  596. type: "error",
  597. message: t
  598. })
  599. }
  600. ))
  601. }
  602. )).catch((function () { }
  603. ))
  604. }
  605. },
  606. mounted: function () {
  607. var t = this;
  608. setTimeout((function () {
  609. for (var e in t.status)
  610. t.status[e] = !0
  611. }
  612. ), 500)
  613. }
  614. }
  615. }
  616. ).call(this, s("1157"))
  617. };
  618. return window.webpackJsonp.push_(val);
  619. }
  620. };
  621. }
  622. if (config.get("rec.autologin") && document.location.pathname == '/') {
  623. const app = $("#app");
  624. const options = {
  625. childList: true,
  626. attributes: false,
  627. subtree: true
  628. }
  629. const observer = new MutationObserver(() => {
  630. const btn = $('.navbar-login-btn');
  631. if (btn) {
  632. btn.click();
  633. observer.disconnect();
  634. }
  635. });
  636. observer.observe(app, options);
  637. } else if (config.get("rec.opencurrent")) {
  638. const app = $("#app");
  639. const options = {
  640. childList: true,
  641. attributes: false,
  642. subtree: true
  643. }
  644. const observer = new MutationObserver(() => {
  645. const l = $$(".app-list").length;
  646. if (l) {
  647. const links = $$("a");
  648. for (const link of links) {
  649. if (link.target == '_blank') link.removeAttribute("target");
  650. }
  651. }
  652. });
  653. observer.observe(app, options);
  654. }
  655. break;
  656. }
  657. case 'recapi.ustc.edu.cn': {
  658. config.down("rec");
  659. if (!config.get("rec.enabled")) {
  660. console.info("[USTC Helper] 'rec' feature disabled.");
  661. break;
  662. }
  663. if (config.get("rec.autologin")) {
  664. const btn = $("#ltwo > div > button");
  665. if (!btn) {
  666. console.error("[USTC Helper] Login button not found!");
  667. } else {
  668. btn.click();
  669. }
  670. }
  671. break;
  672. }
  673. case 'www.bb.ustc.edu.cn': {
  674. config.down("bb");
  675. if (!config.get("bb.enabled")) {
  676. console.info("[USTC Helper] 'bb' feature disabled.");
  677. break;
  678. }
  679. if (window.location.pathname == '/nginx_auth/' && config.get("bb.autoauth")) {
  680. $('a')?.click();
  681. } else if ((window.location.pathname == '/' || window.location.pathname == '/webapps/login/') && config.get("bb.autologin")) {
  682. $('#login > table > tbody > tr > td:nth-child(2) > span > a')?.click();
  683. } else if (config.get("bb.showhwstatus") && window.location.pathname == '/webapps/blackboard/content/listContent.jsp' && document.getElementById('pageTitleText').children[0].textContent == '作业区') {
  684. const css = document.createElement('style');
  685. css.textContent = ".ustc-helper-bb-ignored { opacity: 0.4; } .ustc-helper-bb-ignored > .details { display: none; }";
  686. document.head.appendChild(css);
  687. const hw_list = document.getElementById('content_listContainer');
  688. const color_config = ['grey', 'green', 'red', 'yellow', 'grey', 'cyan'];
  689. const hint_text = ['查询中', '已提交', '未提交', '查询错误', '已忽略', '已评分'];
  690. // const hint_text = ['Checking', 'Submitted', 'Not submitted', 'Error', 'Ignored', 'Graded'];
  691. function ignore_hw(course_id, content_id, ignore) {
  692. let ignored = localStorage.getItem(course_id) || '[]';
  693. ignored = JSON.parse(ignored);
  694. if (ignore && !ignored.includes(content_id)) {
  695. ignored.push(content_id);
  696. log(`Ignoring "${course_id}/${content_id}"...`);
  697. } else if (!ignore && ignored.includes(content_id)) {
  698. ignored = ignored.filter((v) => v != content_id);
  699. log(`Un-ignoring "${course_id}/${content_id}"...`);
  700. }
  701. if (ignored.length) localStorage.setItem(course_id, JSON.stringify(ignored));
  702. else localStorage.removeItem(course_id);
  703. }
  704. async function query_status(link) {
  705. const r = await fetch(link);
  706. if (!r.ok) {
  707. log(`Failed to fetch "${r.url}": ${r.status} ${r.statusText}`);
  708. return [3, null];
  709. } else {
  710. const parser = new DOMParser();
  711. const html = await r.text();
  712. const doc = parser.parseFromString(html, 'text/html');
  713. const title = doc.getElementById('pageTitleText').textContent.trim();
  714. if (title.startsWith('上载作业')) {
  715. return [2, null];
  716. } else if (title.startsWith('复查提交历史记录')) {
  717. const grade = doc.getElementById("aggregateGrade");
  718. const suffix = doc.getElementById("aggregateGrade_pointsPossible");
  719. if (grade.value !== '-') {
  720. return [5, `${parseFloat(grade.value)}${suffix.textContent.trim()}`];
  721. } else {
  722. return [1, null];
  723. }
  724. } else {
  725. return [3, null];
  726. }
  727. }
  728. }
  729. async function process(hw) {
  730. const link_ = hw.querySelector("h3 > a");
  731. if (!link_) return;
  732. let status = [0, null]; // 0: Checking 1: Uploaded 2: Not uploaded 3: Error
  733. const hint = link_.appendChild(document.createElement('span'));
  734. hint.style.color = color_config[status[0]];
  735. hint.textContent = `(${hint_text[status[0]]})`;
  736. const link = link_.href;
  737. // https://www.bb.ustc.edu.cn/webapps/assignment/uploadAssignment?content_id=_106763_1&course_id=_12559_1&group_id=&mode=view
  738. const params = new URL(link).searchParams;
  739. const course_id = params.get("course_id");
  740. const content_id = params.get("content_id");
  741. let ignored = localStorage.getItem(course_id);
  742. // Check if this homework is ignored
  743. if (ignored) {
  744. ignored = JSON.parse(ignored).includes(content_id);
  745. if (ignored) {
  746. status[0] = 4;
  747. // link_.parentNode.parentNode.parentNode.style.opacity = 0.4;
  748. link_.parentNode.parentNode.parentNode.classList.add("ustc-helper-bb-ignored");
  749. log(`"${course_id}/${content_id}" present in ignore list, so this homework is ignored.`);
  750. }
  751. }
  752. // Not in cache
  753. if (!status[0]) {
  754. status = await query_status(link);
  755. if (status[0] == 1) {
  756. log(`Online query indicated that "${course_id}/${content_id}" is uploaded.`);
  757. } else if (status[0] == 2) {
  758. log(`Online query indicated that "${course_id}/${content_id}" is not uploaded.`);
  759. } else if (status[0] == 5) {
  760. log(`Online query indicated that "${course_id}/${content_id}" is graded.`);
  761. } else {
  762. console.warn(`[USTC Helper] Online query "${course_id}/${content_id}" failed!`);
  763. }
  764. }
  765. hint.style.color = color_config[status[0]];
  766. hint.textContent = `(${hint_text[status[0]]}${status[1] ? " " + status[1] : ""})`;
  767. hint.title = ignored ? "点击取消忽略此作业" : "点击忽略此作业";
  768. hint.addEventListener('click', e => {
  769. e.preventDefault();
  770. ignore_hw(course_id, content_id, !ignored);
  771. hint.title = "刷新页面以生效";
  772. hint.style.color = color_config[4];
  773. hint.textContent = "(请刷新)";
  774. }, { once: true });
  775. }
  776. for (const hw of hw_list.children) {
  777. process(hw);
  778. }
  779. }
  780. break;
  781. }
  782. case 'jw.ustc.edu.cn': {
  783. config.down("jw");
  784. if (!config.get("jw.enabled")) {
  785. console.info("[USTC Helper] 'jw' feature disabled.");
  786. break;
  787. }
  788. if (config.get("jw.login") && window.location.pathname == "/login") {
  789. const btn = document.getElementById('login-unified-wrapper');
  790. if (config.get("jw.login") === 1) {
  791. btn.focus();
  792. } else if (config.get("jw.login") === 2) {
  793. btn.click();
  794. } else if (config.get("jw.login") !== 0) {
  795. console.error(`[USTC Helper] Unknown option for jw.login: ${config.get("jw.login")}`);
  796. }
  797. }
  798. if (config.get("jw.shortcut")) {
  799. if (window.location.pathname == "/home") { // Top frame
  800. timer(() => {
  801. const tabList = $("#e-home-tab-list");
  802. if (!tabList) return false;
  803. const tabs = tabList?.children;
  804. const home = $("#e-top-home-page > li > a > div.home-logo");
  805. const header = tabList.parentElement;
  806. const actions = {
  807. select: (index) => tabs[index]?.querySelector("span.tabTitle")?.click(),
  808. close: (index) => tabs[index]?.querySelector("a > i.fa-times")?.click(),
  809. count: () => tabs.length,
  810. current: () => {
  811. for (let i = 0; i < tabs.length; i++) {
  812. if (tabs[i].classList.contains('active')) return i;
  813. }
  814. return -1;
  815. },
  816. special: () => home?.click()
  817. };
  818. setupShortcuts(header, actions);
  819. return true;
  820. }).then((success) => {
  821. log(success ? "Shortcuts have been setup." : "Failed to setup shortcuts.");
  822. });
  823. const list = $("#primaryCarousel > .carousel-inner");
  824. if (list) {
  825. const left = $("#primaryCarousel > a.left[data-slide='prev']");
  826. const right = $("#primaryCarousel > a.right[data-slide='next']");
  827. setupScroll(list, (delta) => {
  828. if (delta < 0) left.click();
  829. else right.click();
  830. });
  831. }
  832. } else { // Bubble key events up
  833. document.addEventListener("keydown", (e) => {
  834. const active = document.activeElement;
  835. if (active.nodeName === "INPUT" || active.nodeName === "TEXTAREA") return;
  836. window.parent.document.dispatchEvent(new KeyboardEvent(e.type, e));
  837. e.stopPropagation();
  838. });
  839. }
  840. }
  841. if (config.get("jw.score_mask") && window.location.pathname == "/for-std/grade/sheet") {
  842. function get_status(entry) {
  843. // Status:
  844. // false: Normal display
  845. // true: Masked
  846. if (entry.classList.contains("masked")) return true;
  847. else return false;
  848. }
  849. function set_status_internal(entry, state) {
  850. const gpa = entry.children[entry.children.length - 2];
  851. const score = entry.lastChild;
  852. if (state) {
  853. entry.classList.add("masked");
  854. entry.setAttribute("data-gpa", gpa.textContent);
  855. entry.setAttribute("data-score", score.textContent);
  856. gpa.textContent = "";
  857. score.textContent = "";
  858. } else {
  859. entry.classList.remove("masked");
  860. let gpa_val = entry.getAttribute("data-gpa");
  861. let score_val = entry.getAttribute("data-score");
  862. if (gpa_val) gpa.textContent = gpa_val;
  863. if (score_val) score.textContent = score_val;
  864. }
  865. }
  866. function toggle() {
  867. set_status_internal(this, !get_status(this));
  868. }
  869. function set_status(entry, state) {
  870. if (get_status(entry) == state) return;
  871. set_status_internal(entry, state);
  872. }
  873. function toggle_view() {
  874. if (this.hasAttribute("data-value")) {
  875. this.lastChild.textContent = this.getAttribute("data-value");
  876. this.removeAttribute("data-value");
  877. } else {
  878. this.setAttribute("data-value", this.lastChild.textContent);
  879. this.lastChild.textContent = "尚未评教";
  880. }
  881. }
  882. function toggle_rank() {
  883. if (this.hasAttribute("data-value")) {
  884. this.textContent = this.getAttribute("data-value");
  885. this.removeAttribute("data-value");
  886. } else {
  887. this.setAttribute("data-value", this.textContent);
  888. this.textContent = "尚未评教";
  889. }
  890. }
  891. function setup() {
  892. const tables = $$("div.semesters > section > div.semester > table");
  893. tables.forEach((table) => {
  894. let head = table.querySelector("thead");
  895. let entries = table.querySelectorAll("tbody > tr");
  896. head.addEventListener("dblclick", (e) => {
  897. let status = head.getAttribute("data-masked") === "";
  898. entries.forEach((entry) => {
  899. set_status(entry, !status);
  900. });
  901. if (status) head.removeAttribute("data-masked");
  902. else head.setAttribute("data-masked", "");
  903. });
  904. entries.forEach((entry) => {
  905. entry.addEventListener("dblclick", toggle);
  906. });
  907. });
  908. const history_table = $("table.history-table");
  909. history_table.tHead.addEventListener("dblclick", (e) => {
  910. history_table.querySelectorAll("tbody:not(.hidden)").forEach((tbody) => {
  911. const status = tbody.getAttribute("data-masked") === "";
  912. tbody.querySelectorAll("tr").forEach((entry) => {
  913. set_status(entry, !status);
  914. });
  915. if (status) tbody.removeAttribute("data-masked");
  916. else tbody.setAttribute("data-masked", "");
  917. });
  918. });
  919. history_table.querySelectorAll("tbody > tr").forEach((entry) => {
  920. entry.addEventListener("dblclick", toggle);
  921. });
  922. const view = $("div.overview > ul");
  923. view.childNodes.forEach((node) => {
  924. node.addEventListener("dblclick", toggle_view);
  925. });
  926. const rank = $("div.rankinfo > div");
  927. rank.querySelectorAll("b").forEach((node) => {
  928. node.addEventListener("dblclick", toggle_rank);
  929. });
  930. }
  931. timer(() => {
  932. const test = $("div.overview > ul > li > span:nth-child(2)");
  933. if (test.textContent != "NaN") {
  934. setup();
  935. return true;
  936. }
  937. return false;
  938. }, 1000, 8).then((success) => {
  939. console.info("[USTC Helper] Score mask setup " + (success ? "succeeded." : "failed."));
  940. });
  941. }
  942. const jw_css = {
  943. "detailed_time" : `table.timetable tbody th.span::before, table.timetable tbody th.span::after { font-size: smaller; position: absolute; left: 0.1em; opacity: 0.3; }
  944. table.timetable tbody th.span::before { content: attr(data-start); top: 0; } table.timetable tbody th.span::after { content: attr(data-end); bottom: 0; }`,
  945. "css": `div#dropdown-menu-filter { display: none; } div#dropdown-menu-bg { backdrop-filter: blur(3px); } div.second-menu-wrap div.menu-area { width: 100%; }
  946. li.home div.dropdown-menu { width: 25vw !important; min-width: 400px !important; } .primary .item li.primaryLi.hover { transition: transform 0.25s ease; }
  947. .primaryLi .subMenus { cursor: initial; opacity: 0.8; } div#shortcut { width: 27em; } .shortcut-panel .shortcut-item { width: 25%; }
  948. .primary-container .primaryLi .subMenus { width: 400px; border-radius: inherit; overflow: auto; } #e-content-area #e-op-area div.e-toolbarTab { padding: 0 !important; }
  949. .dropdown.path-li .path-dropdown.second-menu-wrap.dropdown-menu { width: auto; padding: 10px; } .dropdown.path-li .path-dropdown.second-menu-wrap.dropdown-menu .menu-area { padding: 0; }`,
  950. "privacy": `#accountLoginInfo, #home-page .info-username, body > div.container div.top-bar > h2.info-title, .list-group-item > span:not(.pull-left) { filter: blur(0.2em); }
  951. img[src='/my/avatar'] { filter: blur(1em); }`
  952. };
  953. setupDynamicStyles("jw", config, jw_css);
  954. if (window.location.pathname.startsWith("/for-std/course-table")) {
  955. if (config.get("jw.sum")) {
  956. const table = $("#lessons");
  957. if (table) {
  958. const rows = table.querySelectorAll("tbody > tr");
  959. const indexes = [3, 9];
  960. let sums = [0, 0];
  961. for (const row of rows) {
  962. for (let i = 0; i < indexes.length; i++) {
  963. sums[i] += parseFloat(row.children[indexes[i]].textContent);
  964. }
  965. }
  966. const head = table.querySelector("thead > tr");
  967. for (let i = 0; i < indexes.length; i++) {
  968. head.children[indexes[i]].title = `总计:${sums[i]}`;
  969. }
  970. }
  971. }
  972. }
  973. break;
  974. }
  975. case 'young.ustc.edu.cn': {
  976. config.down("young");
  977. if (!config.get("young.enabled")) {
  978. console.info("[USTC Helper] 'young' feature disabled.");
  979. break;
  980. }
  981. if (window.location.pathname == '/nginx_auth/' && config.get("young.auto_auth")) {
  982. document.getElementsByTagName('a')[0].click();
  983. return;
  984. }
  985. const app = $("#app");
  986. const router = app.__vue__.$router;
  987. function main(mutations, observer) {
  988. const menu = app.querySelector(".ant-menu-root");
  989. if (!menu) return;
  990. const default_tab = config.get("young.default_tab");
  991. if (default_tab.length) router.push(default_tab);
  992. const submenus = menu.querySelectorAll("li.ant-menu-submenu-horizontal:not(.ant-menu-overflowed-submenu) > div");
  993. if (!submenus.length) return;
  994. observer.disconnect();
  995. if (config.get("young.no_datascreen")) {
  996. app.querySelector("div.header-index-wide > a").remove();
  997. function getCloseBtn() {
  998. return app.querySelector("span[pagekey='/dataAnalysis/visual']")?.nextElementSibling;
  999. }
  1000. function close() {
  1001. const tabs = $(".ant-tabs-nav-animated > div").children;
  1002. if (tabs.length == 1) return false;
  1003. const closeBtn = getCloseBtn();
  1004. if (closeBtn) {
  1005. closeBtn.click();
  1006. return !getCloseBtn();
  1007. } else {
  1008. return false;
  1009. }
  1010. }
  1011. timer(() => close()).then((success) => {
  1012. log(success ? "Data screen closed." : "Failed to close data screen.");
  1013. });
  1014. }
  1015. if (config.get("young.auto_tab")) {
  1016. submenus[0].onclick = (e) => {
  1017. router.push('/dataAnalysis/studentAnalysis');
  1018. e.stopImmediatePropagation();
  1019. }
  1020. submenus[1].onclick = (e) => {
  1021. router.push('/personalInformation/personalReport');
  1022. }
  1023. submenus[2].onclick = (e) => {
  1024. router.push('/myproject/SignUp');
  1025. }
  1026. submenus[5].onclick = (e) => {
  1027. router.push('/isystem/departUserList');
  1028. }
  1029. app.querySelector(".user-dropdown-menu").onclick = (e) => {
  1030. $("ul.user-dropdown-menu-wrapper > li:nth-child(7) > a").click();
  1031. }
  1032. // They're generated dynamically when you hover over the menu...
  1033. // const submenuItems = $$(".ant-menu-submenu.ant-menu-submenu-vertical"); // Submenu items with even more submenus
  1034. // log(submenuItems);
  1035. // submenuItems.forEach((submenuItem) => {
  1036. // const subsubmenuItems = submenuItem.querySelectorAll(".ant-menu.ant-menu-vertical.ant-menu-sub.ant-menu-submenu-content > .ant-menu-item");
  1037. // if (subsubmenuItems.length == 1) {
  1038. // const pathName = subsubmenuItems[0].querySelector("a")?.pathname;
  1039. // if (pathName) {
  1040. // submenuItem.onclick = (e) => {
  1041. // router.push(pathName);
  1042. // }
  1043. // }
  1044. // }
  1045. // });
  1046. }
  1047. if (config.get("young.shortcut")) {
  1048. const tabList = $(".ant-tabs-nav-animated > div")
  1049. const tabs = tabList.children;
  1050. const nav = tabList.parentElement.parentElement;
  1051. const actions = {
  1052. select: (index) => {
  1053. tabs[index].click();
  1054. },
  1055. close: (index) => {
  1056. const closeBtn = tabs[index].querySelector("div > i");
  1057. if (closeBtn) closeBtn.click();
  1058. },
  1059. count: () => tabs.length,
  1060. current: () => {
  1061. let current = 0;
  1062. for (const tab of tabs) {
  1063. if (tab.attributes["aria-selected"].value == "true") {
  1064. break;
  1065. }
  1066. current++;
  1067. }
  1068. return current;
  1069. }
  1070. };
  1071. setupShortcuts(nav, actions);
  1072. }
  1073. }
  1074. const options = {
  1075. childList: true,
  1076. attributes: false,
  1077. subtree: true
  1078. }
  1079. const observer = new MutationObserver(main);
  1080. observer.observe(app, options);
  1081. break;
  1082. }
  1083. case 'wvpn.ustc.edu.cn': {
  1084. config.down("wvpn");
  1085. if (!config.get("wvpn.enabled")) {
  1086. console.info("[USTC Helper] 'wvpn' feature disabled.");
  1087. break;
  1088. }
  1089. if (config.get("wvpn.custom_collection")) {
  1090. const options = {
  1091. childList: true,
  1092. attributes: false,
  1093. subtree: true
  1094. }
  1095. const callback = (mutations, observer) => {
  1096. const input = $("input.portal-search__input");
  1097. const ele = $("div#__layout > div.wrd-webvpn");
  1098. if (!input || !input.placeholder || !ele) return;
  1099. const v = ele.__vue__;
  1100. observer.disconnect();
  1101. function fail(s, hint) {
  1102. console.error("[USTC Helper]", s);
  1103. alert(hint);
  1104. }
  1105. function cancel() {
  1106. console.info("[USTC Helper] User calcelled the operation.");
  1107. alert("你终止了收藏操作!😢");
  1108. }
  1109. function invalid() {
  1110. console.warn("[USTC Helper] Invalid input!");
  1111. alert("你输入了一个不合法的值!🤔");
  1112. }
  1113. function setup(aesjs) {
  1114. input.placeholder = "点击五角星或 Ctrl+D 以收藏 🍻";
  1115. // Encryption, adapted from https://blog.csdn.net/lijiext/article/details/110931285
  1116. const utf8 = aesjs.utils.utf8;
  1117. const hex = aesjs.utils.hex;
  1118. const AesCfb = aesjs.ModeOfOperation.cfb;
  1119. const wrdvpnKey = 'wrdvpnisthebest!';
  1120. const wrdvpnIV = 'wrdvpnisthebest!';
  1121. function textRightAppend(text, mode) {
  1122. const segmentByteSize = mode === 'utf8' ? 16 : 32;
  1123. if (!(text.length % segmentByteSize)) {
  1124. return text;
  1125. }
  1126. const appendLength = segmentByteSize - text.length % segmentByteSize;
  1127. for (let i = 0; i < appendLength; i++) {
  1128. text += '0';
  1129. }
  1130. return text;
  1131. }
  1132. function encrypt(text, key, iv) {
  1133. const textLength = text.length;
  1134. text = textRightAppend(text, 'utf8');
  1135. const keyBytes = utf8.toBytes(key);
  1136. const ivBytes = utf8.toBytes(iv);
  1137. const textBytes = utf8.toBytes(text);
  1138. const aesCfb = new AesCfb(keyBytes, ivBytes, 16);
  1139. const encryptBytes = aesCfb.encrypt(textBytes);
  1140. return hex.fromBytes(ivBytes) + hex.fromBytes(encryptBytes).slice(0, textLength * 2);
  1141. }
  1142. function encryptUrl(url) {
  1143. let port = "";
  1144. let segments = "";
  1145. let protocol = "";
  1146.  
  1147. if (url.startsWith("http://")) {
  1148. url = url.substr(7);
  1149. protocol = "http";
  1150. } else if (url.startsWith("https://")) {
  1151. url = url.substr(8);
  1152. protocol = "https";
  1153. } else {
  1154. return "";
  1155. }
  1156. let v6 = "";
  1157. const match = /\[[0-9a-fA-F:]+?\]/.exec(url);
  1158. if (match) {
  1159. v6 = match[0];
  1160. url = url.slice(match[0].length);
  1161. }
  1162. segments = url.split("?")[0].split(":");
  1163. if (segments.length > 1) {
  1164. port = segments[1].split("/")[0]
  1165. url = url.substr(0, segments[0].length) + url.substr(segments[0].length + port.length + 1);
  1166. }
  1167. const i = url.indexOf('/');
  1168. if (i == -1) {
  1169. if (v6 != "") {
  1170. url = v6;
  1171. }
  1172. url = encrypt(url, wrdvpnKey, wrdvpnIV)
  1173. } else {
  1174. const host = url.slice(0, i);
  1175. const path = url.slice(i);
  1176. if (v6 != "") {
  1177. host = v6;
  1178. }
  1179. url = encrypt(host, wrdvpnKey, wrdvpnIV) + path;
  1180. }
  1181. if (port != "") {
  1182. url = "/" + protocol + "-" + port + "/" + url;
  1183. } else {
  1184. url = "/" + protocol + "/" + url;
  1185. }
  1186. return url;
  1187. }
  1188. // Main functions
  1189. function random_color() {
  1190. const r = Math.floor(Math.random() * 256);
  1191. const g = Math.floor(Math.random() * 256);
  1192. const b = Math.floor(Math.random() * 256);
  1193. return `rgb(${r}, ${g}, ${b})`;
  1194. }
  1195. function add_collect() {
  1196. // Get url
  1197. let url = input.value;
  1198. if (url.length == 0) {
  1199. url = prompt("请输入要收藏的网址:");
  1200. } else {
  1201. input.value = '';
  1202. }
  1203. if (url == undefined || url == null) {
  1204. cancel();
  1205. return;
  1206. } else if (url.length == 0) {
  1207. invalid();
  1208. return;
  1209. }
  1210. if (!url.startsWith("http://") && !url.startsWith("https://")) {
  1211. url = "https://" + url;
  1212. }
  1213. let url_;
  1214. try {
  1215. url_ = new URL(url);
  1216. } catch (error) {
  1217. invalid();
  1218. return;
  1219. }
  1220. // Get name
  1221. let name = ""; let desc = "";
  1222. name = prompt("请输入收藏项目的名称:", url_.hostname);
  1223. if (name == null) {
  1224. cancel();
  1225. return;
  1226. }
  1227. desc = prompt("请输入收藏项目的备注:", url_.hostname);
  1228. if (desc == null) {
  1229. cancel();
  1230. return;
  1231. }
  1232. const id = $("div[data-id=collection].block-group > div.block-group__content")?.childElementCount ?? 0;
  1233. const post_data = {
  1234. "resource_type": "vpn",
  1235. "name": name,
  1236. "detail": desc,
  1237. "url": url,
  1238. "redirect": encryptUrl(url),
  1239. "id": id,
  1240. "group_id": 2,
  1241. "logo": "",
  1242. "_isCollect": false,
  1243. "_displayName": name,
  1244. "_desc": desc,
  1245. _icon: {
  1246. "color": random_color(),
  1247. "content": name[0]
  1248. }
  1249. }
  1250. v.addCollect(post_data);
  1251. }
  1252. // Simple UI
  1253. const a = input.parentElement.appendChild(document.createElement("a"));
  1254. a.text = "⭐";
  1255. a.style = "position: absolute;left: 150px;top: 20px;";
  1256. a.onclick = add_collect;
  1257. // Shortcut
  1258. input.addEventListener("keydown", (e) => {
  1259. if (e.key === 'd' && e.ctrlKey) {
  1260. e.preventDefault();
  1261. add_collect();
  1262. }
  1263. });
  1264. }
  1265. function findAesJs() {
  1266. for (const f of unsafeWindow.webpackJsonp[1][1]) {
  1267. const s = f?.toString() || "";
  1268. if (s.includes("0123456789abcdef")) {
  1269. const receiver = new Object();
  1270. f(receiver, null, null);
  1271. return receiver.exports;
  1272. }
  1273. }
  1274. return null;
  1275. }
  1276. const aesjs = findAesJs();
  1277. if (aesjs) {
  1278. setup(aesjs);
  1279. } else {
  1280. fail("Failed to find Aes-js. You won't be able to use \"custom_collection\" feature.", "未能找到 Aes-js,您将无法使用自定义收藏功能!⚠️");
  1281. }
  1282. }
  1283. const observer = new MutationObserver(callback);
  1284. observer.observe(document.body, options);
  1285. }
  1286. break;
  1287. }
  1288. case 'icourse.club': {
  1289. config.down("icourse");
  1290. if (!config.get("icourse.enabled")) {
  1291. console.info("[USTC Helper] 'icourse' feature disabled.");
  1292. break;
  1293. }
  1294. function flash(ele) {
  1295. ele.animate([
  1296. { opacity: '1' }, // Start state (0%)
  1297. { opacity: '0' }, // 25%
  1298. { opacity: '1' }, // 50%
  1299. { opacity: '0' }, // 75%
  1300. { opacity: '1' } // End state (100%)
  1301. ], {
  1302. duration: 1000,
  1303. iterations: 2
  1304. });
  1305. }
  1306. function generateList(name, sel, download) {
  1307. const sideBar = $("div.col-md-4.rl-pd-lg");
  1308. const items = $$(sel);
  1309. if (!sideBar || items.length == 0) return;
  1310. const list = sideBar.appendChild(document.createElement("div"));
  1311. list.classList.add("ud-pd-md", "dashed");
  1312. const title = list.appendChild(document.createElement("h4"));
  1313. title.classList.add("blue");
  1314. title.textContent = name;
  1315. function addItem(ele) {
  1316. const [name, link] = [ele.textContent, ele.href];
  1317. const div = list.appendChild(document.createElement("div"));
  1318. div.classList.add("ud-pd-sm");
  1319. const a = div.appendChild(document.createElement("a"));
  1320. a.textContent = name;
  1321. a.href = link;
  1322. const ext = link.split('.').pop();
  1323. if (download && name.endsWith(ext)) {
  1324. a.download = name;
  1325. ele.download = name;
  1326. }
  1327. const span = div.appendChild(document.createElement("span"));
  1328. span.classList.add("grey", "float-right");
  1329. span.textContent = "定位";
  1330. span.style.cursor = "pointer";
  1331. span.addEventListener("click", (e) => {
  1332. e.preventDefault();
  1333. ele.focus();
  1334. setTimeout(() => flash(ele), 100);
  1335. });
  1336. span.insertAdjacentHTML('afterbegin', '<span class="glyphicon glyphicon-share-alt grey"></span>')
  1337. }
  1338. items.forEach(addItem);
  1339. }
  1340. if (config.get("icourse.filelist")) {
  1341. generateList("文件列表", "div.review-content a[href^='/uploads/files/']", true);
  1342. }
  1343. if (config.get("icourse.linklist")) {
  1344. generateList("链接列表", "div.review-content a:not([href^='/uploads/files/'])", false);
  1345. }
  1346. if (config.get("icourse.native_top")) {
  1347. const goTop = $("#gotop");
  1348. goTop?.addEventListener("click", (e) => {
  1349. window.scrollTo({ top: 0, behavior: 'smooth' });
  1350. e.stopPropagation();
  1351. }, { capture: true });
  1352. }
  1353. if (config.get("icourse.shortcut")) {
  1354. for (const textArea of $$("textarea")) { // Comment section
  1355. const submit = textArea.nextElementSibling.firstElementChild;
  1356. if (submit && submit.tagName == "BUTTON") {
  1357. textArea.addEventListener("keyup", handleKeyup);
  1358. }
  1359. }
  1360. function handleKeyup(e) {
  1361. const submit = this.nextElementSibling.firstElementChild;
  1362. if (e.ctrlKey && e.key == "Enter") {
  1363. submit.click(); // Ctrl+Enter to post comment
  1364. } else if (e.key == "Escape") {
  1365. this.value = ""; // Escape to clear comment
  1366. }
  1367.  
  1368. }
  1369. }
  1370. const icourse_css = {
  1371. "css": `html { scroll-behavior: smooth; } img { max-width: 100%; }`
  1372. };
  1373. setupDynamicStyles("icourse", config, icourse_css);
  1374. break;
  1375. }
  1376. default:
  1377. console.error("[USTC Helper] Unexpected host: " + window.location.host);
  1378. break;
  1379. }
  1380. })();