更好的 Youtube Shorts

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

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

// ==UserScript==
// @name               Better Youtube Shorts
// @name:zh-CN         更好的 Youtube Shorts
// @name:zh-TW         更好的 Youtube Shorts
// @namespace          Violentmonkey Scripts
// @version            1.6.7
// @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==

(() => {
  let userscriptInitialized = false;
  (() => {
    const urlChange = (event = null) => {
      const destinationUrl = event?.destination?.url || "";
      if (destinationUrl.startsWith("about:blank")) return;
      const href = destinationUrl || location.href;
      const isShorts = href.includes("youtube.com/shorts");
      if (userscriptInitialized && !isShorts) {
        localtion.href = destinationUrl;
        userscriptInitialized = false;
      } else if (!userscriptInitialized && isShorts) {
        (() => {
          let volumeStyle = GM_getValue("volumeStyle");
          if (volumeStyle === void 0) {
            volumeStyle = "speaker";
            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}`, () => {
            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.querySelector("#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.stopPropagation();
              e.preventDefault();
              const volumeSlider = document.querySelector("#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.querySelector("#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?.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", () => {
              if (document.fullscreenElement) {
                document.exitFullscreen();
              } else {
                document.querySelector("ytd-app").requestFullscreen();
              }
            });
            document.addEventListener("keydown", (e) => {
              if (
                e.key.toUpperCase() === "ENTER" ||
                e.key.toUpperCase() === "NUMPADENTER"
              ) {
                if (document.fullscreenElement) {
                  document.exitFullscreen();
                } else {
                  document.querySelector("ytd-app").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) {
            let 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 = reel.querySelector("#byts-vol-div");
            let volumeSlider = reel.querySelector("#byts-vol");
            let volumeTextDiv = reel.querySelector("#byts-vol-textdiv");

            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 = reel.querySelector("#byts-progbar");

            if (progressBar === 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 = reel.querySelector("#byts-timeinfo");
            let timeInfoText = reel.querySelector("#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 (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 = reel.querySelector("#byts-autoscroll-div");

            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`;
          }
        })();
        userscriptInitialized = true;
      }
    };
    const historyWrap = (type) => {
      const origin = unsafeWindow.history[type];
      const event = new Event(type);
      return () => {
        const rv = origin.apply(this, arguments);
        event.arguments = arguments;
        unsafeWindow.dispatchEvent(event);
        return rv;
      };
    };
    urlChange();
    if (unsafeWindow.navigation) {
      unsafeWindow.navigation.addEventListener("navigate", (event) => {
        urlChange(event);
      });
      return;
    }
    unsafeWindow.history.pushState = historyWrap("pushState");
    unsafeWindow.history.replaceState = historyWrap("replaceState");
    unsafeWindow.addEventListener("replaceState", (event) => {
      urlChange(event);
    });
    unsafeWindow.addEventListener("pushState", (event) => {
      urlChange(event);
    });
    unsafeWindow.addEventListener("popstate", (event) => {
      urlChange(event);
    });
    unsafeWindow.addEventListener("hashchange", (event) => {
      urlChange(event);
    });
  })();
})();

QingJ © 2025

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