USTC Helper

Various useful functions for USTC students: verification code recognition, auto login, rec performance improvement and more.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         USTC Helper
// @name:zh-CN   USTC 助手
// @license      gpl-3.0
// @namespace    http://tampermonkey.net/
// @version      1.4.0
// @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.2/config.min.js#md5=c45f9b0d19ba69bb2d44918746c4d7ae
// ==/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 function to determine total number of tabs
     * @param {Function} actions.current The function to determine current index of the tab
     * @param {Function} [actions.special] The function 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);
    switch (window.location.host) {
        case 'mail.ustc.edu.cn': {
            config.down("mail");
            if (!config.get("mail.enabled")) {
                console.info("[USTC Helper] 'mail' feature disabled.");
                break;
            }
            if (config.get("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 (!config.get("rec.enabled")) {
                console.info("[USTC Helper] 'rec' feature disabled.");
                break;
            }
            if (config.get("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
                                            // Modify 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 (config.get("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 (config.get("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 (!config.get("rec.enabled")) {
                console.info("[USTC Helper] 'rec' feature disabled.");
                break;
            }
            if (config.get("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 (!config.get("bb.enabled")) {
                console.info("[USTC Helper] 'bb' feature disabled.");
                break;
            }
            if (window.location.pathname == '/nginx_auth/' && config.get("bb.autoauth")) {
                $('a')?.click();
            } else if ((window.location.pathname == '/' || window.location.pathname == '/webapps/login/') && config.get("bb.autologin")) {
                $('#login > table > tbody > tr > td:nth-child(2) > span > a')?.click();
            } else if (config.get("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 (!config.get("jw.enabled")) {
                console.info("[USTC Helper] 'jw' feature disabled.");
                break;
            }
            if (config.get("jw.login") && window.location.pathname == "/login") {
                const btn = document.getElementById('login-unified-wrapper');
                if (config.get("jw.login") === 1) {
                    btn.focus();
                } else if (config.get("jw.login") === 2) {
                    btn.click();
                } else if (config.get("jw.login") !== 0) {
                    console.error(`[USTC Helper] Unknown option for jw.login: ${config.get("jw.login")}`);
                }
            }
            if (config.get("jw.shortcut")) {
                if (window.location.pathname == "/home") { // Top frame
                    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();
                        });
                    }
                } else { // Bubble key events up
                    document.addEventListener("keydown", (e) => {
                        const active = document.activeElement;
                        if (active.nodeName === "INPUT" || active.nodeName === "TEXTAREA") return;
                        window.parent.document.dispatchEvent(new KeyboardEvent(e.type, e));
                        e.stopPropagation();
                    });
                }
            }
            if (config.get("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 (config.get("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 (!config.get("young.enabled")) {
                console.info("[USTC Helper] 'young' feature disabled.");
                break;
            }
            if (window.location.pathname == '/nginx_auth/' && config.get("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 = config.get("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 (config.get("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 (config.get("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 (config.get("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 (!config.get("wvpn.enabled")) {
                console.info("[USTC Helper] 'wvpn' feature disabled.");
                break;
            }
            if (config.get("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 (!config.get("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 (config.get("icourse.filelist")) {
                generateList("文件列表", "div.review-content a[href^='/uploads/files/']", true);
            }
            if (config.get("icourse.linklist")) {
                generateList("链接列表", "div.review-content a:not([href^='/uploads/files/'])", false);
            }
            if (config.get("icourse.native_top")) {
                const goTop = $("#gotop");
                goTop?.addEventListener("click", (e) => {
                    window.scrollTo({ top: 0, behavior: 'smooth' });
                    e.stopPropagation();
                }, { capture: true });
            }
            if (config.get("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;
    }
})();