您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Extends the functionality of the CTU website
// ==UserScript== // @name CTU Extend // @namespace https://psushko.com // @homepage https://github.com/Pavel-Sushko/ctu-extend // @website https://psushko.com // @source https://github.com/Pavel-Sushko/ctu-extend // @version 0.2.0 // @description Extends the functionality of the CTU website // @author Pavel Sushko <[email protected]> // @license MIT // @match https://studentlogin.coloradotech.edu/* // @icon https://www.google.com/s2/favicons?sz=64&domain=coloradotech.edu // @grant GM_addElement // ==/UserScript== GM_addElement('script', { src: 'https://cdnjs.cloudflare.com/ajax/libs/mathjs/12.4.1/math.js', type: 'text/javascript', }); const PAGES = { home: /^https?:\/\/studentlogin\.coloradotech\.edu\/\?.+#\/home\/active\/all$/i, gradebook: /^https?:\/\/studentlogin\.coloradotech\.edu\/\?.+#\/class\/\d+\/gradebook$/i, degreePlan: /^https?:\/\/studentlogin\.coloradotech\.edu\/\?.+#\/portal\/my-program\/degree-plan$/i, }; // #region DOM Manipulation /** * Get the first element that contains the lookup string * * @param {String} tag The tag to search for * @param {String} lookupString The string to search for * @returns {Element} The first element that contains the lookup string */ const getElement = (tag, lookupString) => { for (const element of document.querySelectorAll(tag)) if (element.textContent.includes(lookupString)) return element; }; // Function to create a tooltip const createTooltip = (element, tooltipText) => { const tooltip = document.createElement('span'); tooltip.className = 'tooltip-text'; tooltip.innerText = tooltipText; element.style.position = 'relative'; element.appendChild(tooltip); element.onmouseover = () => { tooltip.style.visibility = 'visible'; tooltip.style.opacity = '1'; }; element.onmouseout = () => { tooltip.style.visibility = 'hidden'; tooltip.style.opacity = '0'; }; }; // Add CSS for tooltip const addTooltipStyles = () => { const style = document.createElement('style'); style.innerHTML = ` .tooltip-text { visibility: hidden; width: 120px; background-color: black; color: #fff; text-align: center; border-radius: 6px; padding: 5px 0; position: absolute; z-index: 1; bottom: 110%; /* Position the tooltip above the text */ left: 50%; margin-left: -60px; opacity: 0; transition: opacity 0.3s; } .tooltip-text::after { content: ''; position: absolute; top: 100%; /* At the bottom of the tooltip */ left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: black transparent transparent transparent; } .tooltip:hover .tooltip-text { visibility: visible; opacity: 1; } `; document.head.appendChild(style); }; const createId = (string) => string .toLowerCase() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s/g, '-') .replace(/-+/g, '-'); // #endregion // #region Page Handlers /** * Handles the home page */ const handleHome = async () => { addTooltipStyles(); const SELECTORS = { earnedCredit: '.credit-item-title', requiredCredit: '.credits-required', donut: 'pec-donut-chart', }; let creditsEarnedElement = document.querySelector(SELECTORS.earnedCredit); let creditsRequiredElement = document.querySelector(SELECTORS.requiredCredit); let donut = document.querySelector(SELECTORS.donut); while (!creditsEarnedElement || !creditsRequiredElement || !donut) { await new Promise((r) => setTimeout(r, 100)); creditsEarnedElement = document.querySelector(SELECTORS.earnedCredit); creditsRequiredElement = document.querySelector(SELECTORS.requiredCredit); donut = document.querySelector(SELECTORS.donut); } const result = math.evaluate(creditsEarnedElement.innerText + creditsRequiredElement.innerText) * 100; const resultPercentage = `${result.toFixed(2)}%`; createTooltip(donut, resultPercentage); // TODO: Create honours tracking }; /** * Handles the gradebook page */ const handleGradebook = async () => { const gradeThresholds = [ { grade: 'A', threshold: 94 }, { grade: 'A-', threshold: 90 }, { grade: 'B+', threshold: 86 }, { grade: 'B', threshold: 83 }, { grade: 'B-', threshold: 80 }, { grade: 'C+', threshold: 76 }, { grade: 'C', threshold: 73 }, { grade: 'C-', threshold: 70 }, { grade: 'D+', threshold: 65 }, { grade: 'D', threshold: 60 }, { grade: 'F', threshold: 0 }, ]; let addedPoints = 0; // #region Grade Calculations /** * Get the current absolute grade percentage * * @param {Number} earnedPoints * @param {Number} maxPoints * @returns {Number} The current absolute grade percentage */ const getPercentage = (earnedPoints, maxPoints) => (earnedPoints / maxPoints) * 100; /** * Get the letter grade based on the percentage * * @param {Number} percentage * @returns {String} The letter grade */ const getLetterGrade = (percentage) => gradeThresholds.find(({ threshold }) => percentage >= threshold)?.grade || 'Grade not found'; /** * Get the points needed to get an A * * @param {Number} earnedPoints * @param {Number} maxPoints * @returns {Number} The points needed to get an A */ const getPointsToA = (earnedPoints, maxPoints) => Math.max(0, maxPoints * (gradeThresholds[0].threshold / 100) - earnedPoints); // #endregion // #region Main /** * Get the points from the lookup string * * @param {String} lookupString * @param {Boolean} child * @returns {Number} The points from the lookup string */ const getPoints = async (lookupString, child) => { let span = getElement('span', lookupString); while (!span) { await new Promise((r) => setTimeout(r, 100)); span = getElement('span', lookupString); } let spanText = child ? span.querySelector('span').innerText : span.nextElementSibling.innerText; return Number(spanText.includes('N/A') ? '0' : spanText); }; // Define earnedPoints and maxPoints first let earnedPoints = await getPoints('Points Earned to Date:', true); let maxPoints = await getPoints('Total Points Possible in Course:'); /** * Append the grade to the page * * @param {Number} earnedPoints * @param {Number} maxPoints */ const appendGrade = () => { const gradeDiv = getElement('strong', 'Current Course Grade:').parentElement; const absoluteGradeDiv = gradeDiv.cloneNode(true); const percentage = getPercentage(earnedPoints, maxPoints); const grade = getLetterGrade(percentage); absoluteGradeDiv.querySelector('strong').innerText = 'Absolute Course Grade:'; absoluteGradeDiv.querySelector('span').innerText = `${grade} (${getPointsToA(earnedPoints, maxPoints).toFixed( 2 )} points to A)`; const bottomDiv = absoluteGradeDiv.querySelector('div'); bottomDiv.innerHTML = bottomDiv.innerHTML.replace(/\/\s[^<]+/, `/ ${maxPoints} `).replace(' to Date', ''); gradeDiv.after(absoluteGradeDiv); }; /** * Update the grade display when points are added/removed * * @param {Number} addedPoints * @param {Number} earnedPoints * @param {Number} maxPoints */ const updateGrade = (addedPoints) => { const updatedPercentage = getPercentage(earnedPoints + addedPoints, maxPoints); const updatedGrade = getLetterGrade(updatedPercentage); const absoluteGradeDiv = getElement('strong', 'Absolute Course Grade:').parentElement; absoluteGradeDiv.querySelector('span').innerText = `${updatedGrade} (${getPointsToA( earnedPoints + addedPoints, maxPoints ).toFixed(2)} points to A)`; }; /** * Add a checkbox to each row to allow adding/removing points to the grade * * @param {HTMLElement} row */ const addCheckbox = (row) => { const td = document.createElement('td'); const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = createId(row.firstElementChild.innerText); checkbox.checked = false; checkbox.onclick = () => { let assignmentEarnedPoints = Number(row.children[3].innerText.replace(/[^0-9.]/g, '')); let possiblePoints = Number(row.children[4].innerText.replace(/[^0-9.]/g, '')); if (assignmentEarnedPoints !== 0) possiblePoints -= assignmentEarnedPoints; addedPoints += checkbox.checked ? possiblePoints : -possiblePoints; // Update the grade updateGrade(addedPoints); }; td.appendChild(checkbox); row.prepend(td); }; /** * Add a new column to the grade table for checkboxes * * @param {HTMLElement} table * @param {String} header */ const addColumn = (table, header) => { const th = document.createElement('th'); th.innerText = header; table.querySelector('thead tr').prepend(th); for (const row of table.querySelectorAll('tbody tr')) { addCheckbox(row); } }; appendGrade(); addColumn(document.querySelector('table'), 'Add to Grade'); }; // #endregion // #endregion /** * Handles the degree plan page */ const handleDegreePlan = async () => { addTooltipStyles(); const SELECTORS = { earnedCredit: '.credit-item-title', requiredCredit: '.credits-required', donut: 'div.hide-on-mobile pec-donut-chart', }; let creditsEarnedElement = document.querySelector(SELECTORS.earnedCredit); let creditsRequiredElement = document.querySelector(SELECTORS.requiredCredit); let donut = document.querySelector(SELECTORS.donut); while (!creditsEarnedElement || !creditsRequiredElement) { await new Promise((r) => setTimeout(r, 100)); creditsEarnedElement = document.querySelector(SELECTORS.earnedCredit); creditsRequiredElement = document.querySelector(SELECTORS.requiredCredit); } const result = math.evaluate(creditsEarnedElement.innerText + creditsRequiredElement.innerText) * 100; const resultPercentage = `${result.toFixed(2)}%`; let creditsSpan = getElement('span', 'Credits Earned'); while (!creditsSpan) { await new Promise((r) => setTimeout(r, 100)); creditsSpan = getElement('span', 'Credits Earned'); } while (!donut) { await new Promise((r) => setTimeout(r, 100)); donut = document.querySelector(SELECTORS.donut); } creditsSpan.innerHTML += ` (${resultPercentage})`; createTooltip(donut, resultPercentage); }; /** * Handles the pages */ const handlePages = async () => { let prevPage = ''; while (true) { if (prevPage !== window.location.href) { switch (true) { case PAGES.home.test(window.location.href): await handleHome(); break; case PAGES.gradebook.test(window.location.href): await handleGradebook(); break; case PAGES.degreePlan.test(window.location.href): await handleDegreePlan(); break; default: break; } } prevPage = window.location.href; await new Promise((r) => setTimeout(r, 300)); } }; // #endregion /** * Entry point */ (async function () { handlePages(); })();