Greasy Fork

Greasy Fork is available in English.

左上显示视频剩余时长及进度条

显示视频剩余时间和内置进度条

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         左上显示视频剩余时长及进度条
// @author       He
// @version      1.5
// @description  显示视频剩余时间和内置进度条
// @match        *://*/*
// @exclude      *://*live*/*
// @exclude     *://www.huya.com/*
// @exclude     *://www.douyu.com/*
// @exclude     *://www.yy.com/*
// @namespace http://greasyfork.icu/users/808960
// ==/UserScript==

(function() {
    'use strict';

    const containerCache = new WeakMap();

    function setupVideoTimeDisplay(video) {
        if (containerCache.has(video)) return;

        const container = document.createElement('div');
        container.className = 'video-time-display-container';
        container.style.cssText = `
            position: absolute;
            left: 10px;
            top: 10px;
            z-index: 1000;
            display: none; /* 初始隐藏容器 */
        `;

        const timeDisplay = document.createElement('div');
        timeDisplay.className = 'video-time-display';
        timeDisplay.style.cssText = `
            width: 100px;
            color: #C8DCC8;
            background: rgba(0, 0, 0, 0.5);
            padding: 1px 1px 5px 1px;
            font-size: 22px;
            text-align: center;
            border-radius: 5px;
            position: relative;
        `;

        const timeText = document.createElement('div');
        timeText.className = 'video-time-text';
        timeText.style.cssText = `
            line-height: 1.2;
            user-select: none;
        `;

        const progressBar = document.createElement('div');
        progressBar.className = 'video-progress-bar';
        progressBar.style.cssText = `
            width: 95%;
            height: 2px;
            background: rgba(255, 255, 255, 0.3);
            position: absolute;
            bottom: 5px;
            left: 2px;
            overflow: hidden;
        `;

        const bufferedBar = document.createElement('div');
        bufferedBar.className = 'video-buffered-bar';
        bufferedBar.style.cssText = `
            width: 0%;
            height: 100%;
            background: #FF6A00;
            position: absolute;
            left: 0;
            transition: width 0.3s ease;
        `;

        const progressBarInner = document.createElement('div');
        progressBarInner.className = 'video-progress-bar-inner';
        progressBarInner.style.cssText = `
            width: 0%;
            height: 100%;
            background: skyblue;
            position: absolute;
            left: 0;
            transition: width 0.3s ease;
        `;

        progressBar.append(bufferedBar, progressBarInner);
        timeDisplay.append(timeText, progressBar);
        container.append(timeDisplay);

        let parent = video.parentElement;
        while (parent && getComputedStyle(parent).position !== 'relative') {
            parent = parent.parentElement;
        }
        (parent || document.body).append(container);

        containerCache.set(video, container);

        let isUpdating = false;

        const updateDisplay = () => {
            if (isUpdating) return;
            isUpdating = true;

            requestAnimationFrame(() => {
                // 当视频时长无效时隐藏容器
                if (!isFinite(video.duration)) {
                    container.style.display = 'none';
                    isUpdating = false;
                    return;
                }

                // 计算并更新时间显示
                const remaining = video.duration - video.currentTime;
                const mins = String(Math.floor(remaining / 60)).padStart(2, '0');
                const secs = String(Math.floor(remaining % 60)).padStart(2, '0');
                timeText.textContent = `${mins}:${secs}`;

                // 更新进度条
                const progressPercent = (video.currentTime / video.duration) * 100;
                progressBarInner.style.width = `${progressPercent}%`;

                if (video.buffered.length > 0) {
                    const lastBuffer = video.buffered.end(video.buffered.length - 1);
                    const bufferPercent = (lastBuffer / video.duration) * 100;
                    bufferedBar.style.width = `${bufferPercent}%`;
                }

                // 有有效时间时显示容器
                container.style.display = '';
                isUpdating = false;
            });
        };

        const events = ['timeupdate', 'progress', 'loadedmetadata'];
        events.forEach(e => video.addEventListener(e, updateDisplay, { passive: true }));

        updateDisplay();
    }

    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    if (node.tagName === 'VIDEO') {
                        setupVideoTimeDisplay(node);
                    } else {
                        const videos = node.querySelectorAll('video');
                        videos.forEach(video => setupVideoTimeDisplay(video));
                    }
                }
            });
        });
    });

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

    document.querySelectorAll('video').forEach(setupVideoTimeDisplay);
})();