课程思政教学能力提升专题网络培训(改良)

课程思政教学能力提升专题网络培训倍速和自动播放

// ==UserScript==
// @name         课程思政教学能力提升专题网络培训(改良)
// @namespace    http://tampermonkey.net/
// @version      0.1.0
// @description  课程思政教学能力提升专题网络培训倍速和自动播放
// @author       You
// @match        https://study.enaea.edu.cn/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=enaea.edu.cn
// @grant        none
// @license MIT
// ==/UserScript==

(async function () {
  "use strict";
  async function waitElementLoaded(ele, max = 50, n = 1, m = "querySelector") {
    return new Promise((resolve, reject) => {
      let i = 0;
      let timer = setInterval(() => {
        let element = document[m](ele);
        console.log("find element: ", ele);
        if (element) {
          console.log("已找到指定元素: ", element);
          clearInterval(timer); // 停止重复计时
          resolve(element);
        } else {
          i++;
          console.log(`检测次数: ${i}`);
          if (i >= max) {
            console.log(`已检测超过 ${i} 次, 无法获取指定元素. `);
            clearInterval(timer); // 停止重复计时
            reject(null);
          }
        }
      }, n * 1000);
    });
  }
  async function courseDetail() {
    let speed = 2;

    async function waitTime(s = 3) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          return resolve();
        }, s * 1000);
      });
    }

    let currUrl = window.location.href;
    let url = currUrl.split("?")[0];

    function initialize() {
      const html = `
      <div id="acme-box">
          <button id="show" class="btn btn-outline-danger">
              <css-icon class="icon-arrow-up">
          </button>
          <div class="acme-card card">
              <div id="acme-card-header" title="可移动"
                  class="acme-card-header d-flex flex-row align-items-center card-header bg-danger text-white">
                  <css-icon class="icon-apps"></css-icon>
                  <span class="mx-2">控制面板</span>
                  <div class="flex-grow-1"></div>
                  <span id="restore" class="restore">
                      <css-icon class="icon-arrow-down"></css-icon>
                  </span>
              </div>
              <div class="acme-card-body card-body">

              <span>速度: </span>

              <select id="speedx" class="acme-speed">
              <option value="8.0">8.0x</option>
              <option value="6.0">6.0x</option>
              <option value="4.0">4.0x</option>
              <option value="3.0">3.0x</option>
              <option selected="selected"  value="2.0">2.0x</option>
              <option value="1.0">1.0x</option>
              <option value="0.5">0.5x</option>
          </select>

           <button id="set-speedx" class="btn btn-outline-danger">
                  设置播放速度
              </button>
              </div>
          </div>
      </div>`;
      const cssStyle = `
  @charset "UTF-8";


:root {
  --bs-blue: #0d6efd;
  --bs-indigo: #6610f2;
  --bs-purple: #6f42c1;
  --bs-pink: #d63384;
  --bs-red: #dc3545;
  --bs-orange: #fd7e14;
  --bs-yellow: #ffc107;
  --bs-green: #198754;
  --bs-teal: #20c997;
  --bs-cyan: #0dcaf0;
  --bs-white: #fff;
  --bs-gray: #6c757d;
  --bs-gray-dark: #343a40;
  --bs-gray-100: #f8f9fa;
  --bs-gray-200: #e9ecef;
  --bs-gray-300: #dee2e6;
  --bs-gray-400: #ced4da;
  --bs-gray-500: #adb5bd;
  --bs-gray-600: #6c757d;
  --bs-gray-700: #495057;
  --bs-gray-800: #343a40;
  --bs-gray-900: #212529;
  --bs-primary: #0d6efd;
  --bs-secondary: #6c757d;
  --bs-success: #198754;
  --bs-info: #0dcaf0;
  --bs-warning: #ffc107;
  --bs-danger: #dc3545;
  --bs-light: #f8f9fa;
  --bs-dark: #212529;
  --bs-primary-rgb: 13, 110, 253;
  --bs-secondary-rgb: 108, 117, 125;
  --bs-success-rgb: 25, 135, 84;
  --bs-info-rgb: 13, 202, 240;
  --bs-warning-rgb: 255, 193, 7;
  --bs-danger-rgb: 220, 53, 69;
  --bs-light-rgb: 248, 249, 250;
  --bs-dark-rgb: 33, 37, 41;
  --bs-white-rgb: 255, 255, 255;
  --bs-black-rgb: 0, 0, 0;
  --bs-body-color-rgb: 33, 37, 41;
  --bs-body-bg-rgb: 255, 255, 255;
  --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
  --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
  --bs-body-font-family: var(--bs-font-sans-serif);
  --bs-body-font-size: 1rem;
  --bs-body-font-weight: 400;
  --bs-body-line-height: 1.5;
  --bs-body-color: #212529;
  --bs-body-bg: #fff;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

#acme-box {
  position: absolute;
  z-index: 2147483647;
}

#acme-box>#show {
  position: fixed;
  bottom: 20px;
  right: 20px;
  width: 60px;
  height: 60px;
  text-align: center;
  border-radius: 50%;
}

#acme-box>.acme-card {
  display: none;
  width: 320px;
  height: 180px;
  position: fixed;
  bottom: 20px;
  right: 20px;
  font-size: 14px;
}

#acme-box>div>#acme-card-header:hover {
  cursor: grab;
}

#acme-box>div>#acme-card-header:active {
  cursor: grabbing;
}

#acme-box>div>div>#restore:hover {
  cursor: pointer;
}

/* 图标公共样式 */
#acme-box css-icon {
  display: inline-block;
  height: 1em;
  width: 1em;
  font-size: 20px;
  box-sizing: border-box;
  text-indent: -9999px;
  vertical-align: middle;
  position: relative;
}

#acme-box css-icon::before,
#acme-box css-icon::after {
  content: '';
  box-sizing: inherit;
  position: absolute;
  left: 50%;
  top: 50%;
  -ms-transform: translate(-50%, -50%);
  transform: translate(-50%, -50%);
}

/* 图标公共样式线束 */


/* 向上图标 */
#acme-box .icon-arrow-up::before {
  height: .65em;
  width: .65em;
  border-style: solid;
  border-width: 2px 0 0 2px;
  -ms-transform: translate(-50%, -25%) rotate(45deg);
  transform: translate(-50%, -25%) rotate(45deg);
}

/* 向下图标 */
#acme-box .icon-arrow-down::before {
  height: .65em;
  width: .65em;
  border-style: solid;
  border-width: 2px 0 0 2px;
  -ms-transform: translate(-50%, -75%) rotate(225deg);
  transform: translate(-50%, -75%) rotate(225deg);
}

/* 标题栏图标 */
#acme-box .icon-apps::before {
  height: .15em;
  width: .15em;
  background: currentColor;
  box-shadow: -.35em -.35em, -.35em 0, -.35em .35em, 0 -.35em, 0 .35em, .35em -.35em, .35em 0, .35em .35em;
}

#acme-box .d-flex {
  display: flex !important;
}

#acme-box .flex-row {
  flex-direction: row !important;
}

#acme-box .flex-grow-1 {
  flex-grow: 1 !important;
}

#acme-box .align-items-center {
  align-items: center !important;
}

#acme-box .bg-danger {
  --bs-bg-opacity: 1;
  background-color: rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important;
}


#acme-box .text-white {
  --bs-text-opacity: 1;
  color: rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important;
}

#acme-box .btn {
  display: inline-block;
  font-weight: 400;
  line-height: 1.5;
  color: #212529;
  text-align: center;
  text-decoration: none;
  vertical-align: middle;
  cursor: pointer;
  -webkit-user-select: none;
  -moz-user-select: none;
  user-select: none;
  background-color: transparent;
  border: 1px solid transparent;
  padding: 0.375rem 0.75rem;
  font-size: 1rem;
  border-radius: 0.25rem;
  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}


#acme-box .btn-outline-danger {
  color: #dc3545;
  border-color: #dc3545;
}

#acme-box .btn-outline-danger:hover {
  color: #fff;
  background-color: #dc3545;
  border-color: #dc3545;
}

#acme-box .btn-check:focus+.btn-outline-danger,
#acme-box .btn-outline-danger:focus {
  box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.5);
}

#acme-box .btn-check:checked+.btn-outline-danger,
#acme-box .btn-check:active+.btn-outline-danger,
#acme-box .btn-outline-danger:active,
#acme-box .btn-outline-danger.active,
#acme-box .btn-outline-danger.dropdown-toggle.show {
  color: #fff;
  background-color: #dc3545;
  border-color: #dc3545;
}

#acme-box .btn-check:checked+.btn-outline-danger:focus,
#acme-box .btn-check:active+.btn-outline-danger:focus,
#acme-box .btn-outline-danger:active:focus,
#acme-box .btn-outline-danger.active:focus,
#acme-box .btn-outline-danger.dropdown-toggle.show:focus {
  box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.5);
}

#acme-box .btn-outline-danger:disabled,
#acme-box .btn-outline-danger.disabled {
  color: #dc3545;
  background-color: transparent;
}

#acme-box .card {
  /* position: relative;
  display: flex; */
  flex-direction: column;
  min-width: 0;
  word-wrap: break-word;
  background-color: #fff;
  background-clip: border-box;
  border: 1px solid rgba(0, 0, 0, 0.125);
  border-radius: 0.75rem;
}

#acme-box .card-body {
  flex: 1 1 auto;
  padding: 1rem 1rem;
}

#acme-box .card-title {
  margin-bottom: 0.5rem;
}

#acme-box .card-header {
  padding: 0.5rem 1rem;
  margin-bottom: 0;
  background-color: rgba(0, 0, 0, 0.03);
  border-bottom: 1px solid rgba(0, 0, 0, 0.125);
  /* border-radius: 0.75rem; */
  border-top-left-radius: 0.75rem;
  border-top-right-radius: 0.75rem;
}

#acme-box .mx-2 {
  margin-right: 0.5rem !important;
  margin-left: 0.5rem !important;
}
  `;
      function addStyleCss(styleCss) {
        let style = document.createElement("style");
        style.type = "text/css";
        let text = document.createTextNode(styleCss);
        style.appendChild(text);

        document.querySelector("head").appendChild(style);
      }

      // 在 </body>标签前添加 html
      function addHtml(html) {
        document.body.insertAdjacentHTML("beforeend", html);
      }

      addStyleCss(cssStyle);
      addHtml(html);
    }
    initialize();
    (() => {
      // 实现界面的显示切换及拖动,这部分要放在最后面
      let btnShow = document.querySelector("#show");
      let acmeCard = document.querySelector("#acme-box > .acme-card");
      let acmeCardheader = document.querySelector("#acme-card-header");
      let restore = document.querySelector("#restore");
      let btnSetSpeedx = document.querySelector("#set-speedx");
      let speedSelect = document.querySelector("#speedx");

      speedSelect.addEventListener("change", async (e) => {
        console.log(speedSelect.value);
        speed = speedSelect.value;
        await setPlaySpeed(speed);
      });

      window.addEventListener("hashchange", async function (event) {
        console.log("url changed", event);
        await setPlaySpeed(speed);
      });

      btnShow.addEventListener("click", (e) => {
        acmeCard.style.display = "block";
        btnShow.style.display = "none";
      });

      restore.addEventListener("click", (e) => {
        acmeCard.style.display = "none";
        btnShow.style.display = "block";
      });

      btnSetSpeedx.addEventListener("click", (e) => {
        setPlaySpeed(speed);
      });
      // 鼠标指向标题栏时拖动 acmeCard
      acmeCardheader.addEventListener("mousedown", (e) => {
        let event = e || window.event;

        // 获取鼠标按下时的位置
        let pageX =
          event.pageX || event.clientX + document.documentElement.scrollLeft;
        let pageY =
          event.pageY || event.clientY + document.documentElement.scrollTop;

        // 计算鼠标按下时距例子的位置
        let spaceX = pageX - acmeCard.offsetLeft;
        let spaceY = pageY - acmeCard.offsetTop;

        // 实现拖动的函数
        function moveacmeCard(e) {
          let event = e || window.event;
          // 获取鼠标的位置
          let pageX =
            event.pageX || event.clientX + document.documentElement.scrollLeft;
          let pageY =
            event.pageY || event.clientY + document.documentElement.scrollTop;

          // 计算并设置盒子移动后的位置
          acmeCard.style.left = `${pageX - spaceX}px`;
          acmeCard.style.top = `${pageY - spaceY}px`;

          // 取消选中标题栏的文字
          if (window.getSelection) {
            window.getSelection().removeAllRanges();
          } else {
            document.selection.empty();
          }
        }

        // 移动例子
        document.addEventListener("mousemove", moveacmeCard);

        // 释放鼠标时停止移动
        document.addEventListener("mouseup", () => {
          document.removeEventListener("mousemove", moveacmeCard);
        });
      });
    })();

    const vd = "#J_CC_videoPlayerDiv > video ";
    let video = null;
    async function findVideoEl() {
      const el = await waitElementLoaded(vd);
      video = el;
      let timer;
      video.addEventListener("ended", async function (e) {
        console.log("播放已经结束");
        clearTimeout(timer);
        await waitTime(1);
        const xgReplay = document.querySelector(".xgplayer-replay");
        const styles = getComputedStyle(xgReplay);
        if (styles.display !== "none") {
          const query = new URLSearchParams(location.search);
          window.opener.postMessage({
            courseId: query.get("courseId"),
          });
        }
      });

      //   video.addEventListener("durationchange", async function (e) {
      //     console.log("播放时长改变");
      //     await waitTime(1);
      //     setPlaySpeed(speed);
      //   });

      video.addEventListener("pause", async function (e) {
        console.log("播放已暂停");

        timer = setTimeout(showPause, 1000);
      });
      setPlaySpeed(speed);
      video.play();
      video.muted = true;
      video.addEventListener("DOMNodeRemovedFromDocument", findVideoEl);
    }

    async function setPlaySpeed(speed = 4) {
      video.playbackRate = parseInt(speed);
      console.log("speed: ", video.playbackRate);
    }
    async function showPause() {
      try {
        let pauseBtn = await waitElementLoaded(".dialog-box  button", 2);
        if (pauseBtn) {
          console.log("触发离机检测,已恢复");
          pauseBtn.click();
        } else {
          video?.play();
        }
      } catch (e) {
        console.log("no dialog button.");
      }
    }
    findVideoEl();
    // setInterval(async function () {
    //   console.log("show pause dialog.");
    //   await showPause();
    // }, 1000 * 60);
  }
  async function courseList() {
    let lists = [];
    let btn;
    let courseNum;
    const insertBtn = () => {
      const btnHtml = `

        <button id="cus_btn" style="position:absolute;right:20px;bottom:20px;">收集课程<div id="course_num">?</div></button>
          `;
      document.body.insertAdjacentHTML("beforeend", btnHtml);
      btn = document.getElementById("cus_btn");
      courseNum = document.getElementById("course_num");
      btn.addEventListener("click", initLists);
    };
    async function initLists() {
      lists = Array.from(
        await waitElementLoaded("a[title=开始学习]", 50, 1, "querySelectorAll")
      );
      courseNum && (courseNum.innerText = lists.length);
    }
    insertBtn();
    initLists();

    window.addEventListener("message", (data) => {
      console.log("messgae", data);
      if (!data.data.courseId) {
        return;
      }
      const curIndex = lists.findIndex((item) => {
        console.log(item.getAttribute("courseid-id"), data.data.courseId);
        return item.getAttribute("courseid-id") === data.data.courseId;
      });
      if (curIndex === -1) {
        console.error("错误的courseId");
        return;
      }
      const nextCourse = lists[curIndex + 1];
      if (nextCourse) {
        nextCourse.click();
      } else {
        console.log("当前页课程已学完");
      }
    });
  }

  const path = location.pathname;
  if (path === "/circleIndexRedirect.do") {
    console.log("课程列表页启动");
    courseList();
  }
  if (path === "/viewerforccvideo.do") {
    console.log("课程详情页启动");
    courseDetail();
  }

  // Your code here...
})();