Greasy Fork is available in English.

Show Udemy Time Remaining

Gets time remaining for any modules in a Udemy course

// ==UserScript==
// @name         Show Udemy Time Remaining
// @namespace    Namespace (usually a URL)
// @version      1.0.0
// @description  Gets time remaining for any modules in a Udemy course
// @author       lundeen-bryan
// @match        https://www.udemy.com/*
// @grant        GM_registerMenuCommand
// @icon         URL of an icon for the script
// @license      License information (e.g., MIT, GPL)
// ==/UserScript==

/**
 *
 * Name..............: Show Udemy Time Remaining
 * Description.......: This script enhances Udemy course pages by calculating and displaying the total remaining time for uncompleted modules.
 *                     It automatically identifies unwatched course content and sums up the durations, providing a quick overview of the time
 *                     required to complete the course. Additionally, it adds convenient buttons to the course interface for expanding all
 *                     sections, collapsing all sections, and showing the total remaining time.
 * Syntax............: The script operates automatically on Udemy course overview pages that contain '#overview' in the URL. It also provides
 *                     buttons for manual control. (Currently needs user to click the button * in the Udemy menu)
 * Parameters........: None.
 * Return data type..: Void. The script directly modifies the webpage content without returning data.
 * Links.............: n/a
 * Author............: lundeen-bryan
 * Related...........: This script is specific to Udemy courses and is designed to work with the current structure of Udemy's course content
 *                     pages as of the script's last update.
 * Example...........: To see the script in action, navigate to a Udemy course overview page (the URL should contain '#overview') and observe
 *                     the added "Remaining Time" display and control buttons. Manual triggers are available through the Tampermonkey extension
 *                     menu under "Activate Udemy Time Tracker."
 *
 */

