Youtube Shorts Improved

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

2024-02-15 या दिनांकाला. सर्वात नवीन आवृत्ती पाहा.

// ==UserScript==
// @name               Youtube Shorts Improved
// @name:zh-CN         更好的Youtube Shorts
// @name:zh-TW         更好的Youtube Shorts
// @name:ja            より良いYoutube Shorts
// @namespace          Violentmonkey Scripts
// @version            1.1.3
// @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
// @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%;
      }`
);

var vid = null;
var reel = null;
var progbar = null;
var seekMouseDown = false;
var bytsVol = null;
var bytsTimeInfo = null;
var lastCurSeconds = 0;
var progress = null;
var progressTime = 0;
var shortsAndPlayerReady = 0;
var autoScrollVal = true;
var autoscrollInput = null;

const observer = new MutationObserver((mutations) => {
  for (const mutation of mutations) {
    for (const node of mutation.addedNodes) {
      try {
        if (
          node.tagName === "YTD-SHORTS" ||
          node.classList.contains("html5-video-player")
        ) {
          shortsAndPlayerReady++;
          if (shortsAndPlayerReady === 2) {
            observer.disconnect();
            autoScrollVal = GM_getValue("bytsAutoscroll", true);
            updateVidElemWithRAF();
            addEventListener("keydown", function (e) {
              switch (e.key.toUpperCase()) {
                case "ARROWLEFT":
                  vid.currentTime -= 2;
                  break;
                case "ARROWRIGHT":
                  vid.currentTime += 2;
                  break;
                default:
                  break;
              }
            });
          }
        }
      } catch (_) {}
    }
  }
});
observer.observe(document.documentElement, {
  childList: true,
  subtree: true,
});

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

function updateVidElemWithRAF() {
  updateVidElem();
  requestAnimationFrame(updateVidElemWithRAF);
}

function navigationButtonDown() {
  const button = document
    .querySelector("#navigation-button-down")
    .querySelector("button");
  if (button !== null) {
    button.click();
  }
}

function updateVidElem() {
  vid = document.querySelector(".html5-video-player video");
  reel = vid.closest("ytd-reel-video-renderer");
  if (vid === null || reel === null) {
    return;
  }

  // Progress Bar
  if (reel.querySelector("#byts-progbar") === null) {
    const progressbar = reel.querySelector("#progress-bar");
    if (progressbar !== null) {
      progressbar.remove();
    }

    if (document.querySelector("#byts-progbar") === null) {
      const progbar = document.createElement("div");
      progbar.id = "byts-progbar";
      progbar.style.cssText =
        "user-select: none; cursor: pointer; width: 98%; height: 6px; background-color: #343434; position: absolute; margin-top: 846px; border-radius: 10px";
      reel.appendChild(progbar);
    } else {
      reel.appendChild(document.querySelector("#byts-progbar"));
    }
    progbar = document.querySelector("#byts-progbar");
    progbar.addEventListener("mousemove", (e) => {
      if (seekMouseDown) {
        vid.currentTime = ((e.offsetX * 1) / reel.offsetWidth) * vid.duration;
      }
    });
    progbar.addEventListener("mousedown", () => {
      seekMouseDown = true;
    });
    progbar.addEventListener("mouseleave", () => {
      seekMouseDown = false;
    });
    progbar.addEventListener("mouseup", (e) => {
      seekMouseDown = false;
      vid.currentTime = ((e.offsetX * 1) / reel.offsetWidth) * vid.duration;
    });
  }

  // Progress Bar (Inner Red Bar)
  progressTime = (vid.currentTime / vid.duration) * 100;
  if ((progress = progbar.querySelector("#byts-progress")) === null) {
    progress = document.createElement("div");
    progress.id = "byts-progress";
    progress.style.cssText = `user-select: none; background-color: #FF0000; height: 100%; border-radius: 10px; width: ${progressTime}%;`;
    progress.addEventListener("mouseup", (e) => {
      const selected_val = (e.offsetX * 1) / reel.offsetWidth;
      vid.currentTime = selected_val * vid.duration;
    });
    progbar.appendChild(progress);
  } else {
    progress.style.width = `${progressTime}%`;
  }

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

  if (
    curSecs != lastCurSeconds ||
    reel.querySelector("#byts-timeinfo") === null
  ) {
    lastCurSeconds = curSecs;

    let curMinutes = Math.floor(curSecs / 60);
    let curSeconds = curSecs % 60;

    // TimeInfo Element
    if (reel.querySelector("#byts-timeinfo") === null) {
      if (document.querySelector("#byts-timeinfo") === null) {
        const 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;`;
        const 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);
        reel.appendChild(timeInfo);
      } else {
        reel.appendChild(document.querySelector("#byts-timeinfo"));
      }
      bytsTimeInfo = document.querySelector("#byts-timeinfo");
    }

    document.querySelector(
      "#byts-timeinfo-textdiv"
    ).textContent = `${curMinutes}:${padTo2Digits(
      curSeconds
    )} / ${durMinutes}:${padTo2Digits(durSeconds)}`;
  }
  bytsTimeInfo.style.marginTop = `${reel.offsetHeight + 2}px`;

  // Volume Slide
  if (reel.querySelector("#byts-vol") === null) {
    if (document.querySelector("#byts-vol") === null) {
      vid.volume = GM_getValue("bytsVolume", 1.0);
      const volSlider = document.createElement("input");
      volSlider.style.cssText = `user-select: none; width: 100px; left: 0px; background-color: transparent; position: absolute; margin-top: ${
        reel.offsetHeight + 5
      }px;`;
      volSlider.type = "range";
      volSlider.id = "byts-vol";
      volSlider.className = "volslider";
      volSlider.name = "vol";
      volSlider.min = 0.0;
      volSlider.max = 1.0;
      volSlider.step = 0.05;
      volSlider.value = vid.volume;
      volSlider.addEventListener("input", function () {
        vid.volume = this.value;
        GM_setValue("bytsVolume", this.value);
      });
      reel.appendChild(volSlider);
    } else {
      reel.appendChild(document.querySelector("#byts-vol"));
    }
    bytsVol = document.querySelector("#byts-vol");
  } else {
    try {
      vid.volume = document.querySelector("#byts-vol").value;
      document.querySelector("#byts-vol").value = vid.volume;
    } catch (_) {}
  }
  bytsVol.style.marginTop = `${reel.offsetHeight + 5}px`;

  // AutoScroll
  if (reel.querySelector("#byts-autoscroll-div") === null) {
    if (document.querySelector("#byts-autoscroll-div") === null) {
      let astc = "";
      if (autoScrollVal) {
        astc = " checked";
      }
      const 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";
      autoscrollInput = document.createElement("input");
      autoscrollInput.id = "byts-autoscroll-input";
      autoscrollInput.type = "checkbox";
      autoscrollInput.checked = autoScrollVal;
      autoscrollInput.addEventListener("input", function () {
        GM_setValue("bytsAutoscroll", this.checked);
        if (this.checked) {
          autoScrollVal = true;
        } else {
          autoScrollVal = false;
        }
      });
      const autoscrollSlider = document.createElement("span");
      autoscrollSlider.className = "slider round";
      autoscrollSwitch.appendChild(autoscrollInput);
      autoscrollSwitch.appendChild(autoscrollSlider);
      autoscrollDiv.appendChild(autoscrollSwitch);
      reel.appendChild(autoscrollDiv);
    } else {
      reel.appendChild(document.querySelector("#byts-autoscroll-div"));
    }
    bytsVol = document.querySelector("#byts-autoscroll-div");
  } else {
    if (autoScrollVal == true) {
      vid.removeAttribute("loop");
      vid.addEventListener("ended", navigationButtonDown);
    } else {
      vid.setAttribute("loop", true);
      vid.removeEventListener("ended", navigationButtonDown);
    }
  }
  bytsVol.style.marginTop = `${reel.offsetHeight + 2}px`;
}