北化网课雨

never try to take over the world!

// ==UserScript==
// @name         北化网课雨
// @namespace    http://tampermonkey.net/
// @version      0.4.2
// @description  never try to take over the world!
// @author       Snowman
// @match        https://buct.yuketang.cn/pro/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=yuketang.cn
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    class Utils {

        /** 页面环境 */
        static Environment = {
            UNKNOWN: 0,
            VIDEO: 1,
            SCORE: 2,
        }

        /** 获取当前环境,以决定启动哪些模块 */
        static GetEnvironment() {

            let path = location.pathname.split('/');

            if (path[path.length - 2] == 'video') {
                return Utils.Environment.VIDEO;
            }
            if (path[path.length - 1] == 'score') {
                return Utils.Environment.SCORE;
            }
            return Utils.Environment.UNKNOWN;

        }

        /** 关闭浏览器当前标签页 */
        static CloseWindow() {
            var userAgent = navigator.userAgent;
            if (userAgent.indexOf("Firefox") != -1 || userAgent.indexOf("Chrome") != -1) {
                window.location.href = "about:blank";
                window.close();
            } else {
                window.opener = null;
                window.open("", "_self");
                window.close();
            }
        }

        /**
         * 向目标元素注入 CSS
         * @param {Element} target 注入目标
         * @param {string} css CSS 内容
         */
        static InsertCSS(target, css) {

            if (!target) return false;

            let style = document.createElement("style");
            let text = document.createTextNode(css);
            style.appendChild(text);
            target.appendChild(style);
            return true;

        }


        /**
         * 选择单个元素
         * @param {string} selector JS Selector 语句
         * @param {boolean} mustExist 元素是否必须存在
         * @returns {Function}
         */
        static SelectOne = (selector, mustExist = false) => {

            let _selector = selector;
            let _element = null;

            /** @returns {Element | null} */
            let f = function () {
                if (_element == null)
                    _element = document.querySelector(_selector);
                if (mustExist && _element == null)
                    throw { why: "错误:找不到元素", _selector };
                else
                    return _element;
            }
            return f;
        }


        /**
         * 选择所有元素
         * @param {string} selector JS Selector 语句
         * @param {boolean} mustExist 元素是否必须存在
         * @returns {Function}
         */
        static SelectAll = (selector, mustExist = true) => {

            let _selector = selector;
            let _elements = null;

            /** @returns {Element | null} */
            let f = function () {
                if (_elements == null)
                    _elements = document.querySelectorAll(_selector);
                if (mustExist && _elements == null)
                    throw { why: "错误:找不到元素", _selector };
                else
                    return _elements;
            }
            return f;
        }


        /**
         * 坚持不懈地尝试做一件事
         * @param {Function} task 要做的事,必须返回布尔值表示是否已成功完成
         * @param {number} maxTries 最大尝试次数
         * @param {number} interval 尝试间隔时间
         */
        static TryTryTry(task, maxTries = 5, interval = 1000) {

            let counter = 0;
            let result;

            let clock = setInterval(() => {

                // counter++,并检测是否超出尝试次数
                if (++counter > maxTries) {
                    clearInterval(clock);
                    console.warn("任务失败次数过多,超出尝试上限!", { maxTries, interval, task });
                    return;
                }

                try {
                    result = task();
                } catch (error) {
                    throw { why: "错误:任务执行时自身出错!", error };
                }

                if (typeof result !== 'boolean')
                    throw { why: "错误:任务返回值必须为布尔类型!", result };

                if (result) {
                    clearInterval(clock);
                    console.log("[Debug] 任务完成,尝试次数为 " + counter);
                    return;
                }

            }, interval);

        }

    }


    /** 若干功能模块 */
    class Modules {

        /** 被迫展示自己被黑化的事实 */
        static ShowBlacklized() {
            setTimeout(() => {
                ElementPool.title().innerHTML += `<div style="color:green;"><font>(已黑化)</font><font id="sg-rate"></font></div>`;
                console.log("完成了喵~");
            }, 1000);
        }

        /** 危险的多开 */
        static MultiWatch() {

            let input = prompt("请输入 limit (建议 1 ~ 5 ,默认 3)")
            let counter = 0;
            let limit = input == '' ? 3 : Number(input);
            if (isNaN(limit)) {
                alert("输入无效!");
                return;
            }

            let res = [];

            let els = document
                .querySelector('.right-content')
                .querySelectorAll('li.concrete-tr')

            for (const el of els) {
                if (el.querySelector('.complete-td>:first-child').innerText != "已完成") {
                    counter++;
                    res.push(el);
                    if (counter >= limit)
                        break;
                }
            }

            res.forEach(el => {
                el.querySelector('i+span').click();
            })
        }

        static FuckFocus() {
            document.hasFocus = el_psy_congroo => true;
        }

        static StartPlaying() {
            ElementPool.bigPlayBtn().click();
        }

        /** 更新进度信息的显示 */
        static UpdateProgress() {

            let prog = ElementPool.progress();

            if (prog == null) {
                document.title = "进度:<1%";
                ElementPool.rate().innerText = "暂未获取到视频进度";
                return;
            }

            let n = Number(prog.innerText.slice(4, -1));
            document.title = "进度:" + n + "%";

            if (n >= 100) {
                document.title = "已完成!";
                setTimeout(Utils.CloseWindow, 500);
            }

        }


        /** 转动分数之环(ring) */
        static RotateCircle() {

            Utils.TryTryTry(() => {

                let isCssDone = Utils.InsertCSS(document.body, `
                    circle {
                        animation: steins-gate-circle 1s infinite cubic-bezier(0.5,-0.52, 0.44, 1.61);
                    }

                    @keyframes steins-gate-circle {
                        from {
                            transform: rotate(0deg);
                            -webkit-transform: rotate(0deg);
                        }
                        to {
                            transform: rotate(360deg);
                            -webkit-transform: rotate(360deg);
                        }
                }
                `)

                return isCssDone;
            });

        }

    }


    /**
     * DOM 元素池,使用方法如下:
     * - 取用元素:**作为函数调用**,例如:`ElementPool.title()`;
     * - 添加元素:为常用的 DOM 元素起别名作为属性名,并将
     *  `SelectOne` 或 `SelectAll` 对象作为属性的初始值
    */
    var ElementPool = {

        /** 视频标题,用于显示“已黑化” */
        title: Utils.SelectOne('.title-fl'),

        /** “已黑化”的插槽,用于在标题旁显示进度 */
        rate: Utils.SelectOne('#sg-rate'),

        /** “完成度”,用于获取观看进度 */
        progress: Utils.SelectOne('.el-tooltip>.text', false),

        /** 大播放按钮 */
        bigPlayBtn: Utils.SelectOne('.pause_show>.xt_video_bit_play_btn', false),

        /** 分数环 */
        circle: Utils.SelectOne('.progress-ring>circle'),

        /** 分数页面跳转的按钮 */
        scorePageBtn: Utils.SelectOne('.student-tabs1>:nth-child(4)'),
    };

    // TODO: Globalize
    let env;
    let updClock;

    /** 全てはこれから */
    function Init() {

        env = Utils.GetEnvironment();

        console.log("EnvState: " + env + "\nEnum: ", Utils.Environment);

        // 课程视频页面
        if (env == Utils.Environment.VIDEO) {

            Modules.FuckFocus();

            Modules.ShowBlacklized();

            updClock = setInterval(Modules.UpdateProgress, 1000);

        };

        // 课程分数页面
        if (env == Utils.Environment.SCORE) {

            Modules.RotateCircle();

            Utils.TryTryTry(
                () => {
                    ElementPool
                        ?.circle()
                        .addEventListener('click', Modules.MultiWatch);
                    return ElementPool.circle() != null;
                }
            );


        }

        // 主界面
        if (env == Utils.Environment.UNKNOWN) {

            // 更新环境
            setInterval(() => {
                let cur = Utils.GetEnvironment();
                if (cur != env) {
                    clearInterval(updClock);
                    Init();
                }
            }, 1000);

        }

    }

    Init();

    // Your code here...
})();