Skip to content

Instantly share code, notes, and snippets.

@cuylerstuwe
Last active March 10, 2019 22:16
Show Gist options
  • Select an option

  • Save cuylerstuwe/829583df66be530d775ab4ad45ac6e20 to your computer and use it in GitHub Desktop.

Select an option

Save cuylerstuwe/829583df66be530d775ab4ad45ac6e20 to your computer and use it in GitHub Desktop.
// ==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>&nbsp;</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