Greasy Fork is available in English.

网易云课堂/腾讯课堂课时计算

右边标题栏显示当前课程总课时(如果没有显示成功,请点击标题栏刷新)

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==
// @name         网易云课堂/腾讯课堂课时计算
// @namespace    dawsonenjoy_course_count
// @version      0.0.1
// @description  右边标题栏显示当前课程总课时(如果没有显示成功,请点击标题栏刷新)
// @author       dawsonenjoy
// @homepageURL  https://github.com/dawsonenjoy/tampermonkey_script
// @match        https://study.163.com/course/*
// @match        https://ke.qq.com/course/*
// @run-at       document-body
// @grant        none
// ==/UserScript==

(function () {
  "use strict";
  // Your code here...
  const config = {
    // 页面相关配置,可自定义
    pages: {
      // 相关配置参数:
      // nodes: 课程节点,
      // preprocess: 节点数据处理方式,
      // watcherNode: 监听节点选择器,
      // watcherConfig: 监听配置,
      // pageStyle: 对应页面样式,
      // ----------------------------------------------------------
      // 腾讯课堂
      qq: {
        nodes: ".tt-suffix",
        preprocess: node => parseFloat(node.innerText.slice(1, -3)) * 60 || 0,
        watcherNode: "#js_dir_tab",
        watcherConfig: { attributes: true }
      },
      // 网易云课堂
      "study.163": {
        nodes: ".kstime",
        preprocess: node => TimeUtils.timeToSecond(node.innerText || 0),
        watcherNode: "#j-chapter-list",
        watcherConfig: { childList: true }
      }
      // ----------------------------------------------------------
    },
    // 默认配置
    defaultConfig: {
      nodes: "",
      watcherNode: "",
      watcherConfig: {},
      preprocess: node => parseFloat(node.innerText) || 0,
      pageStyle: ``
    },
    // 获取配置属性
    getConfig(attr) {
      return this.pageConfig[attr] || this.defaultConfig[attr];
    },
    // 属性缓存
    getCacheAttr(attr, getMethod) {
      if (this[`_${attr}`] !== undefined) return this[`_${attr}`];
      this[`_${attr}`] = getMethod instanceof Function && getMethod();
      return this[`_${attr}`];
    },
    // 获取页面配置
    get pageConfig() {
      return this.getCacheAttr("pageConfig", () => {
        for (let page in this.pages) {
          if (location.href.includes(page)) return this.pages[page];
        }
        return this.defaultConfig;
      });
    },
    get nodes() {
      let nodes = this.getConfig("nodes");
      if (!nodes) return [];
      return Array.from(document.querySelectorAll(nodes));
    },
    get preprocess() {
      return this.getConfig("preprocess");
    },
    get watcherNode() {
      let watcherNode = this.getConfig("watcherNode");
      if (!watcherNode) return "";
      return document.querySelector(watcherNode);
    },
    get watcherConfig() {
      return this.getConfig("watcherConfig");
    },
    get root() {
      return this.getCacheAttr("root", () =>
        document.querySelector(".time-bar")
      );
    },
    get body() {
      return this.getCacheAttr("body", () =>
        document.querySelector(".time-body")
      );
    },
    get close() {
      return this.getCacheAttr("close", () =>
        document.querySelector(".time-close")
      );
    }
  };

  // 展示的标题栏
  const timeBar = {
    // 所有要生成的元素配置
    element: {
      styleElement: {
        tag: "style",
        innerHTML: `
          div.time-bar {
              position: fixed;
              top: 100px;
              left: 0px;
              height: 50px;
              width: 230px;
              display: flex;
              justify-content: center;
              align-items: center;
              background: #303541 !important;
              color: white;
              border-radius: 3px;
              font-size: 1.5em;
              cursor: pointer;
              z-index: 999;
          }
          div.time-bar:hover {
              opacity: 0.8;
          }
          .time-body {
              background: inherit;
              padding: 0;
          }
          .time-close {
            position: absolute;
            top: 0;
            right: 2px;
            margin-top: -2px;
            color: white;
          }
          .time-close:hover {
            background: rgba(255, 255, 255, .3)
          }
          `,
        parent: "head"
      },
      divElement: {
        tag: "div",
        className: "time-bar",
        title: "点击刷新",
        innerHTML: `<div class="time-body">总时长:</div>
                    <div class="time-close">×</div>`
      }
    },
    // 排除映射的属性
    excludes: ["parent", "tag"],
    // 关闭bar
    closeTime() {
      config.root && config.root.remove();
    },
    // 更新时间
    updateTime() {
      config.body.innerText = `总时长:${TimeUtils.getTimes()}`;
    },
    // 自动更新时间配置
    watcher() {
      let node = config.watcherNode;
      let watcherConfig = config.watcherConfig;
      if (!node || Object.keys(watcherConfig).length < 1) return;
      let MutationObserver =
        window.MutationObserver ||
        window.WebKitMutationObserver ||
        window.MozMutationObserver;
      let observer = new MutationObserver(() => this.updateTime());
      observer.observe(node, watcherConfig);
    },
    // 刷新和关闭事件
    bindEvent() {
      config.root.addEventListener("click", e => this.updateTime());
      config.close.addEventListener("click", e => this.closeTime());
      this.watcher();
    },
    createElement(oElement) {
      let element = document.createElement(oElement.tag);
      for (let k in oElement) {
        if (this.excludes.includes(k)) continue;
        element[k] = oElement[k];
      }
      document[oElement.parent || "body"].appendChild(element);
    },
    createtimeBar() {
      for (let k in this.element) this.createElement(this.element[k]);
    },
    init() {
      this.createtimeBar();
      this.bindEvent();
    }
  };

  // 时间相关工具
  const TimeUtils = {
    // 获取时间
    getTimes() {
      let nodes = config.nodes;
      let preprocess = config.preprocess;
      if (!nodes) return 0;
      return this.timeSum(nodes.map(node => preprocess(node)));
    },
    // 计算总时长
    timeSum(times) {
      return this.timeFormat(times.reduce((pre, cur) => pre + cur));
    },
    // 格式转换:HH:MM:ss -> s
    timeToSecond(time) {
      if (!time.toString().includes(":")) return 0;
      let timeList = time
        .split(":")
        .map(
          (item, index, p) => parseFloat(item) * 60 ** (p.length - index - 1)
        );
      return timeList.reduce((pre, cur) => pre + cur);
    },
    // 格式转换:s -> HH:MM:ss
    timeFormat(time) {
      let { hour, minute, second } = this.getSecond(time);
      return [hour, minute, second].map(item => parseInt(item)).join(":");
    },
    // 获取小时和剩余秒数
    getHour(time) {
      let hour = Math.floor(time / 3600);
      return { hour: hour, remainSecond: time - hour * 3600 };
    },
    // 获取小时+分钟以及剩余秒数
    getMinute(time) {
      let oHour = this.getHour(time);
      let minute = Math.floor(oHour.remainSecond / 60);
      return {
        ...oHour,
        minute: minute,
        remainSecond: oHour.remainSecond - minute * 60
      };
    },
    // 获取小时+分钟+秒数
    getSecond(time) {
      let oMinute = this.getMinute(time);
      return {
        ...oMinute,
        second: oMinute.remainSecond
      };
    }
  };

  timeBar.init();
})();