Greasy Fork

Greasy Fork is available in English.

北化网课雨

never try to take over the world!

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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...
})();