Show Udemy Time Remaining

Gets time remaining for any modules in a Udemy course

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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");
})();