更好的Youtube Shorts

为Youtube Shorts提供更多的控制功能,包括音量控制,进度条,自动滚动,快捷键等等。

目前为 2024-02-15 提交的版本。查看 最新版本

// ==UserScript==
// @name               Better Youtube Shorts
// @name:zh-CN         更好的Youtube Shorts
// @name:zh-TW         更好的Youtube Shorts
// @name:ja            より良いYoutube Shorts
// @namespace          Violentmonkey Scripts
// @version            1.2.8
// @description        Provides more control features for Youtube Shorts, including volume control, progress bar, auto-scroll, hotkeys, and more.
// @description:zh-CN  为Youtube Shorts提供更多的控制功能,包括音量控制,进度条,自动滚动,快捷键等等。
// @description:zh-TW  為Youtube Shorts提供更多的控制功能,包括音量控制,進度條,自動滾動,快捷鍵等等。
// @description:ja     Youtube Shortsに音量コントロール、プログレスバー、自動スクロール、ホットキーなどの機能を提供します。
// @author             Meriel
// @match              *://www.youtube.com/shorts/*
// @run-at             document-start
// @grant              GM_addStyle
// @grant              GM_getValue
// @grant              GM_setValue
// @grant              GM_registerMenuCommand
// @grant              GM_notification
// @license            MIT
// @icon               https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// ==/UserScript==

