YouTube Permanent ProgressBar

Keeps YouTube progress bar visible all the time.

当前为 2025-02-03 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         YouTube Permanent ProgressBar
// @namespace    http://tampermonkey.net/
// @version      0.3.6
// @description  Keeps YouTube progress bar visible all the time.
// @author       ChromiaCat
// @match        *://www.youtube.com/*
// @license      MIT
// ==/UserScript==

(function() {
    "use strict";

    var style = document.createElement('style');
    var to = { createHTML: s => s },
        tp = window.trustedTypes?.createPolicy ? trustedTypes.createPolicy("", to) : to,
        html = s => tp.createHTML(s);
    style.type = 'text/css';
    style.innerHTML = html(
        '.ytp-autohide .ytp-chrome-bottom{opacity:1!important;display:block!important}' +
        '.ytp-autohide .ytp-chrome-bottom .ytp-progress-bar-container{bottom:-1px!important}' +
        '.ytp-autohide .ytp-chrome-bottom .ytp-chrome-controls{opacity:0!important}' +
        '.ytp-progress-bar-container:not(:hover) .ytp-scrubber-container{display:none!important}'
    );
    document.getElementsByTagName('head')[0].appendChild(style);

    var permanentProgressBar = {
        options: {
            PROGRESSBAR_OPACITY_WINDOW: 1,
            PROGRESSBAR_OPACITY_FULLSCREEN: 0.5,
            UPDATE_VIDEO_TIMER: true,
            UPDATE_PROGRESSBAR: true,
            UPDATE_BUFFERBAR: true,
        },

        // Converts current video time to a human-readable format.
        prettifyVideoTime: function(video) {
            let seconds = "" + Math.floor(video.currentTime % 60);
            let minutes = "" + Math.floor((video.currentTime % 3600) / 60);
            let hours = "" + Math.floor(video.currentTime / 3600);
            if (video.currentTime / 60 > 60) {
                return `${hours}:${minutes.padStart(2, '0')}:${seconds.padStart(2, '0')}`;
            } else {
                return `${minutes}:${seconds.padStart(2, '0')}`;
            }
        },

        // For progress mode, return current time; for buffer mode, return the end of the buffered range that contains currentTime.
        getDuration: function(video, type) {
            if (type === "PROGRESSBAR") {
                return video.currentTime;
            } else if (type === "BUFFERBAR") {
                if (video.buffered.length > 0) {
                    for (let i = 0; i < video.buffered.length; i++) {
                        if (video.currentTime >= video.buffered.start(i) && video.currentTime <= video.buffered.end(i)) {
                            return video.buffered.end(i);
                        }
                    }
                }
                return 0;
            }
        },

        // Updates the current time display on the player.
        updateCurrentTimeField: function(player) {
            const video = player.querySelector("video");
            const currentTimeEl = player.querySelector(".ytp-time-current");
            if (!video || !currentTimeEl) return;
            currentTimeEl.innerText = permanentProgressBar.prettifyVideoTime(video);
        },

        // For non-chaptered videos, update the progress and buffer bars directly.
        updateOverallProgressBar: function(player) {
            const video = player.querySelector("video");
            const progressBar = player.querySelector(".ytp-play-progress");
            const bufferBar = player.querySelector(".ytp-load-progress");
            if (!video || !progressBar || !bufferBar) return;
            if (!video.duration) return;

            let progressRatio = video.currentTime / video.duration;
            let bufferRatio = this.getDuration(video, "BUFFERBAR") / video.duration;

            progressBar.style.transform = `scaleX(${Math.min(1, progressRatio.toFixed(5))})`;
            bufferBar.style.transform = `scaleX(${Math.min(1, bufferRatio.toFixed(5))})`;
        },

        // For chaptered videos, update each chapter element directly based on current time.
        updateProgressBarWithChapters: function(player, type) {
            const video = player.querySelector("video");
            if (!video || isNaN(video.duration)) return;

            // Get the chapter elements and corresponding progress elements.
            const chapterElements = player.getElementsByClassName("ytp-progress-bar-padding");
            let chapterProgressEls;
            if (type === "PROGRESSBAR") {
                chapterProgressEls = player.getElementsByClassName("ytp-play-progress");
            } else if (type === "BUFFERBAR") {
                chapterProgressEls = player.getElementsByClassName("ytp-load-progress");
            }
            if (!chapterElements || !chapterProgressEls) return;

            // Compute total width of the progress bar (sum of all chapter widths)
            let totalWidth = 0;
            for (let i = 0; i < chapterElements.length; i++) {
                totalWidth += chapterElements[i].offsetWidth;
            }
            const durationWidthRatio = video.duration / totalWidth;

            let accumulatedWidth = 0;
            for (let i = 0; i < chapterElements.length; i++) {
                const chapterWidth = chapterElements[i].offsetWidth;
                const chapterEndTime = durationWidthRatio * (accumulatedWidth + chapterWidth);
                const chapterStartTime = durationWidthRatio * accumulatedWidth;
                let currentTimeForType = this.getDuration(video, type);
                let ratio;
                if (currentTimeForType >= chapterEndTime) {
                    ratio = 1;
                } else if (currentTimeForType < chapterStartTime) {
                    ratio = 0;
                } else {
                    ratio = (currentTimeForType - chapterStartTime) / (chapterEndTime - chapterStartTime);
                }
                chapterProgressEls[i].style.transform = `scaleX(${Math.min(1, ratio.toFixed(5))})`;
                accumulatedWidth += chapterWidth;
            }
        },

        // The main update function which selects chapter-mode or overall mode.
        update: function() {
            const player = document.querySelector(".html5-video-player");
            if (!player) return;

            if (this.options.UPDATE_VIDEO_TIMER) {
                this.updateCurrentTimeField(player);
            }

            // If chapter elements exist, update chapter-mode; otherwise use overall mode.
            let chapterElements = player.getElementsByClassName("ytp-progress-bar-padding");
            if (chapterElements.length > 0) {
                if (this.options.UPDATE_PROGRESSBAR) {
                    this.updateProgressBarWithChapters(player, "PROGRESSBAR");
                }
                if (this.options.UPDATE_BUFFERBAR) {
                    this.updateProgressBarWithChapters(player, "BUFFERBAR");
                }
            } else {
                this.updateOverallProgressBar(player);
            }
        },

        // The update loop runs on each animation frame.
        updateLoop: function() {
            permanentProgressBar.update();
            requestAnimationFrame(permanentProgressBar.updateLoop);
        },

        start: function() {
            requestAnimationFrame(this.updateLoop);
        }
    };

    permanentProgressBar.start();
})();