// ==UserScript==
// @name Udemy - Improved Course Library
// @namespace https://github.com/TadWohlrapp
// @description Shows ratings and other additional info for all courses in your Udemy library
// @icon https://github.com/TadWohlrapp/Udemy-Improved-Library-Userscript/raw/main/icon.png
// @icon64 https://github.com/TadWohlrapp/Udemy-Improved-Library-Userscript/raw/main/icon64.png
// @author Tad Wohlrapp (https://github.com/TadWohlrapp)
// @homepageURL https://github.com/TadWohlrapp/Udemy-Improved-Library-Userscript
// @version 1.0.0
// @supportURL https://github.com/TadWohlrapp/Udemy-Improved-Library-Userscript/issues
// @match https://www.udemy.com/home/my-courses/*
// @compatible chrome Tested with Tampermonkey v4.11 and Violentmonkey v2.12.9
// @compatible firefox Tested with Greasemonkey v4.9
// @license MIT
// @downloadURL none
// ==/UserScript==
// ==OpenUserJS==
// @author Taddiboy
// ==/OpenUserJS==
(function () {
'use strict';
const i18n = loadTranslations();
const lang = getLang(document.documentElement.lang);
function fetchCourses() {
listenForArchiveToggle();
const courseContainers = document.querySelectorAll('[data-purpose="enrolled-course-card"]:not(.details-done)');
if (courseContainers.length == 0) { return; }
[...courseContainers].forEach((courseContainer) => {
const isPartialRefresh = courseContainer.classList.contains('partial-refresh');
const courseId = courseContainer.querySelector('.card--learning__image').href.replace('https://www.udemy.com/course-dashboard-redirect/?course_id=', '');
const courseCustomDiv = document.createElement('div');
courseCustomDiv.classList.add('card__custom', 'js-removepartial');
courseContainer.appendChild(courseCustomDiv);
courseContainer.classList.add('details-done');
courseContainer.classList.remove('partial-refresh');
// Add Link to course overview to options dropdown
const courseLinkLi = document.createElement('li');
courseLinkLi.setAttribute('role', 'presentation');
courseLinkLi.classList.add('custom-course-link', 'js-removepartial');
courseLinkLi.innerHTML = `
${i18n[lang].overview}
`;
const dropdownUl = courseContainer.querySelector('.dropdown-menu');
dropdownUl.appendChild(courseLinkLi);
// Find existing elements in DOM
const thumbnailDiv = courseContainer.querySelector('.card__image');
const detailsName = courseContainer.querySelector('.details__name');
const detailsInstructor = courseContainer.querySelector('.details__instructor');
const progressText = courseContainer.querySelector('.progress__text');
const progressBar = courseContainer.querySelector('.details__progress');
const startCourseText = courseContainer.querySelector('.details__start-course');
const detailsBottom = courseContainer.querySelector('.details__bottom');
// If progress made
if (progressText != null) {
// Add progress bar below thumbnail
const progressBarSpan = document.createElement('span');
progressBarSpan.classList.add('impr__progress-bar', 'js-removepartial');
progressBarSpan.innerHTML = progressBar.innerHTML;
thumbnailDiv.appendChild(progressBarSpan);
// Add progress percentage to thumbnail bottom right
const progressTextSpan = document.createElement('span');
progressTextSpan.classList.add('card__thumb-overlay', 'card__course-runtime', 'hover-show', 'js-removepartial');
progressTextSpan.innerHTML = progressText.innerHTML;
thumbnailDiv.appendChild(progressTextSpan);
// Remove existing progress percentage
progressText.parentNode.removeChild(progressText);
}
// Remove existing progress bar
if (!isPartialRefresh) {
progressBar.parentNode.removeChild(progressBar);
}
// If "START COURSE" exists, remove it. It's clutter
if (startCourseText != null) {
startCourseText.parentNode.removeChild(startCourseText);
}
if (!isPartialRefresh) {
// If instructor title exists, remove it as well
const instructorTitle = detailsInstructor.querySelector('span');
if (instructorTitle != null) {
instructorTitle.parentNode.removeChild(instructorTitle);
}
// Switch classes on course name and instructor
detailsName.classList.add('impr__name');
detailsName.classList.remove('details__name');
detailsInstructor.classList.add('impr__instructor');
detailsInstructor.classList.remove('details__instructor');
}
// If course page has draft status, do not even to fetch its data via API
if (courseContainer.querySelector('.card--learning__details').href.includes('/draft/')) {
if (!isPartialRefresh) {
detailsBottom.parentNode.removeChild(detailsBottom);
}
courseContainer.querySelector('.card__course-link').style.textDecoration = "line-through";
courseCustomDiv.classList.add('card__nodata');
courseCustomDiv.innerHTML += i18n[lang].notavailable;
// We're done with this course
return;
}
const fetchUrl = 'https://www.udemy.com/api-2.0/courses/' + courseId + '?fields[course]=rating,num_reviews,num_subscribers,content_length_video,last_update_date,locale,has_closed_caption,caption_languages,num_published_lectures';
fetch(fetchUrl)
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error(response.status);
}
})
.then(json => {
if (typeof json === 'undefined') { return; }
// Get everything from JSON and put it in variables
const rating = json.rating.toFixed(1);
const reviews = json.num_reviews;
const enrolled = json.num_subscribers;
const runtime = json.content_length_video;
const updateDate = json.last_update_date;
const locale = json.locale.title;
const localeCode = json.locale.locale;
const hasCaptions = json.has_closed_caption;
const captionsLangs = json.caption_languages;
// Format "Last updated" Date
let updateDateShort = '';
let updateDateLong = '';
if (updateDate) {
updateDateShort = updateDate.replace(/(\d{4})-(\d{2})-(\d{2})/, '$2\/$1');
updateDateLong = new Date(updateDate).toLocaleDateString(lang, { year: 'numeric', month: 'long', day: 'numeric' });
}
// Small helper for rating strip color
const getColor = v => `hsl(${(Math.round((1 - v) * 120))},100%,45%)`;
const colorValue = r => Math.min(Math.max((5 - r) / 2, 0), 1);
// If captions are available, create the tag for it. We'll add it in template string later
let captionsTag = '';
if (hasCaptions) {
const captionsString = captionsLangs.join('
');
captionsTag = `
`;
}
// Returns true or false depending if stars are visible
const isShowingStars = courseContainer.querySelector('.details__bottom--review');
// Now let's handle own ratings
// Set up empty html
let myRatingHtml = '';
let ratingButton;
let ratingOwn = 0;
// If ratings stars ARE visible, proceed to build own rating stars
if (isShowingStars != null) {
// If I have voted, count the stars and tell me how I voted
ratingOwn = countAllWidths(isShowingStars.querySelectorAll('[style]')); // between 0 and 5
// Find the rating-button, and remove its css class
ratingButton = isShowingStars.querySelector('[role="button"]');
// Remove the old stars from ratingButton
ratingButton.removeChild(ratingButton.querySelector('.star-rating-shell'));
// Build the html
myRatingHtml = `
${buildStars(ratingOwn)}
${setDecimal(ratingOwn, lang)}()
`;
}
const ratingStripColor = ratingOwn > 0 ? ratingOwn : rating;
let updateDateInfo = '';
if (updateDateShort !== '' && updateDateLong !== '') {
updateDateInfo = `