GM_addStyle(
    `input[type="range"].volslider {
      height: 14px;
      -webkit-appearance: none;
      margin: 10px 0;
    }
    input[type="range"].volslider:focus {
      outline: none;
    }
    input[type="range"].volslider::-webkit-slider-runnable-track {
      height: 8px;
      cursor: pointer;
      box-shadow: 0px 0px 0px #000000;
      background: rgb(50 50 50);
      border-radius: 25px;
      border: 1px solid #000000;
    }
    input[type="range"].volslider::-webkit-slider-thumb {
      -webkit-appearance: none;
      width: 20px;
      height: 20px;
      margin-top: -7px;
      border-radius: 0px;
      background-image: url("https://i.imgur.com/vcQoCVS.png");
      background-size: 20px;
      background-repeat: no-repeat;
      background-position: 50%;
    }
    input[type="range"]:focus::-webkit-slider-runnable-track {
      background: rgb(50 50 50);
    }
  
    .switch {
      position: relative;
      display: inline-block;
      width: 46px;
      height: 20px;
    }
    .switch input {
      opacity: 0;
      width: 0;
      height: 0;
    }
    .slider {
      position: absolute;
      cursor: pointer;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background-color: #ccc;
      -webkit-transition: 0.4s;
      transition: 0.4s;
    }
    .slider:before {
      position: absolute;
      content: "";
      height: 12px;
      width: 12px;
      left: 4px;
      bottom: 4px;
      background-color: white;
      -webkit-transition: 0.4s;
      transition: 0.4s;
    }
    input:checked + .slider {
      background-color: #ff0000;
    }
    input:focus + .slider {
      box-shadow: 0 0 1px #ff0000;
    }
    input:checked + .slider:before {
      -webkit-transform: translateX(26px);
      -ms-transform: translateX(26px);
      transform: translateX(26px);
    }
    /* Rounded sliders */
    .slider.round {
      border-radius: 12px;
    }
    .slider.round:before {
      border-radius: 50%;
    }`
  );
  
  let seekMouseDown = false;
  let lastCurSeconds = 0;
  let autoScrollVal = GM_getValue("autoscroll", true);
  let constantVolume = GM_getValue("constantVolume", false);
  
  GM_registerMenuCommand(
    `Constant Volume: ${constantVolume ? "On" : "Off"}`,
    function () {
      constantVolume = !constantVolume;
      GM_setValue("constantVolume", constantVolume);
      location.reload();
    }
  );
  
  let shortsReady = false;
  let videoPlayerReady = false;
  const observer = new MutationObserver((mutations) => {
    outer: for (const mutation of mutations) {
      for (const node of mutation.addedNodes) {
        if (!shortsReady) shortsReady = node.tagName === "YTD-SHORTS";
        if (!videoPlayerReady)
          videoPlayerReady =
            typeof node.className === "string" &&
            node.className.includes("html5-video-player");
        if (shortsReady && videoPlayerReady) {
          observer.disconnect();
          let videoPlayer = document.querySelector(".html5-video-player video");
          addShortcuts(videoPlayer);
          updateVidElemWithRAF();
          break outer;
        }
      }
    }
  });
  observer.observe(document.documentElement, {
    childList: true,
    subtree: true,
  });
  
  function addShortcuts(videoPlayer) {
    addEventListener("keydown", function (e) {
      switch (e.key.toUpperCase()) {
        case "ARROWLEFT":
          videoPlayer.currentTime -= 2;
          break;
        case "ARROWRIGHT":
          videoPlayer.currentTime += 2;
          break;
        default:
          break;
      }
    });
  }
  
  function padTo2Digits(num) {
    return num.toString().padStart(2, "0");
  }
  
  function updateVidElemWithRAF() {
    updateVidElem();
    requestAnimationFrame(updateVidElemWithRAF);
  }
  
  function navigationButtonDown() {
    document.querySelector("#navigation-button-down button").click();
  }
  
  function updateVidElem() {
    const videoPlayer = document.querySelector(".html5-video-player video");
    const reelVideoRenderer = videoPlayer.closest("ytd-reel-video-renderer");
    if (videoPlayer === null || reelVideoRenderer === null) {
      return;
    }
  
    // Volume Slider
    let volumeSlider = document.querySelector("#byts-vol");
    if (reelVideoRenderer.querySelector("#byts-vol") === null) {
      if (volumeSlider === null) {
        volumeSlider = document.createElement("input");
        volumeSlider.style.cssText = `user-select: none; width: 100px; left: 0px; background-color: transparent; position: absolute; margin-top: ${
          reelVideoRenderer.offsetHeight + 5
        }px;`;
        volumeSlider.type = "range";
        volumeSlider.id = "byts-vol";
        volumeSlider.className = "volslider";
        volumeSlider.name = "vol";
        volumeSlider.min = 0.0;
        volumeSlider.max = 1.0;
        volumeSlider.step = 0.01;
        volumeSlider.value = videoPlayer.volume;
        volumeSlider.addEventListener("input", function () {
          videoPlayer.volume = this.value;
          GM_setValue("bytsVolume", this.value);
        });
      }
      reelVideoRenderer.appendChild(volumeSlider);
    }
    if (constantVolume) {
      videoPlayer.volume = document.querySelector("#byts-vol").value;
    }
    document.querySelector("#byts-vol").value = videoPlayer.volume;
  
    volumeSlider.style.marginTop = `${reelVideoRenderer.offsetHeight + 5}px`;
  
    // Progress Bar
    let progressBar = document.querySelector("#byts-progbar");
    if (reelVideoRenderer.querySelector("#byts-progbar") === null) {
      const builtinProgressbar = reelVideoRenderer.querySelector("#progress-bar");
      if (builtinProgressbar !== null) {
        builtinProgressbar.remove();
      }
      if (progressBar === null) {
        progressBar = document.createElement("div");
        progressBar.id = "byts-progbar";
        progressBar.style.cssText =
          "user-select: none; cursor: pointer; width: 98%; height: 6px; background-color: #343434; position: absolute; margin-top: 846px; border-radius: 10px";
      }
      reelVideoRenderer.appendChild(progressBar);
  
      progressBar.addEventListener("mousemove", (e) => {
        if (seekMouseDown) {
          videoPlayer.currentTime =
            ((e.offsetX * 1) / reelVideoRenderer.offsetWidth) *
            videoPlayer.duration;
        }
      });
      progressBar.addEventListener("mousedown", () => {
        seekMouseDown = true;
      });
      progressBar.addEventListener("mouseleave", () => {
        seekMouseDown = false;
      });
      progressBar.addEventListener("mouseup", (e) => {
        seekMouseDown = false;
        videoPlayer.currentTime =
          ((e.offsetX * 1) / reelVideoRenderer.offsetWidth) *
          videoPlayer.duration;
      });
    }
  
    // Progress Bar (Inner Red Bar)
    let progressTime = (videoPlayer.currentTime / videoPlayer.duration) * 100;
    let InnerProgressBar = progressBar.querySelector("#byts-progress");
    if (InnerProgressBar === null) {
      InnerProgressBar = document.createElement("div");
      InnerProgressBar.id = "byts-progress";
      InnerProgressBar.style.cssText = `user-select: none; background-color: #FF0000; height: 100%; border-radius: 10px; width: ${progressTime}%;`;
      InnerProgressBar.addEventListener("mouseup", (e) => {
        const selectedVal = (e.offsetX * 1) / reelVideoRenderer.offsetWidth;
        videoPlayer.currentTime = selectedVal * videoPlayer.duration;
      });
      progressBar.appendChild(InnerProgressBar);
    }
    InnerProgressBar.style.width = `${progressTime}%`;
  
    // Time Info
    let durSecs = Math.floor(videoPlayer.duration);
    let durMinutes = Math.floor(durSecs / 60);
    let durSeconds = durSecs % 60;
    let curSecs = Math.floor(videoPlayer.currentTime);
  
    let timeInfo = document.querySelector("#byts-timeinfo");
    let timeInfoTextDiv = document.querySelector("#byts-timeinfo-textdiv");
    if (
      curSecs != lastCurSeconds ||
      reelVideoRenderer.querySelector("#byts-timeinfo") === null
    ) {
      lastCurSeconds = curSecs;
      let curMinutes = Math.floor(curSecs / 60);
      let curSeconds = curSecs % 60;
  
      if (reelVideoRenderer.querySelector("#byts-timeinfo") === null) {
        if (timeInfo === null) {
          timeInfo = document.createElement("div");
          timeInfo.id = "byts-timeinfo";
          timeInfo.style.cssText = `user-select: none; display: flex; right: auto; left: auto; position: absolute; margin-top: ${
            reelVideoRenderer.offsetHeight + 2
          }px;`;
          timeInfoTextDiv = document.createElement("div");
          timeInfoTextDiv.id = "byts-timeinfo-textdiv";
          timeInfoTextDiv.style.cssText = `display: flex; margin-right: 5px; margin-top: 4px; color: white; font-size: 1.2rem;`;
          timeInfoTextDiv.textContent = `${curMinutes}:${padTo2Digits(
            curSeconds
          )} / ${durMinutes}:${padTo2Digits(durSeconds)}`;
          timeInfo.appendChild(timeInfoTextDiv);
        }
        reelVideoRenderer.appendChild(timeInfo);
      }
      timeInfoTextDiv.textContent = `${curMinutes}:${padTo2Digits(
        curSeconds
      )} / ${durMinutes}:${padTo2Digits(durSeconds)}`;
    }
    timeInfo.style.marginTop = `${reelVideoRenderer.offsetHeight + 2}px`;
  
    // AutoScroll
    let autoScrollDiv = document.querySelector("#byts-autoscroll-div");
    if (reelVideoRenderer.querySelector("#byts-autoscroll-div") === null) {
      if (autoScrollDiv === null) {
        autoScrollDiv = document.createElement("div");
        autoScrollDiv.id = "byts-autoscroll-div";
        autoScrollDiv.style.cssText = `user-select: none; display: flex; right: 0px; position: absolute; margin-top: ${
          reelVideoRenderer.offsetHeight + 2
        }px;`;
        const autoScrollTextDiv = document.createElement("div");
        autoScrollTextDiv.style.cssText = `display: flex; margin-right: 5px; margin-top: 4px; color: white; font-size: 1.2rem;`;
        autoScrollTextDiv.textContent = "Auto-Scroll: ";
        autoScrollDiv.appendChild(autoScrollTextDiv);
        const autoScrollSwitch = document.createElement("label");
        autoScrollSwitch.className = "switch";
        const autoscrollInput = document.createElement("input");
        autoscrollInput.id = "byts-autoscroll-input";
        autoscrollInput.type = "checkbox";
        autoscrollInput.checked = autoScrollVal;
        autoscrollInput.addEventListener("input", function () {
          autoScrollVal = this.checked;
          GM_setValue("autoscroll", this.checked);
        });
        const autoScrollSlider = document.createElement("span");
        autoScrollSlider.className = "slider round";
        autoScrollSwitch.appendChild(autoscrollInput);
        autoScrollSwitch.appendChild(autoScrollSlider);
        autoScrollDiv.appendChild(autoScrollSwitch);
      }
      reelVideoRenderer.appendChild(autoScrollDiv);
    }
    if (autoScrollVal === true) {
      videoPlayer.removeAttribute("loop");
      videoPlayer.removeEventListener("ended", navigationButtonDown);
      videoPlayer.addEventListener("ended", navigationButtonDown);
    } else {
      videoPlayer.setAttribute("loop", true);
      videoPlayer.removeEventListener("ended", navigationButtonDown);
    }
  
    autoScrollDiv.style.marginTop = `${reelVideoRenderer.offsetHeight + 2}px`;
  }
  

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址