Last active
March 10, 2019 22:16
-
-
Save cuylerstuwe/829583df66be530d775ab4ad45ac6e20 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Udemy Course Section Time Estimates | |
| // @namespace salembeats | |
| // @version 3.3 | |
| // @description How long are these sections gonna take?! Latest update (3/10/19): Bugfixes for crashes caused by sections that contain zero video runtime (e.g., all quizzes/etc.) | |
| // @author Cuyler Stuwe (salembeats) | |
| // @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.1/moment.min.js | |
| // @require https://gist.github.com/salembeats/2661fe710d1520a50cb0f8fa8b23d052/raw/ec9a488a06227d8baf1729f30cfc3a985680e1fb/mutil.library.user.js | |
| // @include https://www.udemy.com/*/learn/v4/content | |
| // @grant none | |
| // ==/UserScript== | |
| const globals = { | |
| settings: { | |
| SECTION_MINUTES_ESTIMATE_COLOR: "orange", | |
| MESSAGE_ABOVE_SECTIONS_COLOR: "red", | |
| STANDARD_TIME_EXTENSION_FACTOR: 1.5, | |
| HOURS_TARGET_PER_DAY: 2, | |
| SECTION_COMPLETION_BACKGROUND_COLOR: "orange", | |
| SECTION_FINISHED_BACKGROUND_COLOR: "green", | |
| SECTION_COMPLETION_TEXT_COLOR: "white", | |
| SHOULD_USE_COMPLETION_ICONS: false, | |
| }, | |
| totalCourseMinutes: 0, | |
| totalCourseExtendedMinutes: 0, | |
| totalExtendedCourseTimeRoundedToHours: 0, | |
| estimatedTotalDays: 0 | |
| }; | |
| const summer = (accumulator, value) => accumulator + value; | |
| function secondsToMinutesAndSeconds(seconds) { | |
| return { | |
| minutes: Math.floor(seconds/60), | |
| seconds: seconds % 60 | |
| }; | |
| } | |
| function minutesToHoursAndMinutes(minutes) { | |
| return { | |
| hours: Math.floor(minutes/60), | |
| minutes: minutes % 60 | |
| }; | |
| } | |
| function toNearestQuarterHour(hours, minutes) { | |
| return { | |
| minutes: (((minutes + 7.5)/15 | 0) * 15) % 60, | |
| hours: ((((minutes/105) + .5) | 0) + hours) % 24 | |
| }; | |
| } | |
| async function waitForTimesToLoad(additionalPaddingMs = 1000) { | |
| await MU.$wait("[class^='course-content-dashboard--dashboard']"); | |
| await new Promise((resolve, reject) => { | |
| setTimeout(() => { | |
| resolve(); | |
| }, additionalPaddingMs); | |
| }); | |
| } | |
| function injectNewStyles() { | |
| document.body.insertAdjacentHTML("beforeend", ` | |
| <style> | |
| .message-above-sections { | |
| color: ${globals.settings.MESSAGE_ABOVE_SECTIONS_COLOR}; | |
| } | |
| .section-minutes-estimate { | |
| color: ${globals.settings.SECTION_MINUTES_ESTIMATE_COLOR}; | |
| } | |
| .section-completion-status { | |
| margin-top: 5px; | |
| color: ${globals.settings.SECTION_COMPLETION_TEXT_COLOR}; | |
| z-index: 2; | |
| } | |
| .userscript-progress-bar { | |
| z-index: -1; | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| height: 100%; | |
| background: ${globals.settings.SECTION_COMPLETION_BACKGROUND_COLOR}; | |
| } | |
| .userscript-progress-bar[style*='width: 100%'] { | |
| background: ${globals.settings.SECTION_FINISHED_BACKGROUND_COLOR}; | |
| } | |
| .userscript-progress-background { | |
| z-index: -2; | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| height: 100%; | |
| width: 100%; | |
| background: black; | |
| } | |
| </style> | |
| `); | |
| if(globals.settings.SHOULD_USE_COMPLETION_ICONS) { | |
| document.body.insertAdjacentHTML("beforeend", ` | |
| <style> | |
| .userscript-progress-bar[style*='width: 100%']::before { | |
| content: url("https://www.freeiconspng.com/uploads/finish-icon-finish-flag-goal-icon-1.png"); | |
| zoom: 10%; | |
| position: relative; | |
| left: 800px; | |
| top: -1100px; | |
| } | |
| .userscript-progress-bar:not([style*='width: 100%']):not([style='width: 0%;'])::before { | |
| content: url("http://www.letterskingdom.com/french/wip.png"); | |
| zoom: 15%; | |
| position: relative; | |
| left: 550px; | |
| top: -700px; | |
| } | |
| </style> | |
| `); | |
| } | |
| } | |
| function insertMessagesAboveSectionsAccordion(...messages) { | |
| let sectionsAccordion = document.querySelector("[class^='course-content-dashboard--dashboard']"); | |
| let htmlToInsert = ""; | |
| for(let message of messages) { | |
| htmlToInsert += `<div class="message-above-sections">${message}</div>`; | |
| } | |
| sectionsAccordion.insertAdjacentHTML("beforebegin", htmlToInsert + "<div> </div>"); | |
| } | |
| async function main() { | |
| await waitForTimesToLoad(); | |
| injectNewStyles(); | |
| let sections = document.querySelectorAll("[data-purpose^='section-panel']"); | |
| console.log(sections); | |
| for(let section of sections) { | |
| let titleElement = section.querySelector("[class*='section-heading']"); | |
| let timeElements = section.querySelectorAll("[class*='curriculum-item--duration']"); | |
| let times = Array.from(timeElements).filter(timeElement => timeElement.innerText !== "").map(timeElement => (timeElement || "").innerText.trim()); | |
| console.log(times); | |
| let completedTimes = Array.from(timeElements) | |
| .filter(timeElement => | |
| timeElement.nextElementSibling.className.includes("curriculum-item--is-completed") && | |
| timeElement.innerText !== "" | |
| ) | |
| .map(timeElement => | |
| (timeElement || "").innerText.trim() | |
| ); | |
| let completedTimesAsMoments = completedTimes.map(completedTime => moment(completedTime, "mm:ss")); | |
| console.log(completedTimesAsMoments); | |
| let totalCompletedMinutes = (completedTimesAsMoments || [moment("00:00", "mm:ss")]) | |
| .map(currentMoment => currentMoment.minutes()) | |
| .reduce(summer, 0); | |
| let totalCompletedSeconds = (completedTimesAsMoments || [moment("00:00", "mm:ss")]) | |
| .map(currentMoment => currentMoment.seconds()) | |
| .reduce(summer, 0); | |
| let timesAsMoments = times.map(time => moment(time, "mm:ss")); | |
| let minutes = timesAsMoments.map(currentMoment => currentMoment.minutes()); | |
| let seconds = timesAsMoments.map(currentMoment => currentMoment.seconds()); | |
| let totalMinutes = minutes.reduce(summer, 0); | |
| let totalSeconds = seconds.reduce(summer, 0); | |
| let {minutes: additionalMinutes, seconds: additionalSeconds} = secondsToMinutesAndSeconds(totalSeconds); | |
| totalMinutes += additionalMinutes + Math.round(additionalSeconds / 60); | |
| let totalMinutesPlusExtension = Math.round(totalMinutes * globals.settings.STANDARD_TIME_EXTENSION_FACTOR); | |
| let {hours: extendedHours, minutes: extendedMinutes} = minutesToHoursAndMinutes(totalMinutesPlusExtension); | |
| let roundedCompletionMinutes = totalCompletedMinutes + Math.round(totalCompletedSeconds / 60); | |
| let roundedCompletionPercentage = (+(roundedCompletionMinutes / totalMinutes).toFixed(2))*100; | |
| const {minutes: quarterHourMinutes, hours: quarterHourHours} = toNearestQuarterHour(extendedHours, extendedMinutes); | |
| titleElement.insertAdjacentHTML("afterend", ` | |
| <span class="section-minutes-estimate"> | |
| (~ ${totalMinutes} minutes) [@${globals.settings.STANDARD_TIME_EXTENSION_FACTOR}x: ${totalMinutesPlusExtension} minutes for section. Schedule ${quarterHourHours}:${quarterHourMinutes.toLocaleString("en-US", {minimumIntegerDigits: 2, maximumFractionDigits: 0})}] | |
| </span> | |
| <div class="section-completion-status" style="position: relative;"> | |
| Completed: ~${roundedCompletionMinutes} minutes (${roundedCompletionPercentage}%). | |
| <div class="userscript-progress-bar" style="width: ${roundedCompletionPercentage}%;"></div> | |
| <div class="userscript-progress-background"></div> | |
| </div> | |
| `); | |
| globals.totalCourseMinutes += totalMinutes; | |
| } | |
| globals.totalCourseExtendedMinutes = globals.totalCourseMinutes * globals.settings.STANDARD_TIME_EXTENSION_FACTOR; | |
| globals.totalExtendedCourseTimeRoundedToHours = Math.round(globals.totalCourseExtendedMinutes / 60); | |
| globals.estimatedTotalDays = Math.ceil(globals.totalExtendedCourseTimeRoundedToHours / globals.settings.HOURS_TARGET_PER_DAY); | |
| const additionalPercentage = Math.round((globals.settings.STANDARD_TIME_EXTENSION_FACTOR - 1) * 100); | |
| let courseTimeEstimateHoursMessage = `When setting aside an additional ${additionalPercentage}% of video runtime to complete exercises, this course looks like it will take ` + globals.totalCourseExtendedMinutes + " minutes to complete."; | |
| let courseTimeEstimateDaysMessage = "At " + globals.settings.HOURS_TARGET_PER_DAY + " hours per day, you should set aside " + globals.estimatedTotalDays + " days to complete this course."; | |
| insertMessagesAboveSectionsAccordion(courseTimeEstimateHoursMessage, courseTimeEstimateDaysMessage); | |
| } | |
| main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment