Enhanced Audio Speed Controller with Time Info, Speed Highlight, and Toggle

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

当前为 2024-09-26 提交的版本,查看 最新版本

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

(function () {
    'use strict';

    let startTime = Date.now(); // Track the real-time start
    let isPanelVisible = true;  // Track panel visibility

    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

        // Format the time string to always show two digits
        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; // Correct percentage format

            // Time remaining at current speed
            const timeRemaining = (adjustedDuration - currentTimeDisp);
            // Total elapsed wall clock time (accounting for pauses)
            const elapsedWallClockTime = (Date.now() - startTime) / 1000 / 60;

            // Update the DOM elements with the values
            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() {
        // Check if control panel already exists
        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%'; // Position it at 20% from the top (adjust as needed)
        controlDiv.style.right = '0'; // Align to the far right edge
        controlDiv.style.background = 'rgba(0, 0, 0, 0.15)'; // 85% transparent background
        controlDiv.style.padding = '5px';
        controlDiv.style.borderRadius = '5px';
        controlDiv.style.zIndex = '999999'; // High z-index
        controlDiv.style.display = 'flex';
        controlDiv.style.flexDirection = 'column'; // Stack buttons vertically
        controlDiv.style.fontSize = '7pt'; // Make font smaller as requested
        controlDiv.style.transition = 'transform 0.5s ease'; // Smooth horizontal sliding transition

        // Section for time stats
        const timeStats = document.createElement('div');
        timeStats.style.marginBottom = '4px'; // Spacing above the buttons
        timeStats.style.fontSize = '6pt';
        timeStats.style.color = 'white';
        timeStats.style.fontWeight = "bold";
        timeStats.style.textAlign = 'center';

        // Define an array of objects, where each object contains the id and innerHTML
        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 }
        ];

        // Loop over the timeStatsData array to create and append each stat element
        timeStatsData.forEach(stat => {
            const statDiv = document.createElement('div');
            statDiv.id = stat.id;
            statDiv.innerHTML = stat.label + "<br>--:--:--"; // Placeholder until data is available
            timeStats.appendChild(statDiv);
        });

        // Append all the time stats
        controlDiv.appendChild(timeStats);

        // Now we create the speed control buttons as before
        const speeds = [1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 3.25, 3.5];
        let activeButton = null; // Track the currently active button

        speeds.forEach(speed => {
            const button = document.createElement('button');
            button.innerText = speed.toFixed(2); // Label each button with the speed
            button.style.padding = '3px 5px'; // Small button size
            button.style.marginBottom = '2px'; // Small margin between buttons
            button.style.backgroundColor = '#bada55';
            button.style.border = 'none';
            button.style.borderRadius = '3px';
            button.style.cursor = 'pointer';
            button.style.fontSize = '8px';
            button.style.fontWeight = 'bold'; // Small font size
            button.style.color = '#222';
            button.style.width = '36px'; // Small width for the buttons

            // Function to set button as active
            function setActiveButton() {
                if (activeButton) {
                    // Reset previously active button's style
                    activeButton.style.backgroundColor = '#bada55';
                    activeButton.style.color = '#222';
                    activeButton.style.fontWeight = 'normal';
                }
                // Set new active button's style
                button.style.backgroundColor = '#006400'; // Dark green background
                button.style.color = '#fff'; // White text
                button.style.fontWeight = 'bold'; // Bold text
                activeButton = button; // Set this as the active button
            }

            // Set audio speed when button is clicked
            button.addEventListener('click', function () {
                const audioElement = document.querySelector('audio');
                if (audioElement) {
                    audioElement.playbackRate = speed;
                    setActiveButton(); // Highlight the active button
                }
            });

            controlDiv.appendChild(button);
        });

        // Create toggle button attached to the top-left of the control panel
        const toggleButton = document.createElement('button');
        toggleButton.innerText = '◀'; // Icon for sliding out/in
        toggleButton.style.position = 'absolute';
        toggleButton.style.top = '5px';
        toggleButton.style.left = '-15px'; // Positioned just outside the left of the control panel
        toggleButton.style.backgroundColor = '#bada55';
        toggleButton.style.border = 'none';
        toggleButton.style.borderRadius = '20%';
        toggleButton.style.cursor = 'pointer';
        toggleButton.style.padding = '2px';
        toggleButton.style.zIndex = '1000'; // Ensure it stays on top

        // Event listener for the toggle button
        toggleButton.addEventListener('click', () => {
            if (isPanelVisible) {
                controlDiv.style.transform = 'translateX(100%)'; // Slide horizontally to the right
                toggleButton.innerText = '▶'; // Change icon to indicate sliding back
            } else {
                controlDiv.style.transform = 'translateX(0)'; // Slide back to the original position
                toggleButton.innerText = '◀'; // Change icon back
            }
            isPanelVisible = !isPanelVisible; // Toggle visibility flag
        });

        // Append toggle button to the control panel
        controlDiv.appendChild(toggleButton);

        // Simplify the control panel sliding transition
        controlDiv.style.transition = 'transform 0.5s ease'; // Smooth horizontal sliding transition

        // Append the control panel to the body
        document.body.appendChild(controlDiv);


    }

    // CSS to rotate the emoji
    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) {
            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
    });

})();