更好的 Youtube Shorts

為 Youtube Shorts提供更多的控制功能,包括音量控制,進度條,自動滾動,快捷鍵等等。

目前為 2024-05-01 提交的版本,檢視 最新版本

// ==UserScript==
// @name               Better Youtube Shorts
// @name:zh-CN         更好的 Youtube Shorts
// @name:zh-TW         更好的 Youtube Shorts
// @namespace          Violentmonkey Scripts
// @version            1.6.1
// @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提供更多的控制功能,包括音量控制,進度條,自動滾動,快捷鍵等等。
// @author             Meriel
// @match              *://www.youtube.com/*
// @run-at             document-start
// @grant              GM_addStyle
// @grant              GM_getValue
// @grant              GM_setValue
// @grant              GM_registerMenuCommand
// @license            MIT
// @icon               https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// ==/UserScript==

(function () {
  const main = function () {
    let volumeStyle = GM_getValue("volumeStyle");
    if (volumeStyle === void 0) {
      volumeStyle = "dot";
      GM_setValue("volumeStyle", volumeStyle);
    }

    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;
  }
  ${
    volumeStyle === "dot"
      ? `input[type="range"].volslider::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 15px;
    height: 15px;
    margin-top: -4px;
    border-radius: 50%;
    background: white;
  }`
      : `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 video = null;
    let audioInitialized = false;
    let autoScroll = GM_getValue("autoScroll", true);
    let volume = GM_getValue("volume", 0);
    let constantVolume = GM_getValue("constantVolume");
    let operationMode = GM_getValue("operationMode");
    if (constantVolume === void 0) {
      constantVolume = false;
      GM_setValue("constantVolume", constantVolume);
    }
    if (operationMode === void 0) {
      operationMode = "Shorts";
      GM_setValue("operationMode", operationMode);
    }

    GM_registerMenuCommand(
      `Constant Volume: ${constantVolume ? "On" : "Off"}`,
      function () {
        constantVolume = !constantVolume;
        GM_setValue("constantVolume", constantVolume);
        location.reload();
      }
    );

    GM_registerMenuCommand(`Operating mode: ${operationMode}`, function () {
      operationMode = operationMode === "Video" ? "Shorts" : "Video";
      GM_setValue("operationMode", operationMode);
      location.reload();
    });

    GM_registerMenuCommand(`Volume Style: ${volumeStyle}`, function () {
      volumeStyle = volumeStyle === "speaker" ? "dot" : "speaker";
      GM_setValue("volumeStyle", volumeStyle);
      location.reload();
    });

    const observer = new MutationObserver(
      (mutations, shortsReady = false, videoPlayerReady = false) => {
        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-main-video");
            }
            if (shortsReady && videoPlayerReady) {
              observer.disconnect();
              video = node;
              if (constantVolume) {
                video.volume = volume;
              }
              addShortcuts();
              updateVidElemWithRAF();
              break outer;
            }
          }
        }
      }
    );
    observer.observe(document.documentElement, {
      childList: true,
      subtree: true,
    });

    function videoOperationMode(e) {
      if (!e.shiftKey) {
        if (
          e.key.toUpperCase() === "ARROWUP" ||
          e.key.toUpperCase() === "ARROWDOWN"
        ) {
          e.stopPropagation();
          e.preventDefault();
          const volumeSlider = document.getElementById("byts-vol");
          switch (e.key.toUpperCase()) {
            case "ARROWUP":
              video.volume = Math.min(1, video.volume + 0.01);
              volumeSlider.value = video.volume;
              break;
            case "ARROWDOWN":
              video.volume = Math.max(0, video.volume - 0.01);
              volumeSlider.value = video.volume;
              break;
            default:
              break;
          }
        } else if (
          e.key.toUpperCase() === "ARROWLEFT" ||
          e.key.toUpperCase() === "ARROWRIGHT"
        ) {
          switch (e.key.toUpperCase()) {
            case "ARROWLEFT":
              video.currentTime -= 1;
              break;
            case "ARROWRIGHT":
              video.currentTime += 1;
              break;
            default:
              break;
          }
        }
      } else {
        switch (e.key.toUpperCase()) {
          case "ARROWLEFT":
          case "ARROWUP":
            navigationButtonUp();
            break;
          case "ARROWRIGHT":
          case "ARROWDOWN":
            navigationButtonDown();
            break;
          default:
            break;
        }
      }
    }

    function shortsOperationMode(e) {
      if (
        e.key.toUpperCase() === "ARROWUP" ||
        e.key.toUpperCase() === "ARROWDOWN"
      ) {
        e.preventDefault();
        e.stopPropagation();
        const volumeSlider = document.getElementById("byts-vol");
        if (e.shiftKey) {
          switch (e.key.toUpperCase()) {
            case "ARROWUP":
              video.volume = Math.min(1, video.volume + 0.02);
              volumeSlider.value = video.volume;
              break;
            case "ARROWDOWN":
              video.volume = Math.max(0, video.volume - 0.02);
              volumeSlider.value = video.volume;
              break;
            default:
              break;
          }
        } else {
          switch (e.key.toUpperCase()) {
            case "ARROWUP":
              navigationButtonUp();
              break;
            case "ARROWDOWN":
              navigationButtonDown();
              break;
            default:
              break;
          }
        }
      } else if (
        e.key.toUpperCase() === "ARROWLEFT" ||
        e.key.toUpperCase() === "ARROWRIGHT"
      ) {
        const volumeSlider = document.getElementById("byts-vol");
        if (e.shiftKey) {
          switch (e.key.toUpperCase()) {
            case "ARROWLEFT":
              video.volume = Math.max(0, video.volume - 0.01);
              volumeSlider.value = video.volume;
              break;
            case "ARROWRIGHT":
              video.volume = Math.min(1, video.volume + 0.01);
              volumeSlider.value = video.volume;
              break;
            default:
              break;
          }
        } else {
          switch (e.key.toUpperCase()) {
            case "ARROWLEFT":
              video.currentTime -= 1;
              break;
            case "ARROWRIGHT":
              video.currentTime += 1;
              break;
            default:
              break;
          }
        }
      }
    }

    function addShortcuts() {
      if (operationMode === "Video") {
        const observer = new MutationObserver((mutations) => {
          for (const mutation of mutations) {
            for (const node of mutation.addedNodes) {
              if (node && node.id === "byts-vol-div") {
                document.addEventListener(
                  "keydown",
                  (e) => {
                    videoOperationMode(e);
                    if (constantVolume) {
                      constantVolume = false;
                      requestAnimationFrame(() => (constantVolume = true));
                    }
                  },
                  {
                    capture: true,
                  }
                );
                observer.disconnect();
              }
            }
          }
        });
        observer.observe(document.documentElement, {
          childList: true,
          subtree: true,
        });
      } else {
        document.addEventListener(
          "keydown",
          (e) => {
            shortsOperationMode(e);
            if (constantVolume) {
              constantVolume = false;
              requestAnimationFrame(() => (constantVolume = true));
            }
          },
          {
            capture: true,
          }
        );
      }
      video.addEventListener("dblclick", () => {
        const ytdApp = document.getElementsByTagName("ytd-app")[0];
        if (document.fullscreenElement) {
          document.exitFullscreen();
        } else {
          ytdApp.requestFullscreen();
        }
      });
      document.addEventListener("keydown", (e) => {
        if (
          e.key.toUpperCase() === "ENTER" ||
          e.key.toUpperCase() === "NUMPADENTER"
        ) {
          if (document.fullscreenElement) {
            document.exitFullscreen();
          } else {
            ytdApp.requestFullscreen();
          }
        }
      });
    }

    function padTo2Digits(num) {
      return num.toString().padStart(2, "0");
    }

    function updateVidElemWithRAF() {
      try {
        updateVidElem();
      } catch (_) {}
      requestAnimationFrame(updateVidElemWithRAF);
    }

    function navigationButtonDown() {
      document.querySelector("navigation-button-down button").click();
    }

    function navigationButtonUp() {
      document.querySelector("navigation-button-up button").click();
    }

    function setVideoPlaybackTime(event, player) {
      const rect = player.getBoundingClientRect();
      let offsetX = event.clientX - rect.left;
      if (offsetX < 0) {
        offsetX = 0;
      } else if (offsetX > player.offsetWidth) {
        offsetX = player.offsetWidth - 1;
      }
      video.currentTime = (offsetX / player.offsetWidth) * video.duration;
    }

    function updateVidElem() {
      const currentVideo = document.querySelector(
        "#shorts-player > div.html5-video-container > video"
      );
      if (video !== currentVideo) {
        video = currentVideo;
      }

      if (!audioInitialized && constantVolume) {
        video.volume = volume;
      }

      const reel = document.querySelector("ytd-reel-video-renderer[is-active]");
      if (reel === null) {
        return;
      }

      if (operationMode === "Shorts") {
        document.removeEventListener("keydown", videoOperationMode, {
          capture: true,
        });
        document.addEventListener("keydown", shortsOperationMode, {});
      } else {
        document.removeEventListener("keydown", shortsOperationMode, {});
        document.addEventListener("keydown", videoOperationMode, {
          capture: true,
        });
      }

      // Volume Slider
      let volumeSliderDiv = document.getElementById("byts-vol-div");
      let volumeSlider = document.getElementById("byts-vol");
      let volumeTextDiv = document.getElementById("byts-vol-textdiv");
      if (reel.querySelector("#byts-vol-div") === null) {
        if (volumeSliderDiv === null) {
          volumeSliderDiv = document.createElement("div");
          volumeSliderDiv.id = "byts-vol-div";
          volumeSliderDiv.style.cssText = `user-select: none; width: 100px; left: 0px; background-color: transparent; position: absolute; margin-left: 5px; margin-top: ${
            reel.offsetHeight + 5
          }px;`;
          volumeSlider = document.createElement("input");
          volumeSlider.style.cssText = `user-select: none; width: 80px; left: 0px; background-color: transparent; position: absolute; margin-top: 0px;`;
          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 = video.volume;
          volumeSlider.addEventListener("input", function () {
            video.volume = this.value;
            GM_setValue("volume", this.value);
          });
          volumeSliderDiv.appendChild(volumeSlider);
          volumeTextDiv = document.createElement("div");
          volumeTextDiv.id = "byts-vol-textdiv";
          volumeTextDiv.style.cssText = `user-select: none; background-color: transparent; position: absolute; color: white; font-size: 1.2rem; margin-left: ${
            volumeSlider.offsetWidth + 5
          }px`;
          volumeTextDiv.textContent = `${(
            video.volume.toFixed(2) * 100
          ).toFixed()}%`;
          volumeSliderDiv.appendChild(volumeTextDiv);
        }
        reel.appendChild(volumeSliderDiv);
        audioInitialized = true;
      }
      if (constantVolume) {
        video.volume = volumeSlider.value;
      }
      volumeSlider.value = video.volume;
      volumeTextDiv.textContent = `${(
        video.volume.toFixed(2) * 100
      ).toFixed()}%`;
      volumeSliderDiv.style.marginTop = `${reel.offsetHeight + 5}px`;
      volumeTextDiv.style.marginLeft = `${volumeSlider.offsetWidth + 5}px`;

      // Progress Bar
      let progressBar = document.getElementById("byts-progbar");
      if (reel.querySelector("#byts-progbar") === null) {
        const builtinProgressbar = reel.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; border-radius: 10px; margin-top: ${
            reel.offsetHeight - 6
          }px;`;
        }
        reel.appendChild(progressBar);

        let wasPausedBeforeDrag = false;
        progressBar.addEventListener("mousedown", (e) => {
          seekMouseDown = true;
          wasPausedBeforeDrag = video.paused;
          setVideoPlaybackTime(e, progressBar);
          video.pause();
        });
        document.addEventListener("mousemove", (e) => {
          if (!seekMouseDown) return;
          setVideoPlaybackTime(e, progressBar);
          if (!video.paused) {
            video.pause();
          }
          e.preventDefault();
        });
        document.addEventListener("mouseup", () => {
          if (!seekMouseDown) return;
          seekMouseDown = false;
          if (!wasPausedBeforeDrag) {
            video.play();
          }
        });
      }
      progressBar.style.marginTop = `${reel.offsetHeight - 6}px`;

      // Progress Bar (Inner Red Bar)
      const progressTime = (video.currentTime / video.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}%;`;
        progressBar.appendChild(InnerProgressBar);
      }
      InnerProgressBar.style.width = `${progressTime}%`;

      // Time Info
      const durSecs = Math.floor(video.duration);
      const durMinutes = Math.floor(durSecs / 60);
      const durSeconds = durSecs % 60;
      const curSecs = Math.floor(video.currentTime);

      let timeInfo = document.getElementById("byts-timeinfo");
      let timeInfoText = document.getElementById("byts-timeinfo-textdiv");

      if (
        !Number.isNaN(durSecs) &&
        reel.querySelector("#byts-timeinfo") !== null
      ) {
        timeInfoText.textContent = `${Math.floor(curSecs / 60)}:${padTo2Digits(
          curSecs % 60
        )} / ${durMinutes}:${padTo2Digits(durSeconds)}`;
      }
      if (
        curSecs !== lastCurSeconds ||
        reel.querySelector("#byts-timeinfo") === null
      ) {
        lastCurSeconds = curSecs;
        const curMinutes = Math.floor(curSecs / 60);
        const curSeconds = curSecs % 60;

        if (reel.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: ${
              reel.offsetHeight + 2
            }px;`;
            timeInfoText = document.createElement("div");
            timeInfoText.id = "byts-timeinfo-textdiv";
            timeInfoText.style.cssText = `display: flex; margin-right: 5px; margin-top: 4px; color: white; font-size: 1.2rem;`;
            timeInfoText.textContent = `${curMinutes}:${padTo2Digits(
              curSeconds
            )} / ${durMinutes}:${padTo2Digits(durSeconds)}`;
            timeInfo.appendChild(timeInfoText);
          }
          reel.appendChild(timeInfo);
          timeInfoText.textContent = `${curMinutes}:${padTo2Digits(
            curSeconds
          )} / ${durMinutes}:${padTo2Digits(durSeconds)}`;
        }
      }
      timeInfo.style.marginTop = `${reel.offsetHeight + 2}px`;

      // AutoScroll
      let autoScrollDiv = document.getElementById("byts-autoscroll-div");
      if (reel.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: ${
            reel.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 = autoScroll;
          autoscrollInput.addEventListener("input", function () {
            autoScroll = 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);
        }
        reel.appendChild(autoScrollDiv);
      }
      if (autoScroll === true) {
        video.removeAttribute("loop");
        video.removeEventListener("ended", navigationButtonDown);
        video.addEventListener("ended", navigationButtonDown);
      } else {
        video.setAttribute("loop", true);
        video.removeEventListener("ended", navigationButtonDown);
      }
      autoScrollDiv.style.marginTop = `${reel.offsetHeight + 2}px`;
    }
  };

  const urlObserver = function () {
    function urlChange(event = null) {
      const destinationUrl = event?.destination?.url || "";
      if (destinationUrl.startsWith("about:blank")) return;
      href = destinationUrl || location.href;
      if (!href.includes("youtube.com/shorts")) {
        return;
      } else {
        main();
      }
    }
    const historyWrap = function (type) {
      const origin = unsafeWindow.history[type];
      const event = new Event(type);
      return function () {
        const rv = origin.apply(this, arguments);
        event.arguments = arguments;
        unsafeWindow.dispatchEvent(event);
        return rv;
      };
    };
    if (unsafeWindow.navigation) {
      unsafeWindow.navigation.addEventListener("navigate", (event) => {
        urlChange(event);
      });
      return;
    }
    unsafeWindow.history.pushState = historyWrap("pushState");
    unsafeWindow.history.replaceState = historyWrap("replaceState");
    unsafeWindow.addEventListener("replaceState", function (event) {
      urlChange(event);
    });
    unsafeWindow.addEventListener("pushState", function (event) {
      urlChange(event);
    });
    unsafeWindow.addEventListener("popstate", function (event) {
      urlChange(event);
    });
    unsafeWindow.addEventListener("hashchange", function (event) {
      urlChange(event);
    });
  };

  urlObserver();
})();

QingJ © 2025

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