(function () {
  "use strict";

  function initializeScript() {
    "use strict";

    // Check if the URL fragment includes '#overview'
    if (!location.hash.includes("overview")) {
      console.log("Not on the overview page, script will not run.");
      return; // Exit the script if not on the overview page
    }

    // Call main function
    addScriptButton();
    fetchAndDisplayCourseData();
  }

  function getMetaContentByName(name) {
    const element = document.querySelector(`meta[name="${name}"]`);
    return element && element.getAttribute("content");
  }

  function storeCourseMetadata(data) {
    localStorage.setItem("courseMetadata", JSON.stringify(data));
  }

  function expandUnopenedSections() {
    const unopenedSections = [];
    document
      .querySelectorAll(".section--section--yXfqc > span")
      .forEach((section) => {
        const isExpanded = section.getAttribute("data-checked") === "checked";
        if (!isExpanded) {
          const accordionTitle = section.parentNode.querySelector(
            ".ud-accordion-panel-heading"
          );
          if (accordionTitle) {
            accordionTitle.click();
            unopenedSections.push(section);
          } else {
            console.warn(
              "Could not find the accordion panel heading for one of the sections. The website structure might have changed."
            );
          }
        }
      });
    return unopenedSections;
  }

  function calculateTotalMinutes() {
    let totalMinutes = 0;
    document
      .querySelectorAll(".item-link.ud-custom-focus-visible")
      .forEach((item) => {
        const isChecked = item.querySelector(".ud-real-toggle-input").checked;
        if (!isChecked) {
          const timer = item.querySelector(".ud-text-xs span");
          if (timer) {
            let time = parseInt(timer.innerText.replace("min", "").trim(), 10);
            totalMinutes += isNaN(time) ? 0 : time;
          }
        }
      });
    return totalMinutes;
  }

  function convertDuration(totalMinutes) {
    const hours = String(Math.trunc(totalMinutes / 60)).padStart(2, "0");
    const minutes = String(totalMinutes % 60).padStart(2, "0");
    return { hours, minutes };
  }

  function collapseSections(sections) {
    sections.forEach((section) => {
      const accordionTitle = section.parentNode.querySelector(
        ".ud-accordion-panel-heading"
      );
      if (accordionTitle) {
        accordionTitle.click();
      } else {
        console.warn(
          "Could not find the accordion panel heading for one of the sections while trying to collapse. The website structure might have changed."
        );
      }
    });
  }

  function insertRemainingDuration(totalMinutes) {
    const { hours, minutes } = convertDuration(totalMinutes);
    const displayArea = document.querySelector(
      'dd[data-purpose="course-additional-stats"]'
    );
    if (displayArea) {
      const existingVideoDuration = displayArea.querySelector(
        "div:nth-child(2)"
      );
      const existingRemainingTimeElement = displayArea.querySelector(
        ".remaining-time"
      );

      if (existingRemainingTimeElement) {
        existingRemainingTimeElement.textContent = `Remaining Time: ${hours}:${minutes}`;
      } else {
        const remainingTimeElement = document.createElement("div");
        remainingTimeElement.textContent = `Remaining Time: ${hours}:${minutes}`;
        remainingTimeElement.className = "remaining-time";
        remainingTimeElement.style.backgroundColor = "yellow"; // Set the background color to yellow
        existingVideoDuration.insertAdjacentElement(
          "afterend",
          remainingTimeElement
        );
      }
    }
  }

  function addScriptButton() {
    const statsSection = document.querySelector(
      'dd[data-purpose="course-additional-stats"]'
    );
    if (!statsSection) return;

    const buttonContainer = document.createElement("div");
    buttonContainer.style.marginTop = "10px";

    // Button for Remaining Time
    const remainingTimeButton = document.createElement("button");
    remainingTimeButton.id = "courseMetadataButton";
    remainingTimeButton.textContent = "Remaining Time";
    remainingTimeButton.style.backgroundColor = "#007bff";
    remainingTimeButton.style.color = "#fff";
    remainingTimeButton.style.border = "none";
    remainingTimeButton.style.padding = "10px";
    remainingTimeButton.style.cursor = "pointer";
    remainingTimeButton.style.display = "block"; // ensures each button takes up the full width of the container
    remainingTimeButton.style.marginBottom = "5px"; // adds a little space between buttons
    remainingTimeButton.addEventListener("click", () => {
      fetchAndDisplayCourseData();
    });
    buttonContainer.appendChild(remainingTimeButton);

    // Button for Expand All
    const expandAllButton = document.createElement("button");
    expandAllButton.textContent = "Expand All";
    expandAllButton.style.backgroundColor = "#008000";
    expandAllButton.style.color = "#fff";
    expandAllButton.style.border = "none";
    expandAllButton.style.padding = "10px";
    expandAllButton.style.cursor = "pointer";
    expandAllButton.style.display = "block"; // ensures each button takes up the full width of the container
    expandAllButton.style.marginBottom = "5px"; // adds a little space between buttons
    expandAllButton.addEventListener("click", () => {
      expandAllSections();
    });
    buttonContainer.appendChild(expandAllButton);

    // Button for Collapse All
    const collapseAllButton = document.createElement("button");
    collapseAllButton.textContent = "Collapse All";
    collapseAllButton.style.backgroundColor = "#ff0000";
    collapseAllButton.style.color = "#fff";
    collapseAllButton.style.border = "none";
    collapseAllButton.style.padding = "10px";
    collapseAllButton.style.cursor = "pointer";
    collapseAllButton.style.display = "block"; // ensures each button takes up the full width of the container
    collapseAllButton.addEventListener("click", () => {
      collapseAllSections();
    });
    buttonContainer.appendChild(collapseAllButton);

    statsSection.parentNode.insertBefore(
      buttonContainer,
      statsSection.nextSibling
    );
  }

  function expandAllSections() {
    document
      .querySelectorAll(".section--section--yXfqc > span")
      .forEach((section) => {
        const isExpanded = section.getAttribute("data-checked") === "checked";
        if (!isExpanded) {
          const accordionTitle = section.parentNode.querySelector(
            ".ud-accordion-panel-heading"
          );
          if (accordionTitle) {
            accordionTitle.click();
          } else {
            console.warn(
              "Could not find the accordion panel heading for one of the sections. The website structure might have changed."
            );
          }
        }
      });
  }

  function collapseAllSections() {
    document
      .querySelectorAll(".section--section--yXfqc > span")
      .forEach((section) => {
        const isExpanded = section.getAttribute("data-checked") === "checked";
        if (isExpanded) {
          const accordionTitle = section.parentNode.querySelector(
            ".ud-accordion-panel-heading"
          );
          if (accordionTitle) {
            accordionTitle.click();
          } else {
            console.warn(
              "Could not find the accordion panel heading for one of the sections while trying to collapse. The website structure might have changed."
            );
          }
        }
      });
  }

  function fetchAndDisplayCourseData() {
    const title = getMetaContentByName("twitter:title");
    const url = getMetaContentByName("twitter:url");
    const description = getMetaContentByName("twitter:description");
    const totalMinutes = calculateTotalMinutes();

    const courseMetadata = {
      courseTitle: title,
      courseURL: url,
      courseDescription: description,
      remainingDuration: totalMinutes,
    };

    storeCourseMetadata(courseMetadata);
    insertRemainingDuration(totalMinutes);

    const sectionsToCollapse = expandUnopenedSections();
    collapseSections(sectionsToCollapse);
  }

  // Automatically attempt to run the script on page load
  initializeScript();

  // Register a Tampermonkey menu command to manually trigger the script
  GM_registerMenuCommand("Activate Udemy Time Tracker", initializeScript, "t");
})();