YouTube Live Theme

Removes the progress bar from YouTube live streams.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         YouTube Live Theme
// @namespace    ModLabs
// @version      1.0.0-GitHUb
// @description  Removes the progress bar from YouTube live streams.
// @license      Apache License 2.0
// @author       ModLabs
// @match        https://www.youtube.com/*
// @match        https://m.youtube.com/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  const CHECK_INTERVAL_MS = 1200;
  const NAVIGATION_EVENTS = ['yt-navigate-finish', 'yt-page-data-updated'];
  const SHADOW_STYLE_ID = 'yt-live-progress-remover-shadow-style';
  const PLAYER_HIDE_CLASS = 'yt-live-progress-hidden';
  const PLAYER_PROGRESS_ACTIVE_CLASS = 'yt-live-progress-active';
  const HOVER_ZONE_PX = 90;
  const HIDE_DELAY_MS = 400;
  const PROGRESS_BAR_Y_OFFSET_PX = 54;
  const DEBUG = false;
  const DEBUG_OVERLAY_ID = 'yt-live-progress-debug';
  let lastLiveElement = null;

  const getLiveIndicatorElement = () => document.querySelector('.ytp-live');
  const getLiveBadgeElement = () => document.querySelector('.ytp-live');

  const removeDebugOverlay = () => {
    const o = document.getElementById(DEBUG_OVERLAY_ID);
    if (o) o.remove();
    if (lastLiveElement) {
      lastLiveElement.style.outline = '';
      lastLiveElement.style.outlineOffset = '';
      lastLiveElement = null;
    }
  };

  const showDebugOverlay = (el, badge) => {
    if (!DEBUG) return;
    let overlay = document.getElementById(DEBUG_OVERLAY_ID);
    const target = badge || el;
    const info = `${badge ? 'badge ' : ''}${target.tagName.toLowerCase()}${target.id ? '#' + target.id : ''}${target.classList.length ? '.' + [...target.classList].join('.') : ''}`;
    if (!overlay) {
      overlay = document.createElement('div');
      overlay.id = DEBUG_OVERLAY_ID;
      overlay.style.cssText = 'position:fixed;z-index:999999;top:8px;left:8px;padding:6px 10px;font:12px/1.3 system-ui,Segoe UI,Roboto,sans-serif;background:rgba(0,0,0,0.65);color:#ffbfd1;border:1px solid rgba(255,255,255,0.18);border-radius:8px;backdrop-filter:blur(8px) saturate(180%);-webkit-backdrop-filter:blur(8px) saturate(180%);pointer-events:none;box-shadow:0 4px 14px -4px rgba(0,0,0,0.6)';
      document.documentElement.appendChild(overlay);
    }
    overlay.textContent = 'Live detected: ' + info;
  };
  const CONTROL_SHADOW_CSS = `
    .html5-video-player.${PLAYER_HIDE_CLASS} .ytp-gradient-bottom {
      background: linear-gradient(
        0deg,
        rgba(0, 0, 0, 0.68) 0%,
        rgba(0, 0, 0, 0.38) 55%,
        rgba(0, 0, 0, 0) 100%
      ) !important;
    }

    .html5-video-player.${PLAYER_HIDE_CLASS} .ytp-progress-bar-container {
      opacity: 0 !important;
      transform: translateY(${PROGRESS_BAR_Y_OFFSET_PX}px);
      transition: opacity 240ms ease;
      background: transparent !important;
      border: 1px solid transparent !important;
      box-shadow: none !important;
      position: relative;
      overflow: hidden;
      pointer-events: auto !important;
    }

    .html5-video-player.${PLAYER_HIDE_CLASS} .ytp-progress-bar-container,
    .html5-video-player.${PLAYER_HIDE_CLASS} .ytp-progress-bar-container .ytp-progress-bar,
    .html5-video-player.${PLAYER_HIDE_CLASS} .ytp-progress-bar-container .ytp-progress-bar * {
      border-radius: 16px !important;
    }

    .html5-video-player.${PLAYER_HIDE_CLASS} .ytp-progress-bar-container .ytp-progress-bar {
      opacity: 0.4 !important;
  height: 8px !important;
    }

    .html5-video-player.${PLAYER_HIDE_CLASS}.${PLAYER_PROGRESS_ACTIVE_CLASS} .ytp-progress-bar-container {
      opacity: 1 !important;
      transform: translateY(${PROGRESS_BAR_Y_OFFSET_PX}px);
      transition: opacity 160ms ease;
      background:
        linear-gradient(182deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 55%, rgba(0,0,0,0.35) 100%),
        rgba(0,0,0,0.42) !important;
      border: 1px solid rgba(255,255,255,0.18) !important;
      box-shadow:
        inset 0 0 0 1px rgba(255,255,255,0.05),
        inset 0 1px 0 rgba(255,255,255,0.28),
        0 4px 14px -4px rgba(0,0,0,0.55),
        0 18px 40px -10px rgba(0,0,0,0.55);
      backdrop-filter: blur(30px) saturate(260%) brightness(0.92);
      -webkit-backdrop-filter: blur(30px) saturate(260%) brightness(0.92);
      overflow: hidden;
      pointer-events: auto !important;
    }

    .html5-video-player.${PLAYER_HIDE_CLASS}.${PLAYER_PROGRESS_ACTIVE_CLASS} .ytp-progress-bar-container::before {
      content: '';
      position: absolute;
      inset: 2px 3px 3px 3px;
      border-radius: inherit;
      background:
        radial-gradient(120% 140% at 15% 0%, rgba(255,255,255,0.38) 0%, rgba(255,255,255,0) 55%),
        linear-gradient(90deg, rgba(255,255,255,0.18) 0%, rgba(255,255,255,0) 22%, rgba(255,255,255,0) 78%, rgba(255,255,255,0.22) 100%);
      mix-blend-mode: screen;
      opacity: 0.22;
      pointer-events: none;
    }

    .html5-video-player.${PLAYER_HIDE_CLASS}.${PLAYER_PROGRESS_ACTIVE_CLASS} .ytp-progress-bar-container::after {
      content: '';
      position: absolute;
      inset: 0;
      background:
        linear-gradient(180deg, rgba(255,255,255,0.20) 0%, rgba(255,255,255,0.05) 42%, rgba(0,0,0,0.55) 100%),
        radial-gradient(85% 120% at 50% -30%, rgba(255,255,255,0.30) 0%, rgba(255,255,255,0) 70%);
      opacity: 0.18;
      pointer-events: none;
    }

    .html5-video-player.${PLAYER_HIDE_CLASS}.${PLAYER_PROGRESS_ACTIVE_CLASS} .ytp-progress-bar {
      height: 8px !important;
      border-radius: 999px !important;
      position: relative;
      z-index: 1;
      overflow: visible !important;
    }

    .html5-video-player.${PLAYER_HIDE_CLASS}.${PLAYER_PROGRESS_ACTIVE_CLASS} .ytp-progress-bar-padding {
      border-radius: 999px !important;
    }

    .html5-video-player.${PLAYER_HIDE_CLASS}.${PLAYER_PROGRESS_ACTIVE_CLASS} .ytp-progress-bar-background {
      background:
        linear-gradient(120deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.02) 100%),
        rgba(0,0,0,0.58) !important;
    }

    .html5-video-player.${PLAYER_HIDE_CLASS}.${PLAYER_PROGRESS_ACTIVE_CLASS} .ytp-load-progress {
      background:
        linear-gradient(125deg, rgba(255,255,255,0.42) 0%, rgba(255,255,255,0.12) 100%) !important;
      opacity: 0.42;
    }

    .html5-video-player.${PLAYER_HIDE_CLASS} .ytp-progress-bar-container .ytp-play-progress,
    .html5-video-player.${PLAYER_HIDE_CLASS}.${PLAYER_PROGRESS_ACTIVE_CLASS} .ytp-progress-bar-container .ytp-play-progress {
      position: relative;
      border-radius: 999px !important;
      opacity: 0.66 !important;
      background:
        linear-gradient(118deg, rgba(255,90,120,0.85) 0%, rgba(255,60,110,0.90) 40%, rgba(255,70,130,0.80) 72%, rgba(255,120,160,0.70) 100%),
        rgba(255,72,110,0.55) !important;
      box-shadow:
        inset 0 0 0 1px rgba(255,255,255,0.55),
        0 0 26px rgba(255,80,120,0.55),
        0 4px 14px rgba(255,80,120,0.25),
        0 0 2px 1px rgba(255,120,150,0.45);
      backdrop-filter: blur(12px) saturate(240%);
      -webkit-backdrop-filter: blur(12px) saturate(240%);
    }

    .html5-video-player.${PLAYER_HIDE_CLASS}.${PLAYER_PROGRESS_ACTIVE_CLASS} .ytp-progress-bar-container .ytp-play-progress::after {
      content: '';
      position: absolute;
      inset: 0;
      border-radius: inherit;
      background:
        linear-gradient(100deg, rgba(255,255,255,0.75) 0%, rgba(255,255,255,0.35) 28%, rgba(255,255,255,0) 72%),
        linear-gradient(0deg, rgba(255,255,255,0.35), rgba(255,255,255,0));
      opacity: 0.55;
      mix-blend-mode: screen;
      pointer-events: none;
    }

    .html5-video-player.${PLAYER_HIDE_CLASS}.${PLAYER_PROGRESS_ACTIVE_CLASS} .ytp-scrubber-container {
      width: 18px !important;
      height: 18px !important;
    }

    .html5-video-player.${PLAYER_HIDE_CLASS}.${PLAYER_PROGRESS_ACTIVE_CLASS} .ytp-scrubber-button {
      width: 18px !important;
      height: 18px !important;
      margin-top: -5px !important;
      margin-left: -9px !important;
      border-radius: 50% !important;
      background:
        linear-gradient(140deg, rgba(255, 255, 255, 0.95) 0%, rgba(224, 228, 235, 0.85) 45%, rgba(176, 182, 196, 0.9) 100%) !important;
      box-shadow:
        0 6px 18px rgba(18, 20, 32, 0.38),
        0 1px 0 rgba(255, 255, 255, 0.65),
        inset 0 0 0 1px rgba(255, 255, 255, 0.95);
      border: none !important;
    }

    .html5-video-player.${PLAYER_HIDE_CLASS}.${PLAYER_PROGRESS_ACTIVE_CLASS} .ytp-scrubber-button::after {
      content: '';
      position: absolute;
      inset: 2px;
      border-radius: 50%;
      background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0.45) 45%, rgba(255, 255, 255, 0) 100%);
      opacity: 0.9;
    }

    .html5-video-player.${PLAYER_HIDE_CLASS}.${PLAYER_PROGRESS_ACTIVE_CLASS} .ytp-chapter-hover-container {
      border-radius: 14px !important;
      backdrop-filter: blur(20px) saturate(180%);
      -webkit-backdrop-filter: blur(20px) saturate(180%);
      background: rgba(16, 20, 32, 0.75);
      border: 1px solid rgba(255, 255, 255, 0.2);
      box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45);
    }
  `;

  let monitorId = null;
  const hoverBindingMap = new WeakMap();

  const bindProgressHoverHandlers = (player) => {
    const container = player.querySelector('.ytp-progress-bar-container');
    if (!container) {
      unbindProgressHoverHandlers(player);
      return;
    }

    const scrubber = player.querySelector('.ytp-scrubber-container');
    const existingBinding = hoverBindingMap.get(player);
    if (existingBinding?.container === container && existingBinding?.scrubber === scrubber) {
      return;
    }

    if (existingBinding) {
      unbindProgressHoverHandlers(player);
    }

    const state = {
      hideTimer: null,
      hoverZone: false,
      pointerInInteractive: false,
      focused: false,
      dragging: false,
    };

    const clearHideTimer = () => {
      if (state.hideTimer !== null) {
        clearTimeout(state.hideTimer);
        state.hideTimer = null;
      }
    };

    const activate = () => {
      clearHideTimer();
      if (!player.classList.contains(PLAYER_PROGRESS_ACTIVE_CLASS)) {
        player.classList.add(PLAYER_PROGRESS_ACTIVE_CLASS);
      }
    };

    const shouldHide = () => {
      return !state.hoverZone && !state.pointerInInteractive && !state.focused && !state.dragging;
    };

    const scheduleHide = () => {
      if (!shouldHide()) {
        return;
      }
      clearHideTimer();
      state.hideTimer = window.setTimeout(() => {
        state.hideTimer = null;
        if (shouldHide()) {
          player.classList.remove(PLAYER_PROGRESS_ACTIVE_CLASS);
        }
      }, HIDE_DELAY_MS);
    };

    const pointerMoveHandler = (event) => {
      const rect = player.getBoundingClientRect();
      const distanceFromBottom = rect.bottom - event.clientY;
      const insideZone = distanceFromBottom >= 0 && distanceFromBottom <= HOVER_ZONE_PX;
      if (insideZone) {
        if (!state.hoverZone) {
          state.hoverZone = true;
          activate();
        }
      } else if (state.hoverZone) {
        state.hoverZone = false;
        scheduleHide();
      }
    };

    const pointerLeaveHandler = () => {
      state.hoverZone = false;
      scheduleHide();
    };

    const pointerEnterInteractive = () => {
      state.pointerInInteractive = true;
      activate();
    };

    const pointerLeaveInteractive = () => {
      state.pointerInInteractive = false;
      scheduleHide();
    };

    const focusInHandler = () => {
      state.focused = true;
      activate();
    };

    const focusOutHandler = () => {
      state.focused = false;
      scheduleHide();
    };

    const pointerDownHandler = () => {
      state.dragging = true;
      activate();
    };

    const pointerUpHandler = () => {
      state.dragging = false;
      scheduleHide();
    };

  player.addEventListener('pointermove', pointerMoveHandler, { passive: true });
  player.addEventListener('pointerleave', pointerLeaveHandler, { passive: true });

  container.addEventListener('pointerenter', pointerEnterInteractive, { passive: true });
  container.addEventListener('pointerleave', pointerLeaveInteractive, { passive: true });
  container.addEventListener('pointerdown', pointerDownHandler, { passive: true });
    container.addEventListener('focusin', focusInHandler);
    container.addEventListener('focusout', focusOutHandler);

  scrubber?.addEventListener('pointerenter', pointerEnterInteractive, { passive: true });
  scrubber?.addEventListener('pointerleave', pointerLeaveInteractive, { passive: true });
  scrubber?.addEventListener('pointerdown', pointerDownHandler, { passive: true });

    window.addEventListener('pointerup', pointerUpHandler, true);

    hoverBindingMap.set(player, {
      container,
      scrubber,
      pointerMoveHandler,
      pointerLeaveHandler,
      pointerEnterInteractive,
      pointerLeaveInteractive,
      pointerDownHandler,
      pointerUpHandler,
      focusInHandler,
      focusOutHandler,
      state,
    });
  };

  const unbindProgressHoverHandlers = (player) => {
    const binding = hoverBindingMap.get(player);
    if (!binding) {
      player.classList.remove(PLAYER_PROGRESS_ACTIVE_CLASS);
      return;
    }

    const {
      container,
      scrubber,
      pointerMoveHandler,
      pointerLeaveHandler,
      pointerEnterInteractive,
      pointerLeaveInteractive,
      pointerDownHandler,
      pointerUpHandler,
      focusInHandler,
      focusOutHandler,
      state,
    } = binding;

    player.removeEventListener('pointermove', pointerMoveHandler);
    player.removeEventListener('pointerleave', pointerLeaveHandler);

    container?.removeEventListener('pointerenter', pointerEnterInteractive);
    container?.removeEventListener('pointerleave', pointerLeaveInteractive);
    container?.removeEventListener('pointerdown', pointerDownHandler);
    container?.removeEventListener('focusin', focusInHandler);
    container?.removeEventListener('focusout', focusOutHandler);

    scrubber?.removeEventListener('pointerenter', pointerEnterInteractive);
    scrubber?.removeEventListener('pointerleave', pointerLeaveInteractive);
    scrubber?.removeEventListener('pointerdown', pointerDownHandler);

    window.removeEventListener('pointerup', pointerUpHandler, true);

    state.dragging = false;
    if (state.hideTimer !== null) {
      clearTimeout(state.hideTimer);
      state.hideTimer = null;
    }

    player.classList.remove(PLAYER_PROGRESS_ACTIVE_CLASS);
    hoverBindingMap.delete(player);
  };

  const logPrefix = '[YT Live Progress Remover]';
  const log = (...args) => console.debug(logPrefix, ...args);

  const isLive = () => {
    const indicator = getLiveIndicatorElement();
    if (indicator) {
      const badge = getLiveBadgeElement();
      const highlightTarget = badge || indicator;
      if (DEBUG) {
        if (lastLiveElement !== highlightTarget) {
          if (lastLiveElement) {
            lastLiveElement.style.outline = '';
            lastLiveElement.style.outlineOffset = '';
          }
          lastLiveElement = highlightTarget;
        }
        showDebugOverlay(indicator, badge);
        highlightTarget.style.outline = '2px solid #ff2d55';
        highlightTarget.style.outlineOffset = '2px';
        console.debug('[YT Live Theme] Live indicator:', indicator, badge ? ' (badge preferred)' : '');
      }
      return true;
    }
    removeDebugOverlay();
    return false;
  };

  const setProgressBarHidden = (hidden) => {
    const players = document.querySelectorAll('.html5-video-player');
    if (!players.length) {
      return false;
    }

    let changed = false;
    players.forEach((player) => {
      if (hidden) {
        injectControlShadowTweaks();
        bindProgressHoverHandlers(player);
        if (!player.classList.contains(PLAYER_HIDE_CLASS)) {
          player.classList.add(PLAYER_HIDE_CLASS);
          changed = true;
        }
      } else if (player.classList.contains(PLAYER_HIDE_CLASS)) {
        player.classList.remove(PLAYER_HIDE_CLASS);
        changed = true;
      }

      if (!hidden) {
        player.classList.remove(PLAYER_PROGRESS_ACTIVE_CLASS);
        unbindProgressHoverHandlers(player);
      }
    });

    if (changed) {
      log(hidden ? 'Hid progress bar for livestream.' : 'Restored progress bar.');
    }

    if (!hidden) {
      const anyLivePlayers = Array.from(players).some(p => p.classList.contains(PLAYER_HIDE_CLASS));
      if (!anyLivePlayers) {
        const style = document.getElementById(SHADOW_STYLE_ID);
        style?.remove();
      }
    }

    return changed;
  };

  const injectControlShadowTweaks = () => {
    if (document.getElementById(SHADOW_STYLE_ID)) {
      return;
    }

    const style = document.createElement('style');
    style.id = SHADOW_STYLE_ID;
    style.textContent = CONTROL_SHADOW_CSS;

    (document.head || document.documentElement).appendChild(style);
    log('Injected control shadow tweaks.');
  };

  const teardownMonitor = () => {
    if (monitorId !== null) {
      clearInterval(monitorId);
      monitorId = null;
      log('Stopped live monitor.');
    }

    setProgressBarHidden(false);
    removeDebugOverlay();
  };

  const ensureMonitor = () => {
    if (monitorId !== null) {
      return;
    }

    monitorId = window.setInterval(() => {
      if (!isLive()) {
        teardownMonitor();
        return;
      }

      setProgressBarHidden(true);
    }, CHECK_INTERVAL_MS);

    log('Started live monitor.');
  };

  const handleStateChange = () => {
    if (isLive()) {
      setProgressBarHidden(true);
      ensureMonitor();
    } else {
      teardownMonitor();
    }
  };

  const waitForPlayerAndHandle = () => {
    const player = document.querySelector('.html5-video-player');
    if (player) {
      handleStateChange();
      return;
    }

    const observer = new MutationObserver((_, obs) => {
      if (document.querySelector('.html5-video-player')) {
        obs.disconnect();
        handleStateChange();
      }
    });

    observer.observe(document.documentElement, {
      childList: true,
      subtree: true,
    });
  };

  const attachNavigationListeners = () => {
    NAVIGATION_EVENTS.forEach((eventName) => {
      window.addEventListener(eventName, () => {
        setTimeout(waitForPlayerAndHandle, 150);
      });
    });

    window.addEventListener('popstate', () => {
      setTimeout(waitForPlayerAndHandle, 150);
    });
  };

  const init = () => {
    waitForPlayerAndHandle();
    attachNavigationListeners();
  };

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init, { once: true });
  } else {
    init();
  }
})();