您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
为 USTC 学生定制的各类实用功能:验证码识别,自动登录,睿客网性能优化以及更多。
// ==UserScript== // @name USTC Helper // @name:zh-CN USTC 助手 // @license gpl-3.0 // @namespace http://tampermonkey.net/ // @version 1.3.8 // @description Various useful functions for USTC students: verification code recognition, auto login, rec performance improvement and more. // @description:zh-CN 为 USTC 学生定制的各类实用功能:验证码识别,自动登录,睿客网性能优化以及更多。 // @author PRO // @match https://mail.ustc.edu.cn/* // @match https://id.ustc.edu.cn/* // @match https://rec.ustc.edu.cn/* // @match https://recapi.ustc.edu.cn/identity/other_login?* // @match https://www.bb.ustc.edu.cn/* // @match https://jw.ustc.edu.cn/* // @match https://young.ustc.edu.cn/login* // @match https://young.ustc.edu.cn/nginx_auth/* // @match https://wvpn.ustc.edu.cn/* // @match https://icourse.club/* // @icon https://id.ustc.edu.cn/gate/linkid/api/image/download/login_favicon.png // @grant unsafeWindow // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_addValueChangeListener // @require https://github.com/PRO-2684/GM_config/releases/download/v1.2.1/config.min.js#md5=525526b8f0b6b8606cedf08c651163c2 // ==/UserScript== (function () { 'use strict'; const window = unsafeWindow; const log = console.log.bind(console, "[USTC Helper]"); const configDesc = { $default: { autoClose: false, }, id: { name: "Unified Authentication", type: "folder", items: { enabled: { name: "Enabled", title: "Whether to enable USTC Helper for Unified Authentication", type: "bool", value: true }, } }, mail: { name: "USTC Mail", type: "folder", items: { enabled: { name: "Enabled", title: "Whether to enable USTC Helper for USTC Mail", type: "bool", value: true }, focus: { name: "Focus", title: "Automatically focuses on login button", type: "bool", value: true }, remove_watermark: { name: "Remove watermark", title: "Remove the annoying watermark", type: "bool", value: true }, remove_background: { name: "Remove background", title: "Remove the background image", type: "bool", value: true }, } }, rec: { name: "Rec", type: "folder", items: { enabled: { name: "Enabled", title: "Whether to enable USTC Helper for Rec", type: "bool", value: true }, autologin: { name: "Auto login", title: "Automatically clicks login button", type: "bool", value: true }, opencurrent: { name: "Open in current tab", title: "Set some links to be opened in current tab (Significantly improves performance)", type: "bool", value: true }, } }, bb: { name: "BB System", type: "folder", items: { enabled: { name: "Enabled", title: "Whether to enable USTC Helper for BB System", type: "bool", value: true }, autoauth: { name: "Auto authenticate", title: "Automatically authenticate when accessing outside school net", type: "bool", value: true }, autologin: { name: "Auto login", title: "Automatically clicks login button", type: "bool", value: true }, showhwstatus: { name: "Show homework status", title: "Query all homework status (may consume some network traffic)", type: "bool", value: true }, } }, jw: { name: "Education Administration System", type: "folder", items: { enabled: { name: "Enabled", title: "Whether to enable USTC Helper for Education Administration System", type: "bool", value: true }, login: { name: "Login", type: "enum", value: 1, options: ['none', 'focus', 'click'], title: "What to do to the login button" }, shortcut: { name: "Shortcut", title: "Enable shortcut support", type: "bool", value: true }, score_mask: { name: "Score mask", title: "Allows you to hide/reveal your scores with dblclick", type: "bool", value: true }, detailed_time: { name: "Detailed time", title: "Show start/end time of each class", type: "bool", value: true }, css: { name: "CSS improve", title: "Minor CSS improvements", type: "bool", value: true }, privacy: { name: "Privacy", title: "Hides your personal information", type: "bool" }, sum: { name: "Sum", title: "Show the sum of credit and period at course table", type: "bool", value: true }, } }, young: { name: "Second Classroom", type: "folder", items: { enabled: { name: "Enabled", title: "Whether to enable USTC Helper for Second Classroom", type: "bool", value: true }, auto_auth: { name: "Auto authenticate", title: "Automatically authenticate when accessing outside school net", type: "bool", value: true }, default_tab: { name: "Default tab", value: "/myproject/SignUp", title: "The tab to be opened on entering" }, auto_tab: { name: "Auto tab", title: "Auto navigate to frequently-used submenu", type: "bool", value: true }, no_datascreen: { name: "No data screen", title: "Remove annoying data screen image", type: "bool", value: true }, shortcut: { name: "Shortcut", title: "Enable shortcut support", type: "bool", value: true } } }, wvpn: { name: "Web VPN", type: "folder", items: { enabled: { name: "Enabled", title: "Whether to enable USTC Helper for Web VPN", type: "bool", value: true }, custom_collection: { name: "Custom collection", title: "Allows you to fully customize your collection", type: "bool", value: true }, } }, icourse: { name: "Icourse", type: "folder", items: { enabled: { name: "Enabled", title: "Whether to enable USTC Helper for Icourse", type: "bool", value: true }, filelist: { name: "File list", title: "Show all uploaded files and name them properly", type: "bool", value: true }, linklist: { name: "Link list", title: "Show all links posted in the review section", type: "bool", value: true }, css: { name: "CSS improve", title: "Minor CSS improvements", type: "bool", value: true }, native_top: { name: "Native top", title: "Use native method to scroll to top", type: "bool", value: true }, shortcut: { name: "Shortcut", title: "Enable shortcut support", type: "bool", value: true }, } } }; const $ = document.querySelector.bind(document); const $$ = document.querySelectorAll.bind(document); async function timer(callback, interval = 500, times = 16) { return new Promise((resolve, reject) => { const timer = window.setInterval(() => { if (times-- === 0) { window.clearInterval(timer); resolve(false); } else if (callback()) { window.clearInterval(timer); resolve(true); } }, interval); }); } function setupDynamicStyles(host, config, styles) { function injectCSS(name) { const css = document.head.appendChild(document.createElement("style")); css.id = `ustc-helper-${host}-${name}`; css.textContent = styles[name]; } function toggleCSS(name, enabled) { const css = $(`#ustc-helper-${host}-${name}`); if (css) { css.disabled = !enabled; } else if (enabled) { injectCSS(name); } } for (const name in styles) { toggleCSS(name, config.proxy[`${host}.${name}`]); } config.addEventListener("set", e => { if (e.detail.prop.startsWith(`${host}.`)) { const name = e.detail.prop.split(".")[1]; if (name in styles) { toggleCSS(name, e.detail.after); } } }); } /** * Setup shortcuts for switching tabs and closing tabs * @param {Element} el The element to receive scroll wheel events * @param {Object} actions The actions for switching & closing tabs * @param {Function} actions.select The function to switch to a tab at given index, starting from 0 * @param {Function} actions.close The function to close a tab at given index, starting from 0 * @param {Function} actions.count The funtion to determine total number of tabs * @param {Function} actions.current The funtion to determine current index of the tab * @param {Function} [actions.special] The funtion to handle key ``` ` ``` */ function setupShortcuts(el, actions) { function delta(n) { const count = actions.count(); const current = actions.current(); actions.select((current + n + count) % count); } document.addEventListener("keydown", (e) => { const active = document.activeElement; if (active.nodeName === "INPUT" || active.nodeName === "TEXTAREA") { return; } const count = actions.count(); const current = actions.current(); switch (e.key) { case "ArrowLeft": delta(-1); break; case "ArrowRight": delta(1); break; case "x": actions.close(current); break; case "`": actions?.special?.(); // Optional default: if (e.key.length == 1) { const idx = Number(e.key); if (!isNaN(idx) && 0 < idx && idx <= count) { actions.select(idx - 1); } } break; } }); setupScroll(el, delta); } /** * Setup shortcuts for scroll wheel * @param {Element} el The element to be scrolled * @param {Function} delta The delta function */ function setupScroll(el, delta) { el.addEventListener("wheel", (e) => { e.preventDefault(); if (e.deltaY < 0) { delta(-1); } else if (e.deltaY > 0) { delta(1); } }); } const config = new GM_config(configDesc); const configProxy = config.proxy; switch (window.location.host) { case 'mail.ustc.edu.cn': { config.down("mail"); if (!configProxy["mail.enabled"]) { console.info("[USTC Helper] 'mail' feature disabled."); break; } if (configProxy["mail.focus"]) { timer(() => { const btn = $(".formLogin .submit"); if (btn) { btn.focus(); return true; } else { return false; } }).then((result) => { console.info(result ? "[USTC Helper] Login button focused." : "[USTC Helper] Login button not found!"); }); } const mail_css = { "remove_watermark": "div.watermark-wrap { display: none; }", "remove_background": ".lymain .lybg { display: none; }" } setupDynamicStyles("mail", config, mail_css); break; } case 'id.ustc.edu.cn': { config.down("id"); break; } case 'rec.ustc.edu.cn': { config.down("rec"); if (!configProxy["rec.enabled"]) { console.info("[USTC Helper] 'rec' feature disabled."); break; } if (configProxy["rec.opencurrent"]) { window.webpackJsonp.push_ = window.webpackJsonp.push; window.webpackJsonp.push = (val) => { if (val[0][0] !== "chunk-5ae262a1") return window.webpackJsonp.push_(val); else { // Following script is adapted from https://rec.ustc.edu.cn/js/chunk-5ae262a1.b84e1461.js val[1]["2c03"] = function (t, e, s) { "use strict"; (function (t) { s("55dd"); var r = s("a67e"); e["a"] = { name: "GroupLister", components: { GroupCreate: function () { return Promise.all([s.e("chunk-390136ce"), s.e("chunk-662e27b9")]).then(s.bind(null, "18fa")) }, GroupAdd: function () { return s.e("chunk-5b916374").then(s.bind(null, "c1c7")) }, GroupEdit: function () { return Promise.all([s.e("chunk-390136ce"), s.e("chunk-0daeb591")]).then(s.bind(null, "1fa6")) } }, data: function () { return { status: { GroupCreateStatus: !1, GroupAddStatus: !1, GroupEditStatus: !1 }, loading: !1, nothing: !1, group: {}, sortBy: {}, headers: [{ id: 1, title: "群名称", class: "groupname", sort: "asc", showSort: !0, field: "group_name" }, { id: 2, title: "群号", class: "groupid", sort: "des", showSort: !1, field: "group_number" }, { id: 3, title: "成员", class: "groupuser", sort: "des", showSort: !1, field: "group_memeber_count" }, { id: 5, title: "分享", class: "groupshare", sort: "des", showSort: !1, field: "group_share_file_count" }, { id: 6, title: "操作", class: "groupmenu", sort: "", showSort: !1 }] } }, created: function () { this.sortBy = this.headers[0], this.getGroups() }, computed: { userInfo: function () { return this.$store.state.user.userInfo } }, watch: { $route: function () { this.getGroups() } }, filters: { identityNameFilter: function (t) { var e; switch (t) { case "owner": e = "群主"; break; case "admin": e = "管理员"; break; case "user": e = "成员"; break; default: break } return e } }, methods: { createGroup: function () { t("#newgroup").modal("show") }, addGroup: function () { t("#addgroup").modal("show") }, invite: function (t) { var e = this.$router.resolve({ name: "group", params: { groupNumber: t.group_number } }); this.$confirm({ showYesBtn: !1, showCopyBtn: !0, copyBtnText: "复制文字", title: "邀请入群", type: "confirm", content: "打开链接进入群组主页即可申请加入群组:".concat(t.group_name, ",群组主页链接:").concat(window.location.origin).concat(e.href) }).then((function () { } )).catch((function () { } )) }, goToGroupCloud: function (t, e) { if (["owner", "admin", "user"].indexOf(t.group_member_identity) < 0) return this.$message({ type: "warning", message: "您不是组群成员,无法进入群盘" }), !1; this.$store.commit("setSetting", { from: !0, drive: "groupdisk", tab: e, group: t }), this.$router.push({ name: "groupDisk", params: { groupNumber: t.group_number } }) }, isShowMenu: function (t) { return ["owner", "admin", "user"].indexOf(t.group_member_identity) > -1 }, isEditGroup: function (t) { return ["owner", "admin"].indexOf(t.group_member_identity) > -1 }, goToGroup: function (t) { var e = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : "group"; if ("wait" === t.group_is_review) return this.$message({ type: "warning", message: "群组待审核,不允许操作!" }), !1; if ("refuse" === t.group_is_review) return this.$message({ type: "warning", message: "群组审核未通过,不允许操作!" }), !1; // Instead of opening in new tab, we prefer to use vue's solution // Modifiy start this.$router.replace({ name: e, params: { groupNumber: t.group_number } }); // Modify end }, goToGroupHome: function (t) { this.$store.commit("SET_GROUP_SHOWDESC", !1), this.$router.push({ name: "group", params: { groupNumber: t } }) }, handleEditGroup: function (e) { var s = this; Object(r["g"])(e.group_number).then((function (t) { s.group = t.entity } )).catch((function (t) { s.$message({ type: "error", message: t }) } )), t("#editgroup").modal("show") }, groupRefresh: function () { this.getGroups() }, sortGroup: function (t) { if (6 === t) return !1; var e = this; this.headers.map((function (s) { return s.id === t ? (s.showSort = !0, s.sort = "des" === s.sort ? "asc" : "des", e.sortBy = s, s) : (s.showSort = !1, s.sort = "des", s) } )), this.sortGroupBy() }, getGroups: function () { var t = this; this.groups = [], this.loading = !0, this.nothing = !1, Object(r["r"])({}).then((function (e) { if (200 === e.status_code) if (t.loading = !1, t.groups = e.entity.datas, e.entity.total > 0) { var s = 0; e.entity.datas.map((function (t) { "user" != t.group_member_identity && t.group_pending_member_count > 0 && (s += t.group_pending_member_count) } )), t.$store.commit("setRequestNums", s), t.sortGroupBy(!0) } else t.nothing = !0; else t.$message({ type: "error", message: e.message }) } )).catch((function (e) { t.$message({ type: "error", message: e }) } )) }, sortGroupBy: function () { var t = this , e = arguments.length > 0 && void 0 !== arguments[0] && arguments[0]; this.groups.sort((function (s, r) { var o; 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], o = "asc" === t.sortBy.sort ? o : -o, o } )) }, groupCancel: function (t) { var e = this , s = "adopt" === t.group_is_review ? "解散" : "删除"; this.$confirm({ type: "confirm", content: "".concat(s, "群后,所有关于本群组的信息都将被删除且无法恢复,确定").concat(s, "【").concat(t.group_name, "】吗?"), showCancleBtn: !0, showYesBtn: !0, custom: [] }).then((function () { Object(r["u"])({ groups_list: [t.group_number] }).then((function (t) { 200 === t.status_code ? (e.$message({ type: "success", message: t.message }), e.getGroups()) : e.$message({ type: "error", message: t.message }) } )).catch((function (t) { e.$message({ type: "error", message: t }) } )) } )).catch((function () { } )) }, groupQuit: function (t) { var e = this; this.$confirm({ type: "confirm", content: "确定退出该群组吗?", showCancleBtn: !0, showYesBtn: !0, custom: [] }).then((function () { Object(r["v"])({ group_number: t, action: "quit", members_list: [e.userInfo.user_number] }).then((function (t) { 200 === t.status_code ? (e.$message({ type: "success", message: t.message }), e.getGroups()) : e.$message({ type: "error", message: t.message }) } )).catch((function (t) { e.$message({ type: "error", message: t }) } )) } )).catch((function () { } )) } }, mounted: function () { var t = this; setTimeout((function () { for (var e in t.status) t.status[e] = !0 } ), 500) } } } ).call(this, s("1157")) }; return window.webpackJsonp.push_(val); } }; } if (configProxy["rec.autologin"] && document.location.pathname == '/') { const app = $("#app"); const options = { childList: true, attributes: false, subtree: true } const observer = new MutationObserver(() => { const btn = $('.navbar-login-btn'); if (btn) { btn.click(); observer.disconnect(); } }); observer.observe(app, options); } else if (configProxy["rec.opencurrent"]) { const app = $("#app"); const options = { childList: true, attributes: false, subtree: true } const observer = new MutationObserver(() => { const l = $$(".app-list").length; if (l) { const links = $$("a"); for (const link of links) { if (link.target == '_blank') link.removeAttribute("target"); } } }); observer.observe(app, options); } break; } case 'recapi.ustc.edu.cn': { config.down("rec"); if (!configProxy["rec.enabled"]) { console.info("[USTC Helper] 'rec' feature disabled."); break; } if (configProxy["rec.autologin"]) { const btn = $("#ltwo > div > button"); if (!btn) { console.error("[USTC Helper] Login button not found!"); } else { btn.click(); } } break; } case 'www.bb.ustc.edu.cn': { config.down("bb"); if (!configProxy["bb.enabled"]) { console.info("[USTC Helper] 'bb' feature disabled."); break; } if (window.location.pathname == '/nginx_auth/' && configProxy["bb.autoauth"]) { $('a')?.click(); } else if ((window.location.pathname == '/' || window.location.pathname == '/webapps/login/') && configProxy["bb.autologin"]) { $('#login > table > tbody > tr > td:nth-child(2) > span > a')?.click(); } else if (configProxy["bb.showhwstatus"] && window.location.pathname == '/webapps/blackboard/content/listContent.jsp' && document.getElementById('pageTitleText').children[0].textContent == '作业区') { const css = document.createElement('style'); css.textContent = ".ustc-helper-bb-ignored { opacity: 0.4; } .ustc-helper-bb-ignored > .details { display: none; }"; document.head.appendChild(css); const hw_list = document.getElementById('content_listContainer'); const color_config = ['grey', 'green', 'red', 'yellow', 'grey', 'cyan']; const hint_text = ['查询中', '已提交', '未提交', '查询错误', '已忽略', '已评分']; // const hint_text = ['Checking', 'Submitted', 'Not submitted', 'Error', 'Ignored', 'Graded']; function ignore_hw(course_id, content_id, ignore) { let ignored = localStorage.getItem(course_id) || '[]'; ignored = JSON.parse(ignored); if (ignore && !ignored.includes(content_id)) { ignored.push(content_id); log(`Ignoring "${course_id}/${content_id}"...`); } else if (!ignore && ignored.includes(content_id)) { ignored = ignored.filter((v) => v != content_id); log(`Un-ignoring "${course_id}/${content_id}"...`); } if (ignored.length) localStorage.setItem(course_id, JSON.stringify(ignored)); else localStorage.removeItem(course_id); } async function query_status(link) { const r = await fetch(link); if (!r.ok) { log(`Failed to fetch "${r.url}": ${r.status} ${r.statusText}`); return [3, null]; } else { const parser = new DOMParser(); const html = await r.text(); const doc = parser.parseFromString(html, 'text/html'); const title = doc.getElementById('pageTitleText').textContent.trim(); if (title.startsWith('上载作业')) { return [2, null]; } else if (title.startsWith('复查提交历史记录')) { const grade = doc.getElementById("aggregateGrade"); const suffix = doc.getElementById("aggregateGrade_pointsPossible"); if (grade.value !== '-') { return [5, `${parseFloat(grade.value)}${suffix.textContent.trim()}`]; } else { return [1, null]; } } else { return [3, null]; } } } async function process(hw) { const link_ = hw.querySelector("h3 > a"); if (!link_) return; let status = [0, null]; // 0: Checking 1: Uploaded 2: Not uploaded 3: Error const hint = link_.appendChild(document.createElement('span')); hint.style.color = color_config[status[0]]; hint.textContent = `(${hint_text[status[0]]})`; const link = link_.href; // https://www.bb.ustc.edu.cn/webapps/assignment/uploadAssignment?content_id=_106763_1&course_id=_12559_1&group_id=&mode=view const params = new URL(link).searchParams; const course_id = params.get("course_id"); const content_id = params.get("content_id"); let ignored = localStorage.getItem(course_id); // Check if this homework is ignored if (ignored) { ignored = JSON.parse(ignored).includes(content_id); if (ignored) { status[0] = 4; // link_.parentNode.parentNode.parentNode.style.opacity = 0.4; link_.parentNode.parentNode.parentNode.classList.add("ustc-helper-bb-ignored"); log(`"${course_id}/${content_id}" present in ignore list, so this homework is ignored.`); } } // Not in cache if (!status[0]) { status = await query_status(link); if (status[0] == 1) { log(`Online query indicated that "${course_id}/${content_id}" is uploaded.`); } else if (status[0] == 2) { log(`Online query indicated that "${course_id}/${content_id}" is not uploaded.`); } else if (status[0] == 5) { log(`Online query indicated that "${course_id}/${content_id}" is graded.`); } else { console.warn(`[USTC Helper] Online query "${course_id}/${content_id}" failed!`); } } hint.style.color = color_config[status[0]]; hint.textContent = `(${hint_text[status[0]]}${status[1] ? " " + status[1] : ""})`; hint.title = ignored ? "点击取消忽略此作业" : "点击忽略此作业"; hint.addEventListener('click', e => { e.preventDefault(); ignore_hw(course_id, content_id, !ignored); hint.title = "刷新页面以生效"; hint.style.color = color_config[4]; hint.textContent = "(请刷新)"; }, { once: true }); } for (const hw of hw_list.children) { process(hw); } } break; } case 'jw.ustc.edu.cn': { config.down("jw"); if (!configProxy["jw.enabled"]) { console.info("[USTC Helper] 'jw' feature disabled."); break; } if (configProxy["jw.login"] && window.location.pathname == "/login") { const btn = document.getElementById('login-unified-wrapper'); if (configProxy["jw.login"] === 1) { btn.focus(); } else if (configProxy["jw.login"] === 2) { btn.click(); } else if (configProxy["jw.login"] !== 0) { console.error(`[USTC Helper] Unknown option for jw.login: ${configProxy["jw.login"]}`); } } if (configProxy["jw.shortcut"] && window.location.pathname == "/home") { timer(() => { const tabList = $("#e-home-tab-list"); if (!tabList) return false; const tabs = tabList?.children; const home = $("#e-top-home-page > li > a > div.home-logo"); const header = tabList.parentElement; const actions = { select: (index) => tabs[index]?.querySelector("span.tabTitle")?.click(), close: (index) => tabs[index]?.querySelector("a > i.fa-times")?.click(), count: () => tabs.length, current: () => { for (let i = 0; i < tabs.length; i++) { if (tabs[i].classList.contains('active')) return i; } return -1; }, special: () => home?.click() }; setupShortcuts(header, actions); return true; }).then((success) => { log(success ? "Shortcuts have been setup." : "Failed to setup shortcuts."); }); const list = $("#primaryCarousel > .carousel-inner"); if (list) { const left = $("#primaryCarousel > a.left[data-slide='prev']"); const right = $("#primaryCarousel > a.right[data-slide='next']"); setupScroll(list, (delta) => { if (delta < 0) left.click(); else right.click(); }); } } if (configProxy["jw.score_mask"] && window.location.pathname == "/for-std/grade/sheet") { function get_status(entry) { // Status: // false: Normal display // true: Masked if (entry.classList.contains("masked")) return true; else return false; } function set_status_internal(entry, state) { const gpa = entry.children[entry.children.length - 2]; const score = entry.lastChild; if (state) { entry.classList.add("masked"); entry.setAttribute("data-gpa", gpa.textContent); entry.setAttribute("data-score", score.textContent); gpa.textContent = ""; score.textContent = ""; } else { entry.classList.remove("masked"); let gpa_val = entry.getAttribute("data-gpa"); let score_val = entry.getAttribute("data-score"); if (gpa_val) gpa.textContent = gpa_val; if (score_val) score.textContent = score_val; } } function toggle() { set_status_internal(this, !get_status(this)); } function set_status(entry, state) { if (get_status(entry) == state) return; set_status_internal(entry, state); } function toggle_view() { if (this.hasAttribute("data-value")) { this.lastChild.textContent = this.getAttribute("data-value"); this.removeAttribute("data-value"); } else { this.setAttribute("data-value", this.lastChild.textContent); this.lastChild.textContent = "尚未评教"; } } function toggle_rank() { if (this.hasAttribute("data-value")) { this.textContent = this.getAttribute("data-value"); this.removeAttribute("data-value"); } else { this.setAttribute("data-value", this.textContent); this.textContent = "尚未评教"; } } function setup() { const tables = $$("div.semesters > section > div.semester > table"); tables.forEach((table) => { let head = table.querySelector("thead"); let entries = table.querySelectorAll("tbody > tr"); head.addEventListener("dblclick", (e) => { let status = head.getAttribute("data-masked") === ""; entries.forEach((entry) => { set_status(entry, !status); }); if (status) head.removeAttribute("data-masked"); else head.setAttribute("data-masked", ""); }); entries.forEach((entry) => { entry.addEventListener("dblclick", toggle); }); }); const history_table = $("table.history-table"); history_table.tHead.addEventListener("dblclick", (e) => { history_table.querySelectorAll("tbody:not(.hidden)").forEach((tbody) => { const status = tbody.getAttribute("data-masked") === ""; tbody.querySelectorAll("tr").forEach((entry) => { set_status(entry, !status); }); if (status) tbody.removeAttribute("data-masked"); else tbody.setAttribute("data-masked", ""); }); }); history_table.querySelectorAll("tbody > tr").forEach((entry) => { entry.addEventListener("dblclick", toggle); }); const view = $("div.overview > ul"); view.childNodes.forEach((node) => { node.addEventListener("dblclick", toggle_view); }); const rank = $("div.rankinfo > div"); rank.querySelectorAll("b").forEach((node) => { node.addEventListener("dblclick", toggle_rank); }); } timer(() => { const test = $("div.overview > ul > li > span:nth-child(2)"); if (test.textContent != "NaN") { setup(); return true; } return false; }, 1000, 8).then((success) => { console.info("[USTC Helper] Score mask setup " + (success ? "succeeded." : "failed.")); }); } const jw_css = { "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; } table.timetable tbody th.span::before { content: attr(data-start); top: 0; } table.timetable tbody th.span::after { content: attr(data-end); bottom: 0; }`, "css": `div#dropdown-menu-filter { display: none; } div#dropdown-menu-bg { backdrop-filter: blur(3px); } div.second-menu-wrap div.menu-area { width: 100%; } li.home div.dropdown-menu { width: 25vw !important; min-width: 400px !important; } .primary .item li.primaryLi.hover { transition: transform 0.25s ease; } .primaryLi .subMenus { cursor: initial; opacity: 0.8; } div#shortcut { width: 27em; } .shortcut-panel .shortcut-item { width: 25%; } .primary-container .primaryLi .subMenus { width: 400px; border-radius: inherit; overflow: auto; } #e-content-area #e-op-area div.e-toolbarTab { padding: 0 !important; } .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; }`, "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); } img[src='/my/avatar'] { filter: blur(1em); }` }; setupDynamicStyles("jw", config, jw_css); if (window.location.pathname.startsWith("/for-std/course-table")) { if (configProxy["jw.sum"]) { const table = $("#lessons"); if (table) { const rows = table.querySelectorAll("tbody > tr"); const indexes = [3, 9]; let sums = [0, 0]; for (const row of rows) { for (let i = 0; i < indexes.length; i++) { sums[i] += parseFloat(row.children[indexes[i]].textContent); } } const head = table.querySelector("thead > tr"); for (let i = 0; i < indexes.length; i++) { head.children[indexes[i]].title = `总计:${sums[i]}`; } } } } break; } case 'young.ustc.edu.cn': { config.down("young"); if (!configProxy["young.enabled"]) { console.info("[USTC Helper] 'young' feature disabled."); break; } if (window.location.pathname == '/nginx_auth/' && configProxy["young.auto_auth"]) { document.getElementsByTagName('a')[0].click(); return; } const app = $("#app"); const router = app.__vue__.$router; function main(mutations, observer) { const menu = app.querySelector(".ant-menu-root"); if (!menu) return; const default_tab = configProxy["young.default_tab"]; if (default_tab.length) router.push(default_tab); const submenus = menu.querySelectorAll("li.ant-menu-submenu-horizontal:not(.ant-menu-overflowed-submenu) > div"); if (!submenus.length) return; observer.disconnect(); if (configProxy["young.no_datascreen"]) { app.querySelector("div.header-index-wide > a").remove(); function getCloseBtn() { return app.querySelector("span[pagekey='/dataAnalysis/visual']")?.nextElementSibling; } function close() { const tabs = $(".ant-tabs-nav-animated > div").children; if (tabs.length == 1) return false; const closeBtn = getCloseBtn(); if (closeBtn) { closeBtn.click(); return !getCloseBtn(); } else { return false; } } timer(() => close()).then((success) => { log(success ? "Data screen closed." : "Failed to close data screen."); }); } if (configProxy["young.auto_tab"]) { submenus[0].onclick = (e) => { router.push('/dataAnalysis/studentAnalysis'); e.stopImmediatePropagation(); } submenus[1].onclick = (e) => { router.push('/personalInformation/personalReport'); } submenus[2].onclick = (e) => { router.push('/myproject/SignUp'); } submenus[5].onclick = (e) => { router.push('/isystem/departUserList'); } app.querySelector(".user-dropdown-menu").onclick = (e) => { $("ul.user-dropdown-menu-wrapper > li:nth-child(7) > a").click(); } // They're generated dynamically when you hover over the menu... // const submenuItems = $$(".ant-menu-submenu.ant-menu-submenu-vertical"); // Submenu items with even more submenus // log(submenuItems); // submenuItems.forEach((submenuItem) => { // const subsubmenuItems = submenuItem.querySelectorAll(".ant-menu.ant-menu-vertical.ant-menu-sub.ant-menu-submenu-content > .ant-menu-item"); // if (subsubmenuItems.length == 1) { // const pathName = subsubmenuItems[0].querySelector("a")?.pathname; // if (pathName) { // submenuItem.onclick = (e) => { // router.push(pathName); // } // } // } // }); } if (configProxy["young.shortcut"]) { const tabList = $(".ant-tabs-nav-animated > div") const tabs = tabList.children; const nav = tabList.parentElement.parentElement; const actions = { select: (index) => { tabs[index].click(); }, close: (index) => { const closeBtn = tabs[index].querySelector("div > i"); if (closeBtn) closeBtn.click(); }, count: () => tabs.length, current: () => { let current = 0; for (const tab of tabs) { if (tab.attributes["aria-selected"].value == "true") { break; } current++; } return current; } }; setupShortcuts(nav, actions); } } const options = { childList: true, attributes: false, subtree: true } const observer = new MutationObserver(main); observer.observe(app, options); break; } case 'wvpn.ustc.edu.cn': { config.down("wvpn"); if (!configProxy["wvpn.enabled"]) { console.info("[USTC Helper] 'wvpn' feature disabled."); break; } if (configProxy["wvpn.custom_collection"]) { const options = { childList: true, attributes: false, subtree: true } const callback = (mutations, observer) => { const input = $("input.portal-search__input"); const ele = $("div#__layout > div.wrd-webvpn"); if (!input || !input.placeholder || !ele) return; const v = ele.__vue__; observer.disconnect(); function fail(s, hint) { console.error("[USTC Helper]", s); alert(hint); } function cancel() { console.info("[USTC Helper] User calcelled the operation."); alert("你终止了收藏操作!😢"); } function invalid() { console.warn("[USTC Helper] Invalid input!"); alert("你输入了一个不合法的值!🤔"); } function setup(aesjs) { input.placeholder = "点击五角星或 Ctrl+D 以收藏 🍻"; // Encryption, adapted from https://blog.csdn.net/lijiext/article/details/110931285 const utf8 = aesjs.utils.utf8; const hex = aesjs.utils.hex; const AesCfb = aesjs.ModeOfOperation.cfb; const wrdvpnKey = 'wrdvpnisthebest!'; const wrdvpnIV = 'wrdvpnisthebest!'; function textRightAppend(text, mode) { const segmentByteSize = mode === 'utf8' ? 16 : 32; if (!(text.length % segmentByteSize)) { return text; } const appendLength = segmentByteSize - text.length % segmentByteSize; for (let i = 0; i < appendLength; i++) { text += '0'; } return text; } function encrypt(text, key, iv) { const textLength = text.length; text = textRightAppend(text, 'utf8'); const keyBytes = utf8.toBytes(key); const ivBytes = utf8.toBytes(iv); const textBytes = utf8.toBytes(text); const aesCfb = new AesCfb(keyBytes, ivBytes, 16); const encryptBytes = aesCfb.encrypt(textBytes); return hex.fromBytes(ivBytes) + hex.fromBytes(encryptBytes).slice(0, textLength * 2); } function encryptUrl(url) { let port = ""; let segments = ""; let protocol = ""; if (url.startsWith("http://")) { url = url.substr(7); protocol = "http"; } else if (url.startsWith("https://")) { url = url.substr(8); protocol = "https"; } else { return ""; } let v6 = ""; const match = /\[[0-9a-fA-F:]+?\]/.exec(url); if (match) { v6 = match[0]; url = url.slice(match[0].length); } segments = url.split("?")[0].split(":"); if (segments.length > 1) { port = segments[1].split("/")[0] url = url.substr(0, segments[0].length) + url.substr(segments[0].length + port.length + 1); } const i = url.indexOf('/'); if (i == -1) { if (v6 != "") { url = v6; } url = encrypt(url, wrdvpnKey, wrdvpnIV) } else { const host = url.slice(0, i); const path = url.slice(i); if (v6 != "") { host = v6; } url = encrypt(host, wrdvpnKey, wrdvpnIV) + path; } if (port != "") { url = "/" + protocol + "-" + port + "/" + url; } else { url = "/" + protocol + "/" + url; } return url; } // Main functions function random_color() { const r = Math.floor(Math.random() * 256); const g = Math.floor(Math.random() * 256); const b = Math.floor(Math.random() * 256); return `rgb(${r}, ${g}, ${b})`; } function add_collect() { // Get url let url = input.value; if (url.length == 0) { url = prompt("请输入要收藏的网址:"); } else { input.value = ''; } if (url == undefined || url == null) { cancel(); return; } else if (url.length == 0) { invalid(); return; } if (!url.startsWith("http://") && !url.startsWith("https://")) { url = "https://" + url; } let url_; try { url_ = new URL(url); } catch (error) { invalid(); return; } // Get name let name = ""; let desc = ""; name = prompt("请输入收藏项目的名称:", url_.hostname); if (name == null) { cancel(); return; } desc = prompt("请输入收藏项目的备注:", url_.hostname); if (desc == null) { cancel(); return; } const id = $("div[data-id=collection].block-group > div.block-group__content")?.childElementCount ?? 0; const post_data = { "resource_type": "vpn", "name": name, "detail": desc, "url": url, "redirect": encryptUrl(url), "id": id, "group_id": 2, "logo": "", "_isCollect": false, "_displayName": name, "_desc": desc, _icon: { "color": random_color(), "content": name[0] } } v.addCollect(post_data); } // Simple UI const a = input.parentElement.appendChild(document.createElement("a")); a.text = "⭐"; a.style = "position: absolute;left: 150px;top: 20px;"; a.onclick = add_collect; // Shortcut input.addEventListener("keydown", (e) => { if (e.key === 'd' && e.ctrlKey) { e.preventDefault(); add_collect(); } }); } function findAesJs() { for (const f of unsafeWindow.webpackJsonp[1][1]) { const s = f?.toString() || ""; if (s.includes("0123456789abcdef")) { const receiver = new Object(); f(receiver, null, null); return receiver.exports; } } return null; } const aesjs = findAesJs(); if (aesjs) { setup(aesjs); } else { fail("Failed to find Aes-js. You won't be able to use \"custom_collection\" feature.", "未能找到 Aes-js,您将无法使用自定义收藏功能!⚠️"); } } const observer = new MutationObserver(callback); observer.observe(document.body, options); } break; } case 'icourse.club': { config.down("icourse"); if (!configProxy["icourse.enabled"]) { console.info("[USTC Helper] 'icourse' feature disabled."); break; } function flash(ele) { ele.animate([ { opacity: '1' }, // Start state (0%) { opacity: '0' }, // 25% { opacity: '1' }, // 50% { opacity: '0' }, // 75% { opacity: '1' } // End state (100%) ], { duration: 1000, iterations: 2 }); } function generateList(name, sel, download) { const sideBar = $("div.col-md-4.rl-pd-lg"); const items = $$(sel); if (!sideBar || items.length == 0) return; const list = sideBar.appendChild(document.createElement("div")); list.classList.add("ud-pd-md", "dashed"); const title = list.appendChild(document.createElement("h4")); title.classList.add("blue"); title.textContent = name; function addItem(ele) { const [name, link] = [ele.textContent, ele.href]; const div = list.appendChild(document.createElement("div")); div.classList.add("ud-pd-sm"); const a = div.appendChild(document.createElement("a")); a.textContent = name; a.href = link; const ext = link.split('.').pop(); if (download && name.endsWith(ext)) { a.download = name; ele.download = name; } const span = div.appendChild(document.createElement("span")); span.classList.add("grey", "float-right"); span.textContent = "定位"; span.style.cursor = "pointer"; span.addEventListener("click", (e) => { e.preventDefault(); ele.focus(); setTimeout(() => flash(ele), 100); }); span.insertAdjacentHTML('afterbegin', '<span class="glyphicon glyphicon-share-alt grey"></span>') } items.forEach(addItem); } if (configProxy["icourse.filelist"]) { generateList("文件列表", "div.review-content a[href^='/uploads/files/']", true); } if (configProxy["icourse.linklist"]) { generateList("链接列表", "div.review-content a:not([href^='/uploads/files/'])", false); } if (configProxy["icourse.native_top"]) { const goTop = $("#gotop"); goTop?.addEventListener("click", (e) => { window.scrollTo({ top: 0, behavior: 'smooth' }); e.stopPropagation(); }, { capture: true }); } if (configProxy["icourse.shortcut"]) { for (const textArea of $$("textarea")) { // Comment section const submit = textArea.nextElementSibling.firstElementChild; if (submit && submit.tagName == "BUTTON") { textArea.addEventListener("keyup", handleKeyup); } } function handleKeyup(e) { const submit = this.nextElementSibling.firstElementChild; if (e.ctrlKey && e.key == "Enter") { submit.click(); // Ctrl+Enter to post comment } else if (e.key == "Escape") { this.value = ""; // Escape to clear comment } } } const icourse_css = { "css": `html { scroll-behavior: smooth; } img { max-width: 100%; }` }; setupDynamicStyles("icourse", config, icourse_css); break; } default: console.error("[USTC Helper] Unexpected host: " + window.location.host); break; } })();