YouTube Progress Bar + Clock

Persistent YouTube Progress Bar

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         YouTube Progress Bar + Clock
// @namespace    slevin
// @version      1.0
// @license      MIT
// @description  Persistent YouTube Progress Bar
// @match        https://www.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @run-at       document-idle
// ==/UserScript==

document.head.append(Object.assign(document.createElement('style'), {
    textContent: `
        .ytp-progress-bar-container {
            transition: bottom 0.2s ease !important;
        }
        #movie_player.ytp-autohide .ytp-progress-bar-container {
            bottom: 8px !important;
        }
        .ytp-autohide .ytp-chrome-bottom {
            display: block !important;
            opacity: 1 !important;
        }
        .ytp-autohide .ytp-chrome-controls {
            opacity: 0 !important;
            pointer-events: none !important;
            transition: opacity 0.2s ease !important;
        }
        .ytp-progress-bar-container::after {
            content: attr(data-time);
            position: absolute;
            bottom: 12px;
            right: 0;
            z-index: 10;
            padding: 3px 8px;
            background: rgba(0, 0, 0, 0.35);
            border-radius: 12px;
            color: #eee;
            font-family: "YouTube Noto", Roboto, Arial, sans-serif;
            font-size: 14px;
            font-weight: 500;
            pointer-events: none;
            opacity: 0;
            transition: opacity 0.2s ease;
        }
        .ytp-autohide .ytp-progress-bar-container::after {
            opacity: 1;
            transition: opacity 0.2s ease;
        }
    `
}));
const fmt = s => isNaN(s) ? '0:00' : (Math.floor(s / 3600) ? Math.floor(s / 3600) + ':' + String(Math.floor((s % 3600) / 60)).padStart(2, '0') : Math.floor((s % 3600) / 60)) + ':' + String(Math.floor(s % 60)).padStart(2, '0');
const scaleFor = (p, start, end) => p >= end ? 1 : (p > start ? (p - start) / (end - start) : 0);
let v, container, scrubber;
document.addEventListener('yt-navigate-finish', () => {
    v = null;
    container = null;
    scrubber = null;
});
(function loop() {
    if (document.querySelector('video')) {
        v ??= document.querySelector('video');
        container ??= document.querySelector('.ytp-progress-bar-container');
        scrubber ??= document.querySelector('.ytp-scrubber-container');
        if (v?.duration && container) {
            container.setAttribute('data-time', `${fmt(v.currentTime)} / ${fmt(v.duration)}`);
            const { left: barLeft, width: barWidth } = container.getBoundingClientRect();
            const progress = v.currentTime / v.duration;
            if (scrubber) scrubber.style.transform = `translateX(${progress * barWidth}px)`;
            let bufferEnd = 0;
            for (let i = 0; i < v.buffered.length; i++) {
                if (v.buffered.start(i) <= v.currentTime) bufferEnd = Math.max(bufferEnd, v.buffered.end(i));
            }
            const bProg = bufferEnd / v.duration;
            document.querySelectorAll('.ytp-progress-list').forEach(seg => {
                const { left, width } = seg.getBoundingClientRect();
                const sStart = (left - barLeft) / barWidth, sEnd = (left - barLeft + width) / barWidth;
                const fill = seg.querySelector('.ytp-play-progress'), load = seg.querySelector('.ytp-load-progress');
                if (fill) fill.style.transform = `scaleX(${scaleFor(progress, sStart, sEnd)})`;
                if (load) load.style.transform = `scaleX(${scaleFor(bProg, sStart, sEnd)})`;
            });
        }
    }
    requestAnimationFrame(loop);
})();
window.addEventListener('keydown', e => {
    if (['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName) || document.activeElement?.isContentEditable || !['ArrowRight', 'ArrowLeft'].includes(e.key)) return;
    e.stopImmediatePropagation();
    e.preventDefault();
    const v = document.querySelector('video');
    if (v) v.currentTime += e.key === 'ArrowRight' ? 5 : -5;
}, true);