Stick YouTube Progress Bar

Stick YouTube video progress bar to the player bottom

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name          Stick YouTube Progress Bar
// @version       1.0.18
// @match         https://www.youtube.com/**
// @author        peng-devs
// @namespace     https://greasyfork.org/users/57176
// @description   Stick YouTube video progress bar to the player bottom
// @icon          https://www.youtube.com/s/desktop/c1d331ff/img/favicon_48x48.png
// @grant         none
// @allFrames     true
// @license       MIT
// ==/UserScript==

(function () {
  "use strict";

  const NAME = "Stick YouTube Progress Bar";
  const UPDATE_INTERVAL = 500;
  const SMOOTH_ANIMATION = false;

  let observer;
  let interval;

  function main() {
    observer?.disconnect();
    clearInterval(interval);
    document.getElementById("stick_progress")?.remove();
    observer = undefined;
    interval = undefined;

    if (
      !location.pathname.startsWith("/watch") &&
      !location.pathname.startsWith("/live")
    )
      return;

    observer = observe(document.body, () => {
      if (!observer) return;

      if (document.querySelector(".ytp-time-display.ytp-live")) {
        document.getElementById("stick_progress")?.remove();
        observer.disconnect();
        console.log(`[${NAME}] canceled in livestream`);
        return;
      }

      const initialized = document.querySelector("video.video-stream");
      if (!initialized) return;

      const duration = initialized.duration;
      if (isNaN(duration)) return;

      console.log(`[${NAME}] initializing...`);

      init_stick_progress_bar();

      observer.disconnect();
      console.log(`[${NAME}] loaded`);
    });
  }

  function init_stick_progress_bar() {
    const { container, progress_bar, buffer_bar } = create_progress_bar();

    const player = document.querySelector("#movie_player");
    player.append(container);

    let video = document.querySelector("video.video-stream[src]");
    interval = setInterval(() => {
      if (!video?.isConnected || !video.getAttribute("src")) {
        // youtube 有時候會抽風把頁面重新 re-render 生成新的 video
        console.debug(`[${NAME}] detect page re-render, reset video`);
        video = document.querySelector("video.video-stream[src]");
      }

      if (!video) return;
      if (!video.duration || isNaN(video.duration)) return;

      // skip during ads to avoid showing ad progress
      if (player.classList.contains("ad-showing")) return;

      const progress = video.currentTime / video.duration;
      progress_bar.style.transform = `scaleX(${progress})`;

      if (video.buffered.length > 0) {
        let buf_end = 0;
        for (let i = 0; i < video.buffered.length; i++) {
          if (
            video.currentTime >= video.buffered.start(i) &&
            video.currentTime <= video.buffered.end(i)
          ) {
            buf_end = video.buffered.end(i);
            break;
          }
        }
        buffer_bar.style.transform = `scaleX(${buf_end / video.duration})`;
      }
    }, UPDATE_INTERVAL);
  }

  function create_progress_bar() {
    if (!document.querySelector(`style[data-source="${NAME}"]`)) {
      inject_custom_style(`

        #stick_progress {
          display: none;
          z-index: 32;
          position: absolute;
          bottom: 4px;
          width: 97.5%;
          height: 4px;
          margin: 0 1.25%;
          background-color: rgba(255, 255, 255, .2);
        }

        .stick_progress_bar, .stick_buffer_bar {
          position: absolute;
          width: 100%;
          height: 100%;

          transform-origin: left;
          transform: scaleX(0);
          ${SMOOTH_ANIMATION ? `transition: all ${UPDATE_INTERVAL - 50}ms linear;` : ""}
        }

        .stick_buffer_bar {
          z-index: 33;
          background-color: rgba(255, 255, 255, .4);
        }

        .stick_progress_bar {
          z-index: 34;
          background-color: #f00;
        }

        .ytp-autohide #stick_progress {
          display: block;
        }
      `);
    }

    const container = document.createElement("div");
    container.id = "stick_progress";

    const progress_bar = document.createElement("div");
    progress_bar.className = "stick_progress_bar";
    container.append(progress_bar);

    const buffer_bar = document.createElement("div");
    buffer_bar.className = "stick_buffer_bar";
    container.append(buffer_bar);

    return {
      container,
      progress_bar,
      buffer_bar,
    };
  }

  function inject_custom_style(css) {
    const style = document.createElement("style");
    style.dataset.source = NAME;
    style.textContent = css;
    document.head.append(style);
  }

  function observe(dom, callback) {
    const observer = new MutationObserver(callback);
    observer.observe(dom, { childList: true, subtree: true });
    return observer;
  }

  document.addEventListener("yt-navigate-finish", main, true);
  main();
})();