Show Udemy Time Remaining

Gets time remaining for any modules in a Udemy course

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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");
})();