Better OriCOCKs

Изменение подсчёта баллов и местами дизайна, а также добавление/доработка расписания

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 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         Better OriCOCKs
// @version      3.0.8
// @description  Изменение подсчёта баллов и местами дизайна, а также добавление/доработка расписания
// @source       https://github.com/Psychosoc1al/better-oricocks
// @author       Antonchik
// @license      MIT
// @namespace    https://github.com/Psychosoc1al
// @match        https://orioks.miet.ru/*
// @icon         https://orioks.miet.ru/favicon.ico
// @run-at       document-body
// @connect      miet.ru
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// ==/UserScript==

(() => {
    "use strict";

    /**
     * Changes the body width to make the interface wider and more readable.
     * Separate function for convenience to be used on different pages
     */
    const changeBodyWidth = function () {
        for (const sheet of document.styleSheets)
            if (
                sheet.href?.includes(
                    "https://orioks.miet.ru/libs/bootstrap/bootstrap.min.css",
                )
            ) {
                for (const element of sheet.cssRules)
                    if (element.cssText.includes("1170px"))
                        element["cssRules"][0].style.width = "1330px";
                return;
            }
    };

    /**
     * Save a key-value pair to the storage
     *
     * @param {string} key - The key to save
     * @param {string | Object} value - The value to save
     */
    const saveKeyValue = function (key, value) {
        // noinspection JSUnresolvedReference
        GM.setValue(key, value);
    };

    /**
     * Retrieves the value associated with the given key
     *
     * @param {string} key - The key to retrieve the value for
     * @return {Promise<string>} - The value associated with the given key
     */
    const loadValueByKey = function (key) {
        // noinspection JSUnresolvedReference,JSCheckFunctionSignatures
        return GM.getValue(key);
    };

    // check to know if we are on the page with grades
    if (document.URL.includes("student/student")) {
        const group = document
            .querySelector('select[name="student_id"] option[selected]')
            .innerText.split(" ")[0];
        const weeksNumbers = {
            "1 числитель": 0,
            "1 знаменатель": 1,
            "2 числитель": 2,
            "2 знаменатель": 3,
        };

        /**
         * Sends a request to the schedule server
         *
         * @param {string} url - The URL to send the request to
         * @param {string} method - The request method
         * @param {string} cookie - The cookie to include in the request headers
         * @return {Promise<Object>} A promise that resolves with the response text
         */
        const sendRequest = function (url, method, cookie = "") {
            // noinspection JSUnresolvedReference,JSUnusedGlobalSymbols
            return GM.xmlHttpRequest({
                url: url,
                method: method,
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                    Cookie: cookie,
                },
                data: `group=${group}`,
                onload: function (responsePromise) {
                    return responsePromise;
                },
                onerror: function (response) {
                    console.log(response);
                },
            });
        };

        /**
         * Adjusts a number to be integer if possible and rounded to at most 2 decimal places if not
         *
         * @param {number} number - The number to be adjusted
         * @return {string} The adjusted number as a string
         */
        const numberToFixedString = function (number) {
            if (!number) return "0";

            let stringedNumber = number.toFixed(2);

            while (stringedNumber.endsWith("0"))
                stringedNumber = stringedNumber.slice(0, -1);

            if (stringedNumber.endsWith("."))
                stringedNumber = stringedNumber.slice(0, -1);

            return stringedNumber;
        };

        /**
         * Gets the grade string representation and its type (projection to five-ball system)
         *
         * @param {number} gradeRatio - the grade ratio (grade / maxGrade)
         * @param {string} controlForm - the control type to check if it is a credit
         *
         * @return {[string, number]} The new grade class as a string
         */
        const getGradeNameAndType = function (gradeRatio, controlForm) {
            const isCredit = controlForm === "Зачёт";

            if (gradeRatio < 0.5) {
                if (gradeRatio < 0.2) return ["Не зачтено", 1];
                return ["Не зачтено", 2];
            } else if (gradeRatio < 0.7)
                return isCredit ? ["Зачтено", 5] : ["Удовлетворительно", 3];
            else if (gradeRatio < 0.86)
                return isCredit ? ["Зачтено", 5] : ["Хорошо", 4];
            else return isCredit ? ["Зачтено", 5] : ["Отлично", 5];
        };

        /**
         * Changes the size of numeric and string grade fields
         */
        const changeGradeFieldsSizes = function () {
            for (const sheet of document.styleSheets)
                if (
                    sheet.href?.includes(
                        "https://orioks.miet.ru/controller/student/student.css",
                    )
                ) {
                    for (const element of sheet.cssRules) {
                        if (element.selectorText === ".w46")
                            element.style.width = "34px";
                        if (
                            [".grade", "#bp"].includes(element["selectorText"])
                        ) {
                            element.style.width = "45px";
                            element.style.padding = "3px";
                        }
                    }
                    break;
                }
            document.querySelector('span[style="width: 60px"]').style.width =
                "fit-content";
        };

        /**
         * Sets the schedule CSS.
         */
        const setScheduleCSS = function () {
            for (const sheet of document.styleSheets)
                if (
                    sheet.href?.includes(
                        "https://orioks.miet.ru/libs/bootstrap/bootstrap.min.css",
                    )
                ) {
                    for (const element of sheet.cssRules)
                        if (
                            element.cssText.startsWith(".table") &&
                            element.style &&
                            element.style.marginTop
                        ) {
                            element.style.marginTop = "5px";
                        }
                    break;
                }

            document
                .querySelectorAll('tr[ng-repeat="c in data"] span')
                .forEach((elem) => (elem.style["white-space"] = "pre-line"));
        };

        /**
         * Gets the schedule by sending a request and passing the protection(?) with setting the cookie
         *
         * @return {Promise<Object>} A JSON object containing the schedule
         */
        const getSchedule = function () {
            return sendRequest("https://miet.ru/schedule/data", "POST").then(
                (responseObject) => {
                    const cookie =
                        responseObject.responseText.match(/wl=.*;path=\//);
                    if (cookie)
                        return sendRequest(
                            "https://miet.ru/schedule/data",
                            "POST",
                            cookie[0],
                        ).then((responseObject) =>
                            JSON.parse(responseObject.responseText),
                        );

                    return JSON.parse(responseObject.responseText);
                },
            );
        };

        /**
         * Parses the schedule data received from the server
         *
         * @return {Promise<Array<Object>>} An array of parsed and formatted schedule elements
         */
        const parseSchedule = function () {
            return getSchedule().then((responseJSON) => {
                const parsedSchedule = [];

                for (const responseJSONElement of responseJSON["Data"]) {
                    const scheduleElement = {};

                    scheduleElement["name"] =
                        responseJSONElement["Class"]["Name"];
                    scheduleElement["teacher"] =
                        responseJSONElement["Class"]["TeacherFull"];
                    scheduleElement["dayNumber"] = responseJSONElement["Day"];
                    scheduleElement["weekNumber"] =
                        responseJSONElement["DayNumber"];
                    scheduleElement["room"] =
                        responseJSONElement["Room"]["Name"];
                    scheduleElement["lessonNumber"] =
                        responseJSONElement["Time"]["Time"];
                    scheduleElement["startTime"] = new Date(
                        responseJSONElement["Time"]["TimeFrom"],
                    ).toLocaleTimeString("ru", {
                        hour: "2-digit",
                        minute: "2-digit",
                    });
                    scheduleElement["endTime"] = new Date(
                        responseJSONElement["Time"]["TimeTo"],
                    ).toLocaleTimeString("ru", {
                        hour: "2-digit",
                        minute: "2-digit",
                    });

                    parsedSchedule.push(scheduleElement);
                }

                return parsedSchedule;
            });
        };

        /**
         * Updates the schedule and processes it
         */
        const processSchedule = function () {
            loadValueByKey("schedule").then((schedule) => {
                parseSchedule().then((parsedSchedule) => {
                    saveKeyValue("schedule", parsedSchedule);
                    if (!schedule) window.location.reload();
                });

                if (schedule) {
                    const parsedSchedule = JSON.parse(JSON.stringify(schedule));
                    const closestLessons = getClosestLessons(parsedSchedule);
                    setSchedule(closestLessons);
                }
            });
        };

        /**
         * Sets the schedule based on the current time and day or on finds the closest lessons
         *
         * @param {Object} schedule - The whole schedule object
         * @param {number} daysOffset - The offset in days from the current day to start search
         * @param {boolean} weekChanged - Whether the week has changed while searching the closest day
         * @return {Object[]} The closest two days lessons list
         */
        const getClosestLessons = function (
            schedule,
            daysOffset = 0,
            weekChanged = false,
        ) {
            let currentTime, currentDayNumber;
            let date = new Date();
            let utcDate = new Date(
                date.getTime() + date.getTimezoneOffset() * 60 * 1000,
            );
            date = new Date(utcDate.getTime() + 3 * 60 * 60 * 1000);

            if (daysOffset === 0) {
                currentTime = date.toLocaleTimeString("ru", {
                    hour: "2-digit",
                    minute: "2-digit",
                    hour12: false,
                });
                currentDayNumber = date.getDay();
            } else {
                date.setDate(date.getDate() + daysOffset);

                currentTime = "00:00";
                currentDayNumber = date.getDay();
            }

            let stringCurrentWeek = document
                .querySelector(".small")
                .innerText.split("\n")[1];
            if (!stringCurrentWeek)
                stringCurrentWeek = document
                    .querySelector(".small")
                    .innerText.split(" ")
                    .slice(3)
                    .join(" ");
            let searchWeekNumber = weeksNumbers[stringCurrentWeek];
            let searchDayNumber = currentDayNumber - 1;
            let closestLessons = [];
            let nextOffset = daysOffset;

            if (typeof searchWeekNumber === "undefined") return [];

            if (currentDayNumber === 0) {
                searchWeekNumber = ++searchWeekNumber % 4;
                searchDayNumber = 0;
                nextOffset++;
                weekChanged = true;
            } else if (weekChanged) searchWeekNumber = ++searchWeekNumber % 4;

            while (!closestLessons.length) {
                searchDayNumber = ++searchDayNumber % 7;
                nextOffset++;
                if (searchDayNumber === 0) {
                    searchWeekNumber = ++searchWeekNumber % 4;
                    searchDayNumber = 1;
                    nextOffset++;
                    weekChanged = true;
                }

                closestLessons = schedule.filter(
                    (lesson) =>
                        lesson.dayNumber === searchDayNumber &&
                        lesson.weekNumber === searchWeekNumber &&
                        (currentDayNumber === searchDayNumber
                            ? lesson.endTime >= currentTime
                            : true) &&
                        !lesson.teacher.includes("УВЦ"),
                );
            }

            closestLessons.sort((a, b) => {
                return a.lessonNumber > b.lessonNumber ? 1 : -1;
            });

            date = new Date();
            date.setDate(date.getDate() + nextOffset - 1);
            const stringDate = date.toLocaleDateString("ru", {
                weekday: "long",
                day: "2-digit",
                month: "2-digit",
            });

            if (daysOffset === 0)
                return [
                    {
                        date: stringDate,
                        lessons: closestLessons,
                    },
                ].concat(getClosestLessons(schedule, nextOffset, weekChanged));
            return [
                {
                    date: stringDate,
                    lessons: closestLessons,
                },
            ];
        };

        /**
         * Updates the grade fields based on the newest data
         */
        const updateGrades = function () {
            const source = document.querySelector("#forang");
            const jsonData = JSON.parse(source.textContent);
            const disciplines = jsonData["dises"];

            for (const element of disciplines) {
                const controlPoints = element["segments"][0]["allKms"];
                const grade = element["grade"];
                const controlForm = element["formControl"]["name"];
                const maxPossibleSum = element["mvb"];
                let sum = 0;

                for (const element of controlPoints) {
                    const balls = element["balls"][0];

                    if (balls && balls["ball"] > 0) sum += balls["ball"];
                }

                grade["b"] = numberToFixedString(sum); // current ball
                grade["p"] = numberToFixedString((sum / maxPossibleSum) * 100); // current percentage
                // [maximal grade ("из ..."), class attribute for coloring]
                [grade["w"], grade["o"]] = getGradeNameAndType(
                    sum / maxPossibleSum,
                    controlForm,
                );
            }

            source.textContent = JSON.stringify(jsonData);
        };

        /**
         * Collapses multiplied lessons with the same name into one
         *
         * @param closestDays - The list of closest days with lessons (see {@link getClosestLessons()})
         * @return {Object[]} The list of closest days with refactored lessons
         */
        const collapseDuplicatedLessons = function (closestDays) {
            for (const day of closestDays) {
                const collapsedLessons = [];
                let currentLesson;
                let currentLessonNumber = 0;
                let lessonCount = 1;

                for (let i = 0; i < day.lessons.length; i++)
                    if (day.lessons[i].name === day.lessons[i + 1]?.name)
                        lessonCount++;
                    else {
                        if (lessonCount > 1) {
                            currentLesson = day.lessons[currentLessonNumber];
                            let name = currentLesson.name;
                            let amountPart = `(${lessonCount} пар${
                                lessonCount < 5 ? "ы" : ""
                            })`;

                            if (name.indexOf("[") !== -1)
                                name = name.replace("[", amountPart + " [");
                            else name += amountPart;

                            currentLesson.name = name;
                            collapsedLessons.push(currentLesson);
                        } else
                            collapsedLessons.push(
                                day.lessons[currentLessonNumber],
                            );

                        currentLessonNumber += lessonCount;
                        lessonCount = 1;
                    }

                day.lessons = collapsedLessons;
            }

            return closestDays;
        };

        /**
         * Sets the schedule based on the closest lessons
         *
         * @param closestDays - The list of closest days with lessons (see {@link getClosestLessons()})
         */
        const setSchedule = function (closestDays) {
            const source = document.querySelector("#forang");
            const jsonData = JSON.parse(source.textContent);
            const schedule = [];

            closestDays = collapseDuplicatedLessons(closestDays);
            for (let i = 0; i < closestDays.length; i++) {
                schedule[i] = [];
                schedule[i][0] = closestDays[i].date;
                schedule[i][1] = [];

                for (const lesson of closestDays[i].lessons) {
                    let lessonName, lessonType;
                    let lessonTypeMatch = lesson.name.match(/\[(.*)]/);

                    if (lessonTypeMatch) {
                        lessonName = lesson.name.match(/(.*) \[?/)[1];
                        lessonType = lessonTypeMatch[1];
                    } else {
                        lessonName = lesson.name;
                        lessonType = "";
                    }

                    schedule[i][1].push({
                        name: `${lessonName}
                            ► ${lesson.teacher}
                            `,
                        type: lessonType,
                        location: lesson.room,
                        time:
                            lesson.startTime === "12:00"
                                ? "12:00/30"
                                : lesson.startTime,
                    });
                }
            }

            jsonData["schedule"] = schedule;
            source.textContent = JSON.stringify(jsonData);
        };

        /**
         * Executes the necessary actions when the page is opened.
         */
        const onPageOpen = function () {
            updateGrades();
            processSchedule();

            changeGradeFieldsSizes();
            changeBodyWidth();
            setScheduleCSS();
        };

        onPageOpen();
    } else if (document.URL.includes("orioks.miet.ru")) changeBodyWidth();
})();