Better Youtube Shorts

Provides more control features for Youtube Shorts, including volume control, progress bar, auto-scroll, hotkeys, and more.

As of 02. 05. 2024. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==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);
    });
  })();
})();