Enhanced Audio Speed Controller with Time Info, Speed Highlight, Font Size Control, and Toggle

Adds time information (duration, currentTime, etc.), adjusts for playback speed, highlights the active speed button, toggle for hiding/showing the control panel, and font size control.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name Enhanced Audio Speed Controller with Time Info, Speed Highlight, Font Size Control, and Toggle
// @namespace http://tampermonkey.net/
// @version 3.3
// @description Adds time information (duration, currentTime, etc.), adjusts for playback speed, highlights the active speed button, toggle for hiding/showing the control panel, and font size control.
// @author You
// @match *://*/*
// @grant none
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    let startTime = Date.now(); // Track the real-time start
    let isPanelVisible = true; // Track panel visibility
    let currentFontSize = 7; // Initial font size in pt

    const odIcon = '🕰️';
    const adIcon = '⏰';
    const ctIcon = '⌚';
    const pcIcon = '➗';
    const trIcon = '⏳';
    const wcIcon = '🕛';

    // Helper function to convert fractional minutes into hh:mm:ss format
    function convertToTimeFormat(minutes) {
        const totalSeconds = Math.floor(minutes * 60); // Convert minutes to seconds
        const hours = Math.floor(totalSeconds / 3600); // Calculate full hours
        const remainingSeconds = totalSeconds % 3600; // Remaining seconds after hours
        const mins = Math.floor(remainingSeconds / 60); // Full minutes
        const secs = remainingSeconds % 60; // Remaining seconds

        const formattedTime =
            (hours > 9 ? hours : '0' + hours) + ':' +
            (mins > 9 ? mins : '0' + mins) + ':' +
            (secs > 9 ? secs : '0' + secs);

        return formattedTime;
    }

    // Function to calculate and update time stats
    function updateTimeStats() {
        const audioElement = document.querySelector('audio');
        if (audioElement && audioElement.duration && !isNaN(audioElement.duration)) {
            const duration = audioElement.duration;
            const adjustedDuration = audioElement.duration / audioElement.playbackRate / 60;
            const currentTime = audioElement.currentTime;
            const playbackRate = audioElement.playbackRate;
            const currentTimeDisp = currentTime / 60 / playbackRate;
            const percentComplete = (currentTime / duration) * 100;

            const timeRemaining = (adjustedDuration - currentTimeDisp);
            const elapsedWallClockTime = (Date.now() - startTime) / 1000 / 60;

            document.getElementById('original-duration').innerHTML = `${odIcon}<br>${convertToTimeFormat(duration / 60)}`;
            document.getElementById('adjusted-duration').innerHTML = `${adIcon}<br>${convertToTimeFormat(adjustedDuration)}`;
            document.getElementById('current-time').innerHTML = `${ctIcon}<br>${convertToTimeFormat(currentTimeDisp)}`;
            document.getElementById('percent-complete').innerHTML = `<span class="rotate">${pcIcon}</span><br>${percentComplete.toFixed(2)}%`;
            document.getElementById('time-remaining').innerHTML = `${trIcon}<br>${convertToTimeFormat(timeRemaining)}`;
            document.getElementById('elapsed-wall-clock').innerHTML = `${wcIcon}<br>${convertToTimeFormat(elapsedWallClockTime)}`;
        }
    }

    // Function to create the control panel with buttons and time information
    function createControlPanel() {
        if (document.getElementById('audio-speed-control')) return;

        const controlDiv = document.createElement('div');
        controlDiv.id = 'audio-speed-control';
        controlDiv.style.position = 'fixed';
        controlDiv.style.top = '20%';
        controlDiv.style.right = '0';
        controlDiv.style.background = 'rgba(0, 0, 0, 0.05)';
        controlDiv.style.padding = '5px';
        controlDiv.style.borderRadius = '5px';
        controlDiv.style.zIndex = '999999';
        controlDiv.style.display = 'flex';
        controlDiv.style.flexDirection = 'column';
        controlDiv.style.fontSize = `${currentFontSize}pt`;
        controlDiv.style.transition = 'transform 0.5s ease';

        const timeStats = document.createElement('div');
        timeStats.style.marginBottom = '4px';
        timeStats.style.fontSize = '6pt';
        timeStats.style.color = 'black';
        timeStats.style.fontWeight = "bold";
        timeStats.style.textAlign = 'center';

        const timeStatsData = [
            { id: 'original-duration', label: odIcon },
            { id: 'adjusted-duration', label: adIcon },
            { id: 'current-time', label: ctIcon },
            { id: 'percent-complete', label: pcIcon },
            { id: 'time-remaining', label: trIcon },
            { id: 'elapsed-wall-clock', label: wcIcon }
        ];

        timeStatsData.forEach(stat => {
            const statDiv = document.createElement('div');
            statDiv.id = stat.id;
            statDiv.innerHTML = stat.label + "<br>--:--:--";
            timeStats.appendChild(statDiv);
        });

        controlDiv.appendChild(timeStats);

             // Create PLAY button
        const playButton = document.createElement('button');
        playButton.innerText = '▶'; // Label with play symbol
        playButton.style.padding = '3px 5px';
        playButton.style.marginBottom = '2px';
        playButton.style.backgroundColor = '#bada55';
        playButton.style.border = 'none';
        playButton.style.borderRadius = '3px';
        playButton.style.cursor = 'pointer';
        playButton.style.fontSize = '8px';
        playButton.style.fontWeight = 'bold';
        playButton.style.color = '#222';
        playButton.style.width = '36px';

        playButton.addEventListener('click', function () {
            const audioElement = document.querySelector('audio');
            if (audioElement) {
                let attempts = 25;
                audioElement.play(); // Initial attempt

                const playInterval = setInterval(() => {
                    if (attempts > 0) {
                        audioElement.play(); // Force play again
                        attempts--;
                    } else {
                        clearInterval(playInterval);
                    }
                }, 125);
            }
        });

        controlDiv.appendChild(playButton);

        // Create PAUSE button
        const pauseButton = document.createElement('button');
        pauseButton.innerText = '||'; // Label with pause symbol
        pauseButton.style.padding = '3px 5px';
        pauseButton.style.marginBottom = '2px';
        pauseButton.style.backgroundColor = '#bada55';
        pauseButton.style.border = 'none';
        pauseButton.style.borderRadius = '3px';
        pauseButton.style.cursor = 'pointer';
        pauseButton.style.fontSize = '8px';
        pauseButton.style.fontWeight = 'bold';
        pauseButton.style.color = '#222';
        pauseButton.style.width = '36px';

        pauseButton.addEventListener('click', function () {
            const audioElement = document.querySelector('audio');
            if (audioElement) {
                let attempts = 25;
                audioElement.pause(); // Initial attempt

                const pauseInterval = setInterval(() => {
                    if (attempts > 0) {
                        audioElement.pause(); // Force pause again
                        attempts--;
                    } else {
                        clearInterval(pauseInterval);
                    }
                }, 125);
            }
        });

        controlDiv.appendChild(pauseButton);

        /*
        // Ceate PAUoE button
        cst pauseButton = document.createElement('button');
        pauseButton.innerText = '||'; // Label with pause symbol
        pauseButton.style.padding = '3px 5px';
        pauseButton.style.marginBottom = '2px';
        pauseButton.style.backgroundColor = '#bada55';
        pauseButton.style.border = 'none';
        pauseButton.style.borderRadius = '3px';
        pauseButton.style.cursor = 'pointer';
        pauseButton.style.fontSize = '8px';
        = 'bold';
        = '#222';
');
            if (audioElement) {
                const targetState = audioElement.paused; // Determine the desired state

                function tryTogglePlayPause() {
                    if (targetState) {
                        audioElement.play().catch(error => {
                            console.error("Error playing audio:", error);
                            setTimeout(tryTogglePlayPause, 100);
                        });
                        pauseButton.innerText = '||';
                    } else {
                        audioElement.pause();
                        pauseButton.innerText = '▶';
                    }
                }

                tryTogglePlayPause(); // Initial attempt
                if (targetState) {
                    // If the target state is playing, schedule retries
                    setTimeout(tryTogglePlayPause, 100);
                }
            }
        });


// pauseButton.addEventListener('click', function () {
// const audioElement = document.querySelector('audio');
// if (audioElement) {
// if (audioElement.paused) {
// audioElement.play();
// pauseButton.innerText = '||';
// } else {
// audioElement.pause();
// pauseButton.innerText = '▶';
// }
// }
// });

       // controlDiv.appendChild(pauseButton);
       */

        const speeds = [1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 3.25, 3.5, 3.75, 4.0, 4.5, 5.0];
        let activeButton = null;

        speeds.forEach(speed => {
            const button = document.createElement('button');
            button.innerText = speed.toFixed(2);
            button.style.padding = '3px 5px';
            button.style.marginBottom = '2px';
            button.style.backgroundColor = '#bada55';
            button.style.border = 'none';
            button.style.borderRadius = '3px';
            button.style.cursor = 'pointer';
            button.style.fontSize = '8px';
            button.style.fontWeight = 'bold';
            button.style.color = '#222';
            button.style.width = '36px';

            function setActiveButton() {
                if (activeButton) {
                    activeButton.style.backgroundColor = '#bada55';
                    activeButton.style.color = '#222';
                    activeButton.style.fontWeight = 'normal';
                }
                button.style.backgroundColor = '#006400';
                button.style.color = '#fff';
                button.style.fontWeight = 'bold';
                activeButton = button;
            }

            button.addEventListener('click', function () {
                const audioElement = document.querySelector('audio');
                if (audioElement) {
                    audioElement.playbackRate = speed;
                    setActiveButton();
                }
            });

            controlDiv.appendChild(button);
        });

        const toggleButton = document.createElement('button');
        toggleButton.innerText = '◀';
        toggleButton.style.position = 'absolute';
        toggleButton.style.top = '5px';
        toggleButton.style.left = '-15px';
        toggleButton.style.backgroundColor = '#bada55';
        toggleButton.style.border = 'none';
        toggleButton.style.borderRadius = '20%';
        toggleButton.style.cursor = 'pointer';
        toggleButton.style.padding = '2px';
        toggleButton.style.zIndex = '1000';

        toggleButton.addEventListener('click', () => {
            if (isPanelVisible) {
                controlDiv.style.transform = 'translateX(100%)';
                toggleButton.innerText = '▶';
            } else {
                controlDiv.style.transform = 'translateX(0)';
                toggleButton.innerText = '◀';
            }
            isPanelVisible = !isPanelVisible;
        });

        controlDiv.appendChild(toggleButton);

        // Font size control
        const fontSizeControl = document.createElement('div');
        fontSizeControl.style.marginTop = '4px';

        const fontIncreaseButton = document.createElement('button');
        fontIncreaseButton.innerText = '⬆';
        fontIncreaseButton.style.margin = '2px';
        fontIncreaseButton.style.padding = '3px';
        fontIncreaseButton.style.cursor = 'pointer';

        fontIncreaseButton.addEventListener('click', () => {
            currentFontSize += 2;
            console.log("Size: " + currentFontSize);
            controlDiv.style.fontSize = `${currentFontSize}pt`;
        });

        const fontDecreaseButton = document.createElement('button');
        fontDecreaseButton.innerText = '⬇';
        fontDecreaseButton.style.margin = '2px';
        fontDecreaseButton.style.padding = '3px';
        fontDecreaseButton.style.cursor = 'pointer';

        fontDecreaseButton.addEventListener('click', () => {
            currentFontSize = Math.max(4, currentFontSize - 2); // Prevent font size from going too small
            controlDiv.style.fontSize = `${currentFontSize}pt`;
        });

        fontSizeControl.appendChild(fontIncreaseButton);
        fontSizeControl.appendChild(fontDecreaseButton);

        controlDiv.appendChild(fontSizeControl);

        document.body.appendChild(controlDiv);
    }

    const style = document.createElement('style');
    style.innerHTML = `
        .rotate {
            display: inline-block;
            transform: rotate(45deg); /* Rotates emoji */
        }
    `;
    document.head.appendChild(style);

    // Update the time stats periodically
    setInterval(updateTimeStats, 1000); // Update every second

    // Wait for the document to fully load and ensure audio element exists
    const observer = new MutationObserver((mutations, observer) => {
        const audioElement = document.querySelector('audio');
        if (audioElement) {
            console.log("Chapter duration:");
            console.log(audioElement.duration / 60);
            audioElement.addEventListener('loadedmetadata', () => {
                audioElement.playbackRate = 2.0; // Set default playback speed to 2.0
                createControlPanel();
                updateTimeStats();
            });
            observer.disconnect(); // Stop observing once the audio is found
        }
    });

    // Start observing the document for changes
    observer.observe(document, {
        childList: true,
        subtree: true
    });

})();