Greasy Fork

Greasy Fork is available in English.

小叶的b站视频时间查询器

查询b站视频总时长并计算倍速下的观看时间,支持窗口拖动与动态调整显示样式!

当前为 2024-10-22 提交的版本,查看 最新版本

// ==UserScript==
// @name 小叶的b站视频时间查询器
// @namespace http://tampermonkey.net/
// @version 1.0.2
// @description 查询b站视频总时长并计算倍速下的观看时间,支持窗口拖动与动态调整显示样式!
// @author 小叶
// @license AGPL License
// @match *://*.bilibili.com/video/*
// @icon https://www.bilibili.com/favicon.ico
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==

(function() {
 'use strict';

    let isPopupVisible = false;
    let containerOpacity = GM_getValue('containerOpacity', 0.8);

    const createPopupTrigger = () => {
        // 删除现有的触发器(如果存在)
        const existingTrigger = document.getElementById('popup-trigger-container');
        if (existingTrigger) {
            existingTrigger.remove();
        }

        const body = document.body;
        const triggerContainer = document.createElement("div");
        triggerContainer.id = "popup-trigger-container";

        // 修改了容器的样式
        triggerContainer.style.cssText = `
            position: fixed;
            left: 0;
            top: 50%;
            transform: translateY(-50%);
            z-index: 999999;
            text-align: center;
            border: 1px solid #00A1D6;
            border-radius: 8px;
            background-color: rgba(255, 255, 255, ${containerOpacity});
            padding: 8px;
            width: 40px;
            transition: all 0.3s ease;
            cursor: pointer;
            margin-left: 5px;
        `;

        // 创建并设置图标
        const icon = document.createElement("img");
        icon.src = "https://www.bilibili.com/favicon.ico";
        icon.alt = "B站图标";
        icon.style.cssText = `
            width: 24px;
            height: 24px;
            display: block;
            margin: 0 auto;
            transition: transform 0.3s ease;
        `;

        // 创建文本容器
        const textContainer = document.createElement("div");
        textContainer.style.cssText = `
            font-size: 12px;
            color: #00A1D6;
            margin-top: 4px;
            white-space: nowrap;
            overflow: hidden;
            display: none;
        `;
        textContainer.innerText = "小叶计时器";

        // 添加hover效果
        triggerContainer.onmouseenter = () => {
            triggerContainer.style.width = '80px';
            textContainer.style.display = 'block';
        };

        triggerContainer.onmouseleave = () => {
            if (!isPopupVisible) {
                triggerContainer.style.width = '40px';
                textContainer.style.display = 'none';
            }
        };

        // 添加点击事件
        triggerContainer.onclick = togglePopup;

        // 组装触发器
        triggerContainer.appendChild(icon);
        triggerContainer.appendChild(textContainer);
        body.appendChild(triggerContainer);

        return triggerContainer;
    };

const togglePopup = () => {
    isPopupVisible = !isPopupVisible;
    const triggerContainer = document.getElementById('popup-trigger-container');
    const textContainer = triggerContainer.querySelector('div'); // 获取文本容器

    if (isPopupVisible) {
        createUI();
        triggerContainer.style.width = '80px';
        textContainer.style.display = 'block';
        textContainer.style.color = '#FF0000';
        textContainer.innerText = '关闭计时器';
    } else {
        closeUI();
        triggerContainer.style.width = '40px';
        textContainer.style.color = '#00A1D6';
        textContainer.innerText = '小叶计时器';

        // 恢复hover效果
        triggerContainer.onmouseenter = () => {
            triggerContainer.style.width = '80px';
            textContainer.style.display = 'block';
        };
        triggerContainer.onmouseleave = () => {
            triggerContainer.style.width = '40px';
            textContainer.style.display = 'none';
        };
    }
};
    const createUI = () => {
        const existingDiv = document.getElementById('time-calculator-container');
        if (existingDiv) {
            existingDiv.remove();
        }

        const body = document.body;
        const container = document.createElement("div");
        container.id = "time-calculator-container";
        container.style.cssText = `padding: 20px; background-color: rgba(255, 255, 255, ${containerOpacity}); position: fixed; left: 20px; top: 20%; width: 280px; max-width: 90%; border-radius: 16px; box-shadow: 0 8px 16px rgba(0,0,0,0.2); border: 1px solid #40E0D0; z-index: 999; text-align: center; font-size: 14px; color: #333;`;

        makeElementDraggable(container);

        const closeButton = document.createElement("button");
        closeButton.innerText = "关闭";
        closeButton.style.cssText = "position: absolute; top: 5px; right: 5px; border: none; background-color: #FF6347; color: #FFF; padding: 5px 10px; cursor: pointer; border-radius: 4px;";
        closeButton.onclick = togglePopup;
        container.appendChild(closeButton);

        const title = document.createElement("h4");
        title.innerText = "小叶的B站时间查询器";
        title.style.cssText = "margin-bottom: 20px; color: #00A1D6; font-weight: bold; text-align: center;";
        container.appendChild(title);

        const inputDiv = document.createElement("div");
        inputDiv.style.cssText = "margin-bottom: 15px; display: flex; justify-content: center; align-items: center;";

        const label1 = document.createElement("label");
        label1.innerText = "从第";
        label1.style.cssText = "margin-right: 5px;";
        inputDiv.appendChild(label1);

        const input1 = document.createElement('input');
        input1.type = "number";
        input1.style.cssText = "border: 1px solid deepskyblue; width: 50px; text-align: center; margin-right: 5px; padding: 5px; border-radius: 4px;";
        input1.min = 1;
        inputDiv.appendChild(input1);

        const label2 = document.createElement("label");
        label2.innerText = "集 到";
        label2.style.cssText = "margin-right: 5px;";
        inputDiv.appendChild(label2);

        const input2 = document.createElement('input');
        input2.type = "number";
        input2.style.cssText = "border: 1px solid deepskyblue; width: 50px; text-align: center; padding: 5px; border-radius: 4px;";
        input2.min = 1;
        inputDiv.appendChild(input2);

        container.appendChild(inputDiv);

        const speedDiv = document.createElement("div");
        speedDiv.style.cssText = "margin-bottom: 15px; display: flex; justify-content: center; align-items: center;";

        const label3 = document.createElement("label");
        label3.innerText = "倍速:";
        label3.style.cssText = "margin-right: 5px;";
        speedDiv.appendChild(label3);

        const input3 = document.createElement('input');
        input3.type = "number";
        input3.style.cssText = "border: 1px solid deepskyblue; width: 60px; text-align: center; padding: 5px; border-radius: 4px; margin-right: 5px;";
        input3.value = 1;
        input3.min = 0.5;
        input3.step = 0.1;
        speedDiv.appendChild(input3);

        const label4 = document.createElement("label");
        label4.innerText = " 倍";
        speedDiv.appendChild(label4);

        container.appendChild(speedDiv);

        const formatDiv = document.createElement("div");
        formatDiv.style.cssText = "margin-bottom: 20px; display: flex; justify-content: center; align-items: center;";

        const formatLabel = document.createElement("label");
        formatLabel.innerText = "显示格式:";
        formatLabel.style.cssText = "margin-right: 5px;";
        formatDiv.appendChild(formatLabel);

        const formatSelect = document.createElement('select');
        formatSelect.style.cssText = "padding: 5px; border-radius: 4px; border: 1px solid deepskyblue;";
        const options = ["时分秒", "仅小时", "仅分钟", "仅秒"];
        options.forEach(optionText => {
            const option = document.createElement('option');
            option.value = optionText;
            option.innerText = optionText;
            formatSelect.appendChild(option);
        });
        formatDiv.appendChild(formatSelect);
        container.appendChild(formatDiv);

        const transparencyDiv = document.createElement("div");
        transparencyDiv.style.cssText = "margin-bottom: 20px; text-align: center;";

        const transparencyLabel = document.createElement("label");
        transparencyLabel.innerText = "调整透明度:";
        transparencyDiv.appendChild(transparencyLabel);

        const transparencySlider = document.createElement('input');
        transparencySlider.type = "range";
        transparencySlider.min = 0.1;
        transparencySlider.max = 1;
        transparencySlider.step = 0.1;
        transparencySlider.value = containerOpacity;
        transparencySlider.style.cssText = "margin-left: 10px;";
        transparencySlider.oninput = (e) => {
            containerOpacity = e.target.value;
            container.style.backgroundColor = `rgba(255, 255, 255, ${containerOpacity})`;
            const triggerContainer = document.getElementById('popup-trigger-container');
            if (triggerContainer) {
                triggerContainer.style.backgroundColor = `rgba(255, 255, 255, ${containerOpacity})`;
            }
            GM_setValue('containerOpacity', containerOpacity);
        };
        transparencyDiv.appendChild(transparencySlider);

        container.appendChild(transparencyDiv);

        const btn = document.createElement('button');
        btn.innerText = "计算时间";
        btn.style.cssText = "width: 100%; padding: 12px; border: none; background-color: #00A1D6; color: #FFFFFF; cursor: pointer; border-radius: 8px; font-size: 16px; margin-bottom: 20px;";
        btn.onmouseover = () => { btn.style.backgroundColor = "#008BB5"; };
        btn.onmouseout = () => { btn.style.backgroundColor = "#00A1D6"; };
        btn.onclick = () => calculateTime(formatSelect.value);
        container.appendChild(btn);

        const resultDiv = document.createElement("div");
        resultDiv.id = "resultDiv";
        resultDiv.style.cssText = "margin-top: 15px; color: #333; font-weight: bold; text-align: center;";
        container.appendChild(resultDiv);

        const footer = document.createElement("div");
        footer.innerText = "小叶计时器";
        footer.style.cssText = "margin-top: 20px; color: #888; font-size: 12px; text-align: center;";
        container.appendChild(footer);

        body.appendChild(container);
    };

    const closeUI = () => {
        const existingDiv = document.getElementById('time-calculator-container');
        if (existingDiv) {
            existingDiv.remove();
        }
    };

    const calculateTime = (format) => {
        const input1Value = parseInt(document.querySelectorAll('input[type=number]')[0].value, 10);
        const input2Value = parseInt(document.querySelectorAll('input[type=number]')[1].value, 10);
        const speedValue = parseFloat(document.querySelectorAll('input[type=number]')[2].value);

        if (isNaN(input1Value) || isNaN(input2Value) || isNaN(speedValue)) {
            updateResult("请输入有效的数值。");
            return;
        }

        if (input1Value < 1 || input2Value < input1Value) {
            updateResult("输入的集数范围不正确。");
            return;
        }

        const durations = document.getElementsByClassName('duration');
        if (durations.length === 0) {
            updateResult("无法获取视频时长,请确保已加载视频列表。");
            return;
        }

        if (input2Value > durations.length) {
            updateResult(`最大为第${durations.length}集`);
            document.querySelectorAll('input[type=number]')[1].value = durations.length;
            return;
        }

        let totalSeconds = 0;
        for (let i = input1Value - 1; i < input2Value; i++) {
            const timeParts = durations[i].innerText.split(':').map(Number);
            let seconds = timeParts.pop();
            let minutes = timeParts.pop() || 0;
            let hours = timeParts.pop() || 0;
            totalSeconds += hours * 3600 + minutes * 60 + seconds;
        }

        totalSeconds /= speedValue;

        // 预先计算时间值
        const hours = Math.floor(totalSeconds / 3600);
        const minutes = Math.floor((totalSeconds % 3600) / 60);
        const seconds = Math.floor(totalSeconds % 60);

        let resultText;
        switch (format) {
            case "时分秒":
                resultText = `总时长:${hours}时${minutes}分${seconds}秒`;
                break;
            case "仅小时":
                resultText = `总时长:${(totalSeconds / 3600).toFixed(2)} 小时`;
                break;
            case "仅分钟":
                resultText = `总时长:${(totalSeconds / 60).toFixed(2)} 分钟`;
                break;
            case "仅秒":
                resultText = `总时长:${Math.round(totalSeconds)} 秒`;
                break;
        }
        updateResult(resultText);
    };

  const updateResult = (text) => {
        const resultDiv = document.getElementById('resultDiv');
        resultDiv.innerText = text;
        setTimeout(() => {
            resultDiv.innerText = '';
        }, 15000); // Clear the message after 15 seconds
    };

    const makeElementDraggable = (element) => {
        let offsetX = 0, offsetY = 0, isDragging = false;

        element.addEventListener('mousedown', (e) => {
            isDragging = true;
            offsetX = e.clientX - element.getBoundingClientRect().left;
            offsetY = e.clientY - element.getBoundingClientRect().top;
            element.style.transition = "none";
        });

        document.addEventListener('mousemove', (e) => {
            if (!isDragging) return;
            element.style.left = `${e.clientX - offsetX}px`;
            element.style.top = `${e.clientY - offsetY}px`;
        });

        document.addEventListener('mouseup', () => {
            isDragging = false;
            element.style.transition = "all 0.3s ease";
        });
    };

    const makeElementDraggableWithSnap = (element) => {
        let offsetX = 0, offsetY = 0, isDragging = false;

        element.addEventListener('mousedown', (e) => {
            isDragging = true;
            offsetX = e.clientX - element.getBoundingClientRect().left;
            offsetY = e.clientY - element.getBoundingClientRect().top;
            element.style.transition = "none";
        });

        document.addEventListener('mousemove', (e) => {
            if (!isDragging) return;
            element.style.left = `${e.clientX - offsetX}px`;
            element.style.top = `${e.clientY - offsetY}px`;
        });

        document.addEventListener('mouseup', () => {
            isDragging = false;
            element.style.transition = "all 0.3s ease";
            snapToEdge(element);
        });
    };

    const snapToEdge = (element) => {
        const windowWidth = window.innerWidth;
        const windowHeight = window.innerHeight;
        const elementRect = element.getBoundingClientRect();

        const snapPadding = 20;
        const snapToLeft = elementRect.left < snapPadding;
        const snapToRight = windowWidth - elementRect.right < snapPadding;
        const snapToTop = elementRect.top < snapPadding;
        const snapToBottom = windowHeight - elementRect.bottom < snapPadding;

        if (snapToLeft) {
            element.style.left = "0px";
        } else if (snapToRight) {
            element.style.left = `${windowWidth - elementRect.width}px`;
        }

        if (snapToTop) {
            element.style.top = "0px";
        } else if (snapToBottom) {
            element.style.top = `${windowHeight - elementRect.height}px`;
        }
    };

    createPopupTrigger();
})();