您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Controls audio playback with speed adjustment and auto-play
当前为
// ==UserScript== // @name Audio Controls with Auto-Play and Speed Management // @namespace http://tampermonkey.net/ // @version 1.4 // @description Controls audio playback with speed adjustment and auto-play // @author You // @match https://inovel12.com/* // @grant none // ==/UserScript== (function() { 'use strict'; // Configuration const DEFAULT_PLAYBACK_RATE = 0.7; const AUTO_PLAY_DELAY = 5000; // 5 seconds const AUDIO_SELECTOR = 'audio[controls]'; const MAX_RETRY_ATTEMPTS = 5; // Maximum number of retry attempts const RETRY_DELAY = 1000; // Delay between retries in milliseconds // State variables let audioElement = null; let playbackRate = parseFloat(localStorage.getItem('audio_playback_rate')) || DEFAULT_PLAYBACK_RATE; let isMinimized = localStorage.getItem('audio_controls_minimized') === 'true' || false; let countdownTimer = null; let retryAttempts = 0; // Position settings - load from localStorage or use defaults let bubblePosition = JSON.parse(localStorage.getItem('audio_bubble_position')) || { top: '20px', left: '20px' }; // Create main container for all controls const mainContainer = document.createElement('div'); mainContainer.style.cssText = ` position: fixed; z-index: 9999; font-family: Arial, sans-serif; `; document.body.appendChild(mainContainer); // Create the expanded control panel const controlPanel = document.createElement('div'); controlPanel.className = 'audio-control-panel'; controlPanel.style.cssText = ` background-color: rgba(0, 0, 0, 0.7); padding: 10px; border-radius: 8px; display: flex; flex-direction: column; gap: 8px; min-width: 180px; `; // Create the minimized bubble view const bubbleView = document.createElement('div'); bubbleView.className = 'audio-bubble'; bubbleView.style.cssText = ` width: 40px; height: 40px; border-radius: 50%; background-color: rgba(0, 0, 0, 0.7); display: flex; justify-content: center; align-items: center; cursor: pointer; user-select: none; `; // Create bubble icon const bubbleIcon = document.createElement('div'); bubbleIcon.style.cssText = ` font-size: 20px; color: white; `; bubbleIcon.innerHTML = '🔊'; bubbleView.appendChild(bubbleIcon); // Create duration display const durationDisplay = document.createElement('div'); durationDisplay.style.cssText = ` color: white; font-size: 14px; text-align: center; margin-bottom: 5px; `; durationDisplay.textContent = 'Audio Duration: --:--'; controlPanel.appendChild(durationDisplay); // Create countdown display const countdownDisplay = document.createElement('div'); countdownDisplay.style.cssText = ` color: #ffcc00; font-size: 14px; text-align: center; margin-bottom: 8px; font-weight: bold; height: 20px; /* Fixed height to prevent layout shifts */ `; countdownDisplay.textContent = ''; controlPanel.appendChild(countdownDisplay); // Create play/pause button const playPauseButton = document.createElement('button'); playPauseButton.style.cssText = ` background-color: #4CAF50; border: none; color: white; padding: 8px 12px; text-align: center; font-size: 14px; border-radius: 4px; cursor: pointer; width: 100%; `; playPauseButton.textContent = '▶️ Play'; controlPanel.appendChild(playPauseButton); // Create speed control container const speedControlContainer = document.createElement('div'); speedControlContainer.style.cssText = ` display: flex; gap: 8px; width: 100%; `; controlPanel.appendChild(speedControlContainer); // Create speed down button const speedDownButton = document.createElement('button'); speedDownButton.style.cssText = ` background-color: #795548; border: none; color: white; padding: 8px 12px; text-align: center; font-size: 14px; border-radius: 4px; cursor: pointer; flex: 1; `; speedDownButton.textContent = '🐢 Slower'; speedControlContainer.appendChild(speedDownButton); // Create speed up button const speedUpButton = document.createElement('button'); speedUpButton.style.cssText = ` background-color: #009688; border: none; color: white; padding: 8px 12px; text-align: center; font-size: 14px; border-radius: 4px; cursor: pointer; flex: 1; `; speedUpButton.textContent = '🐇 Faster'; speedControlContainer.appendChild(speedUpButton); // Create speed display const speedDisplay = document.createElement('div'); speedDisplay.style.cssText = ` color: white; font-size: 14px; text-align: center; margin-top: 5px; `; speedDisplay.textContent = `Speed: ${playbackRate.toFixed(1)}x`; controlPanel.appendChild(speedDisplay); // Create minimize button const minimizeButton = document.createElement('button'); minimizeButton.style.cssText = ` background-color: #607D8B; border: none; color: white; padding: 6px 10px; text-align: center; font-size: 12px; border-radius: 4px; cursor: pointer; margin-top: 8px; `; minimizeButton.textContent = '− Minimize'; controlPanel.appendChild(minimizeButton); // Function to toggle between expanded and minimized views function toggleMinimized() { isMinimized = !isMinimized; updateViewState(); // Save state to localStorage localStorage.setItem('audio_controls_minimized', isMinimized); } // Function to update the current view based on minimized state function updateViewState() { // Clear the container first while (mainContainer.firstChild) { mainContainer.removeChild(mainContainer.firstChild); } if (isMinimized) { // Show bubble view mainContainer.appendChild(bubbleView); // Set position based on saved values mainContainer.style.top = bubblePosition.top; mainContainer.style.left = bubblePosition.left; mainContainer.style.right = 'auto'; mainContainer.style.bottom = 'auto'; } else { // Show expanded control panel mainContainer.appendChild(controlPanel); // If coming from minimized state, place in the same position // Otherwise use default bottom right if (bubblePosition) { mainContainer.style.top = bubblePosition.top; mainContainer.style.left = bubblePosition.left; mainContainer.style.right = 'auto'; mainContainer.style.bottom = 'auto'; } else { mainContainer.style.top = 'auto'; mainContainer.style.left = 'auto'; mainContainer.style.right = '20px'; mainContainer.style.bottom = '20px'; } } } // Make only the bubble draggable let isDragging = false; let dragOffsetX = 0; let dragOffsetY = 0; bubbleView.addEventListener('mousedown', function(e) { // Only initiate drag if user holds for a brief moment setTimeout(() => { if (e.buttons === 1) { // Left mouse button isDragging = true; dragOffsetX = e.clientX - mainContainer.getBoundingClientRect().left; dragOffsetY = e.clientY - mainContainer.getBoundingClientRect().top; bubbleView.style.cursor = 'grabbing'; } }, 100); }); document.addEventListener('mousemove', function(e) { if (!isDragging || !isMinimized) return; e.preventDefault(); // Calculate new position const newLeft = e.clientX - dragOffsetX; const newTop = e.clientY - dragOffsetY; // Keep within viewport bounds const maxX = window.innerWidth - bubbleView.offsetWidth; const maxY = window.innerHeight - bubbleView.offsetHeight; mainContainer.style.left = `${Math.max(0, Math.min(maxX, newLeft))}px`; mainContainer.style.top = `${Math.max(0, Math.min(maxY, newTop))}px`; }); document.addEventListener('mouseup', function(event) { if (isDragging && isMinimized) { isDragging = false; bubbleView.style.cursor = 'pointer'; // Save the position bubblePosition = { top: mainContainer.style.top, left: mainContainer.style.left }; localStorage.setItem('audio_bubble_position', JSON.stringify(bubblePosition)); // Prevent click if we were dragging event.preventDefault(); return false; } else if (isMinimized && (event.target === bubbleView || bubbleView.contains(event.target))) { // If it was a click (not drag) on the bubble, expand toggleMinimized(); } }); // Add touch support for mobile devices - only for bubble bubbleView.addEventListener('touchstart', function(e) { const touch = e.touches[0]; isDragging = true; dragOffsetX = touch.clientX - mainContainer.getBoundingClientRect().left; dragOffsetY = touch.clientY - mainContainer.getBoundingClientRect().top; // Prevent scrolling while dragging e.preventDefault(); }); document.addEventListener('touchmove', function(e) { if (!isDragging || !isMinimized) return; const touch = e.touches[0]; // Calculate new position const newLeft = touch.clientX - dragOffsetX; const newTop = touch.clientY - dragOffsetY; // Keep within viewport bounds const maxX = window.innerWidth - bubbleView.offsetWidth; const maxY = window.innerHeight - bubbleView.offsetHeight; mainContainer.style.left = `${Math.max(0, Math.min(maxX, newLeft))}px`; mainContainer.style.top = `${Math.max(0, Math.min(maxY, newTop))}px`; // Prevent scrolling while dragging e.preventDefault(); }); document.addEventListener('touchend', function(event) { if (isDragging && isMinimized) { isDragging = false; // Save the position bubblePosition = { top: mainContainer.style.top, left: mainContainer.style.left }; localStorage.setItem('audio_bubble_position', JSON.stringify(bubblePosition)); // If touch distance was very small, treat as click const touchMoved = Math.abs(event.changedTouches[0].clientX - (parseInt(mainContainer.style.left) + dragOffsetX)) > 5 || Math.abs(event.changedTouches[0].clientY - (parseInt(mainContainer.style.top) + dragOffsetY)) > 5; if (!touchMoved && (event.target === bubbleView || bubbleView.contains(event.target))) { toggleMinimized(); } } }); // Add click event for minimize button minimizeButton.addEventListener('click', toggleMinimized); // Function to find audio element function findAudioElement() { const audio = document.querySelector(AUDIO_SELECTOR); if (audio && audio !== audioElement) { audioElement = audio; initializeAudio(); } if (!audioElement) { // If not found, try again in 500ms setTimeout(findAudioElement, 500); } } // Function to format time in MM:SS function formatTime(seconds) { const minutes = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${minutes}:${secs.toString().padStart(2, '0')}`; } // Function to run the countdown timer function startCountdown(seconds) { // Clear any existing countdown if (countdownTimer) { clearInterval(countdownTimer); } let remainingSeconds = seconds; updateCountdownDisplay(remainingSeconds); countdownTimer = setInterval(() => { remainingSeconds--; updateCountdownDisplay(remainingSeconds); if (remainingSeconds <= 0) { clearInterval(countdownTimer); countdownTimer = null; // Play the audio when countdown reaches zero if (audioElement) { // Reset retry counter before attempting to play retryAttempts = 0; playAudioWithRetry(); } } }, 1000); } // Function to update countdown display function updateCountdownDisplay(seconds) { countdownDisplay.textContent = `Auto-play in ${seconds} seconds`; } // Function to play audio with retry mechanism function playAudioWithRetry() { if (!audioElement) return; // Attempt to play the audio audioElement.play() .then(() => { // Success - update UI and reset retry counter updatePlayPauseButton(); countdownDisplay.textContent = ''; // Clear countdown display retryAttempts = 0; }) .catch(err => { // Error playing audio - retry if under max attempts retryAttempts++; if (retryAttempts <= MAX_RETRY_ATTEMPTS) { // Update the countdown display with retry information countdownDisplay.textContent = `Auto-play blocked. Retrying (${retryAttempts}/${MAX_RETRY_ATTEMPTS})...`; // Try to play again after a delay setTimeout(() => { togglePlayPause(); // This will call play() again }, RETRY_DELAY); } else { // Max retries reached countdownDisplay.textContent = 'Auto-play failed after multiple attempts'; retryAttempts = 0; } }); } // Function to initialize audio controls function initializeAudio() { if (!audioElement) return; // Set saved playback rate audioElement.playbackRate = playbackRate; // Update UI based on current state updatePlayPauseButton(); // Get duration when metadata is loaded and start countdown if (audioElement.readyState >= 1) { handleAudioLoaded(); } else { audioElement.addEventListener('loadedmetadata', handleAudioLoaded); } // Add event listeners audioElement.addEventListener('play', updatePlayPauseButton); audioElement.addEventListener('pause', updatePlayPauseButton); audioElement.addEventListener('ended', updatePlayPauseButton); } // Function to handle audio loaded event function handleAudioLoaded() { if (!audioElement) return; // Update duration display if (!isNaN(audioElement.duration)) { durationDisplay.textContent = `Audio Duration: ${formatTime(audioElement.duration)}`; } // Start countdown for auto-play (5 seconds) const countdownSeconds = Math.floor(AUTO_PLAY_DELAY / 1000); startCountdown(countdownSeconds); } // Function to update play/pause button state function updatePlayPauseButton() { if (!audioElement) return; if (audioElement.paused) { playPauseButton.textContent = '▶️ Play'; playPauseButton.style.backgroundColor = '#4CAF50'; } else { playPauseButton.textContent = '⏸️ Pause'; playPauseButton.style.backgroundColor = '#F44336'; // If playing, clear countdown if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; countdownDisplay.textContent = ''; } } } // Function to toggle play/pause function togglePlayPause() { if (!audioElement) return; if (audioElement.paused) { // Try to play with retry mechanism playAudioWithRetry(); } else { audioElement.pause(); updatePlayPauseButton(); } } // Function to update playback speed function updatePlaybackSpeed(newRate) { playbackRate = newRate; // Update audio element if exists if (audioElement) { audioElement.playbackRate = playbackRate; } // Update display speedDisplay.textContent = `Speed: ${playbackRate.toFixed(1)}x`; // Save to localStorage localStorage.setItem('audio_playback_rate', playbackRate); } // Function to decrease playback speed function decreaseSpeed() { const newRate = Math.max(0.5, playbackRate - 0.1); updatePlaybackSpeed(newRate); } // Function to increase playback speed function increaseSpeed() { const newRate = Math.min(2.5, playbackRate + 0.1); updatePlaybackSpeed(newRate); } // Set up event listeners for buttons playPauseButton.addEventListener('click', togglePlayPause); speedDownButton.addEventListener('click', decreaseSpeed); speedUpButton.addEventListener('click', increaseSpeed); // Create an observer to watch for new audio elements const audioObserver = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.addedNodes.length) { mutation.addedNodes.forEach(function(node) { if (node.nodeName === 'AUDIO' || (node.nodeType === 1 && node.querySelector(AUDIO_SELECTOR))) { // Reset audio element and reinitialize audioElement = null; findAudioElement(); } }); } }); }); // Start observing the document audioObserver.observe(document.body, { childList: true, subtree: true }); // Initialize the view state updateViewState(); // Start finding audio element findAudioElement(); })();