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