您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Automatically clicks the next chapter button after customizable time, continues timer after audio ends, auto-minimizes on inactivity
// ==UserScript== // @name Auto-Next Chapter // @namespace http://tampermonkey.net/ // @version 1.5 // @description Automatically clicks the next chapter button after customizable time, continues timer after audio ends, auto-minimizes on inactivity // @author You // @match https://inovel*.com/* // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function() { 'use strict'; // Configuration const DEFAULT_MAX_CHAPTERS = 4; // Default maximum number of chapters // Default countdown will be determined based on audio duration or fallback to 7 minutes // Load user preferences from localStorage or use defaults let userSettings = JSON.parse(localStorage.getItem('autoNextSettings')) || {}; let MAX_CHAPTERS = userSettings.maxChapters || DEFAULT_MAX_CHAPTERS; let isMinimized = userSettings.isMinimized || false; // Initialize with a temporary default that will be updated let COUNTDOWN_MINUTES = 7; // Will be updated based on audio duration // Selector for the next chapter button const NEXT_BUTTON_SELECTOR = 'a.nextchap[rel="next"]'; // Selector for the previous chapter button const PREV_BUTTON_SELECTOR = 'a.prevchap[rel="prev"]'; // Auto-minimize settings const INACTIVITY_TIMEOUT = 4000; // Auto-minimize after 4 seconds of inactivity let inactivityTimer = null; // Convert minutes to milliseconds (initial value, will be updated) let countdownMs = COUNTDOWN_MINUTES * 60 * 1000; // Chapter counter - initialize or retrieve from session storage let chaptersNavigated = parseInt(sessionStorage.getItem('auto_next_chapters_count') || '0'); // Check if this is a "next chapter" page by checking session storage const isFirstPage = !sessionStorage.getItem('auto_next_started'); // Timer states let isRunning = false; let isPaused = false; let startTime = 0; let endTime = 0; let remainingTime = countdownMs; let countdownInterval; let settingsPanelOpen = false; let audioHasEnded = false; // Flag to track if audio ended naturally // Create main container const mainContainer = document.createElement('div'); mainContainer.style.cssText = ` position: fixed; bottom: 80px; left: 20px; z-index: 9999; `; document.body.appendChild(mainContainer); // Create the expanded view container const expandedView = document.createElement('div'); expandedView.className = 'auto-next-expanded'; expandedView.style.cssText = ` background-color: rgba(0, 0, 0, 0.7); color: white; padding: 15px; border-radius: 8px; font-size: 16px; font-family: Arial, sans-serif; display: flex; flex-direction: column; align-items: center; gap: 10px; min-width: 180px; max-width: 250px; touch-action: manipulation; `; // Create minimized bubble view with timer const bubbleView = document.createElement('div'); bubbleView.className = 'auto-next-bubble'; bubbleView.style.cssText = ` width: 70px; height: 70px; border-radius: 50%; background-color: rgba(0, 0, 0, 0.7); display: flex; flex-direction: column; justify-content: center; align-items: center; cursor: pointer; padding: 5px; `; // Create timer icon const bubbleIcon = document.createElement('div'); bubbleIcon.style.cssText = ` font-size: 20px; margin-bottom: 2px; `; bubbleIcon.innerHTML = '⏱️'; // Create timer text for bubble const bubbleTimer = document.createElement('div'); bubbleTimer.style.cssText = ` font-size: 12px; color: white; font-weight: bold; `; bubbleTimer.textContent = COUNTDOWN_MINUTES + ':00'; bubbleView.appendChild(bubbleIcon); bubbleView.appendChild(bubbleTimer); // Function to reset the inactivity timer function resetInactivityTimer() { if (inactivityTimer) { clearTimeout(inactivityTimer); inactivityTimer = null; } if (!isMinimized) { inactivityTimer = setTimeout(() => { toggleView(); // Auto-minimize after timeout }, INACTIVITY_TIMEOUT); } } // Function to toggle between views function toggleView() { isMinimized = !isMinimized; updateViewState(); // Reset inactivity timer when toggling resetInactivityTimer(); // Save state userSettings.isMinimized = isMinimized; localStorage.setItem('autoNextSettings', JSON.stringify(userSettings)); } // Function to update the view based on minimized state function updateViewState() { if (isMinimized) { // Show bubble view, hide expanded view if (mainContainer.contains(expandedView)) { mainContainer.removeChild(expandedView); } if (!mainContainer.contains(bubbleView)) { mainContainer.appendChild(bubbleView); } // Clear any inactivity timer if (inactivityTimer) { clearTimeout(inactivityTimer); inactivityTimer = null; } } else { // Show expanded view, hide bubble view if (mainContainer.contains(bubbleView)) { mainContainer.removeChild(bubbleView); } if (!mainContainer.contains(expandedView)) { mainContainer.appendChild(expandedView); } // Start inactivity timer resetInactivityTimer(); } } // Add click handler to bubble bubbleView.addEventListener('click', function(event) { event.stopPropagation(); // Prevent document click from interfering toggleView(); }); // Create timer text element const timerText = document.createElement('div'); timerText.className = 'timer-text'; timerText.style.cssText = ` font-size: 18px; font-weight: bold; margin-bottom: 5px; text-align: center; width: 100%; `; timerText.textContent = `Next chapter in: ${COUNTDOWN_MINUTES}:00`; expandedView.appendChild(timerText); // Create chapter counter text element const chapterCounterText = document.createElement('div'); chapterCounterText.className = 'chapter-counter-text'; chapterCounterText.style.cssText = ` font-size: 14px; color: #ffcc00; margin-bottom: 5px; text-align: center; width: 100%; `; chapterCounterText.textContent = `Chapters: ${chaptersNavigated}/${MAX_CHAPTERS}`; expandedView.appendChild(chapterCounterText); // Create playback rate display element const playbackRateText = document.createElement('div'); playbackRateText.className = 'playback-rate-text'; playbackRateText.style.cssText = ` font-size: 12px; color: #8cf; margin-bottom: 5px; text-align: center; width: 100%; `; const currentPlaybackRate = parseFloat(localStorage.getItem('audio_playback_rate')) || 1.0; playbackRateText.textContent = `Playback Speed: ${currentPlaybackRate.toFixed(1)}x`; expandedView.appendChild(playbackRateText); // Create version display const versionDisplay = document.createElement('div'); versionDisplay.style.cssText = ` color: #aaaaaa; font-size: 9px; text-align: right; width: 100%; margin-bottom: 5px; font-style: italic; `; versionDisplay.textContent = `v1.5`; expandedView.appendChild(versionDisplay); // Create settings panel (initially hidden) const settingsPanel = document.createElement('div'); settingsPanel.style.cssText = ` background-color: rgba(40, 40, 40, 0.95); padding: 12px; border-radius: 5px; margin-top: 8px; display: none; width: 100%; `; // Create minutes input with label const timerSettingContainer = document.createElement('div'); timerSettingContainer.style.cssText = ` display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; `; const timerLabel = document.createElement('label'); timerLabel.textContent = 'Timer (minutes):'; timerLabel.style.marginRight = '10px'; const timerInput = document.createElement('input'); timerInput.type = 'number'; timerInput.min = '0.5'; timerInput.max = '60'; timerInput.step = '0.5'; timerInput.value = COUNTDOWN_MINUTES; timerInput.style.cssText = ` width: 60px; background-color: #333; color: white; border: 1px solid #555; border-radius: 3px; padding: 4px; `; timerSettingContainer.appendChild(timerLabel); timerSettingContainer.appendChild(timerInput); // Create max chapters input with label const maxChaptersContainer = document.createElement('div'); maxChaptersContainer.style.cssText = ` display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; `; const maxChaptersLabel = document.createElement('label'); maxChaptersLabel.textContent = 'Max Chapters:'; maxChaptersLabel.style.marginRight = '10px'; const maxChaptersInput = document.createElement('input'); maxChaptersInput.type = 'number'; maxChaptersInput.min = '1'; maxChaptersInput.max = '20'; maxChaptersInput.step = '1'; maxChaptersInput.value = MAX_CHAPTERS; maxChaptersInput.style.cssText = ` width: 60px; background-color: #333; color: white; border: 1px solid #555; border-radius: 3px; padding: 4px; `; maxChaptersContainer.appendChild(maxChaptersLabel); maxChaptersContainer.appendChild(maxChaptersInput); // Create save and cancel buttons const settingsButtonContainer = document.createElement('div'); settingsButtonContainer.style.cssText = ` display: flex; justify-content: space-between; margin-top: 10px; `; const saveButton = document.createElement('button'); saveButton.textContent = 'Save'; saveButton.style.cssText = ` background-color: #4CAF50; border: none; color: white; padding: 5px 10px; text-align: center; text-decoration: none; font-size: 14px; cursor: pointer; border-radius: 4px; `; const cancelButton = document.createElement('button'); cancelButton.textContent = 'Cancel'; cancelButton.style.cssText = ` background-color: #f44336; border: none; color: white; padding: 5px 10px; text-align: center; text-decoration: none; font-size: 14px; cursor: pointer; border-radius: 4px; `; settingsButtonContainer.appendChild(saveButton); settingsButtonContainer.appendChild(cancelButton); // Add components to settings panel settingsPanel.appendChild(timerSettingContainer); settingsPanel.appendChild(maxChaptersContainer); settingsPanel.appendChild(settingsButtonContainer); // Add settings panel to expanded view expandedView.appendChild(settingsPanel); // Create a button container for all controls const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; flex-direction: column; gap: 10px; margin-top: 5px; width: 100%; `; expandedView.appendChild(buttonContainer); // Create the main multi-function button (Row 1) const actionButton = document.createElement('button'); actionButton.textContent = isFirstPage ? 'Start' : 'Pause'; actionButton.style.cssText = ` background-color: ${isFirstPage ? '#2196F3' : '#4CAF50'}; border: none; color: white; padding: 10px 15px; text-align: center; text-decoration: none; font-size: 17px; font-weight: bold; cursor: pointer; border-radius: 6px; width: 100%; `; buttonContainer.appendChild(actionButton); // Create container for time adjustment buttons (Row 2) const adjustButtonsContainer = document.createElement('div'); adjustButtonsContainer.style.cssText = ` display: flex; justify-content: space-between; width: 100%; gap: 10px; `; // Create -30s button const minusButton = document.createElement('button'); minusButton.textContent = '-30s'; minusButton.style.cssText = ` background-color: #FF9800; border: none; color: white; padding: 8px 0; text-align: center; text-decoration: none; font-size: 14px; cursor: pointer; border-radius: 4px; flex: 1; `; // Create +30s button const plusButton = document.createElement('button'); plusButton.textContent = '+30s'; plusButton.style.cssText = ` background-color: #9C27B0; border: none; color: white; padding: 8px 0; text-align: center; text-decoration: none; font-size: 14px; cursor: pointer; border-radius: 4px; flex: 1; `; // Add time adjustment buttons to their container adjustButtonsContainer.appendChild(minusButton); adjustButtonsContainer.appendChild(plusButton); buttonContainer.appendChild(adjustButtonsContainer); // Create container for minimize and settings buttons (Row 3) const controlButtonsContainer = document.createElement('div'); controlButtonsContainer.style.cssText = ` display: flex; justify-content: space-between; width: 100%; gap: 10px; `; // Create minimize button with text and icon (switched position) const minimizeButton = document.createElement('button'); minimizeButton.innerHTML = '− Minimize'; minimizeButton.style.cssText = ` background-color: #607D8B; border: none; color: white; padding: 8px 0; text-align: center; text-decoration: none; font-size: 14px; cursor: pointer; border-radius: 4px; flex: 1; `; minimizeButton.addEventListener('click', toggleView); // Create settings button with text and icon (switched position) const settingsButton = document.createElement('button'); settingsButton.innerHTML = '⚙️ Settings'; settingsButton.style.cssText = ` background-color: #2196F3; border: none; color: white; padding: 8px 0; text-align: center; text-decoration: none; font-size: 14px; cursor: pointer; border-radius: 4px; flex: 1; `; // Add minimize and settings buttons to their container (switched order) controlButtonsContainer.appendChild(minimizeButton); controlButtonsContainer.appendChild(settingsButton); buttonContainer.appendChild(controlButtonsContainer); // Create Stop button (Row 4) const stopButton = document.createElement('button'); stopButton.textContent = '⛔ Stop Permanently'; stopButton.style.cssText = ` background-color: #f44336; border: none; color: white; padding: 8px 0; text-align: center; text-decoration: none; font-size: 14px; font-weight: bold; cursor: pointer; border-radius: 4px; width: 100%; margin-top: 5px; `; buttonContainer.appendChild(stopButton); // Create container for navigation buttons (Row 5 - new addition) const navButtonsContainer = document.createElement('div'); navButtonsContainer.style.cssText = ` display: flex; justify-content: space-between; width: 100%; gap: 10px; margin-top: 10px; `; // Create Previous Chapter button const prevChapterButton = document.createElement('button'); prevChapterButton.innerHTML = '⬅️ Previous'; prevChapterButton.style.cssText = ` background-color: #FF9800; border: none; color: white; padding: 8px 0; text-align: center; text-decoration: none; font-size: 14px; cursor: pointer; border-radius: 4px; flex: 1; `; // Create Next Chapter button const nextChapterButton = document.createElement('button'); nextChapterButton.innerHTML = 'Next ➡️'; nextChapterButton.style.cssText = ` background-color: #4CAF50; border: none; color: white; padding: 8px 0; text-align: center; text-decoration: none; font-size: 14px; cursor: pointer; border-radius: 4px; flex: 1; `; // Add navigation buttons to their container navButtonsContainer.appendChild(prevChapterButton); navButtonsContainer.appendChild(nextChapterButton); buttonContainer.appendChild(navButtonsContainer); // Function to get audio duration and set default countdown with retry mechanism function setTimerFromAudioDuration() { let retryCount = 0; const MAX_RETRIES = 3; const RETRY_DELAY = 1000; // 1 second between retries const BUFFER_TIME = 10; // 10 seconds buffer time function tryGetDuration() { const audioElements = document.querySelectorAll('audio'); if (audioElements.length > 0) { // Get the first audio element const audio = audioElements[0]; // If duration is already available if (audio.duration && !isNaN(audio.duration) && audio.duration > 0) { // Get current playback rate from localStorage (set by the Audio Controls script) // Default to 1 if not found const playbackRate = parseFloat(localStorage.getItem('audio_playback_rate')) || 1.0; // Set timer to (audio duration ÷ playback speed) + buffer time (converted to minutes) const adjustedDuration = (audio.duration / playbackRate) + BUFFER_TIME; const durationInMinutes = adjustedDuration / 60; COUNTDOWN_MINUTES = Math.ceil(durationInMinutes * 10) / 10; // Round to 1 decimal console.log(`[Auto-Next] Set timer to ${COUNTDOWN_MINUTES} minutes based on audio duration (${audio.duration.toFixed(1)}s) and playback rate (${playbackRate}x) (attempt ${retryCount + 1})`); // Update countdown values countdownMs = COUNTDOWN_MINUTES * 60 * 1000; remainingTime = countdownMs; updateCountdown(); // Update playback rate display playbackRateText.textContent = `Playback Speed: ${playbackRate.toFixed(1)}x`; return true; // Successfully got duration } else { retryCount++; if (retryCount < MAX_RETRIES) { console.log(`[Auto-Next] Couldn't get audio duration, retry ${retryCount}/${MAX_RETRIES}...`); // Try again after delay setTimeout(tryGetDuration, RETRY_DELAY); return false; // Still trying } else { // Max retries reached, use default console.log(`[Auto-Next] Max retries (${MAX_RETRIES}) reached. Using default timer of 7 minutes`); COUNTDOWN_MINUTES = 7; countdownMs = COUNTDOWN_MINUTES * 60 * 1000; remainingTime = countdownMs; updateCountdown(); return true; // Finished with fallback value } } } else { // No audio found after retry if (retryCount < MAX_RETRIES) { retryCount++; console.log(`[Auto-Next] No audio found, retry ${retryCount}/${MAX_RETRIES}...`); setTimeout(tryGetDuration, RETRY_DELAY); return false; } else { // Max retries reached, use default console.log(`[Auto-Next] No audio found after ${MAX_RETRIES} retries. Using default timer of 7 minutes`); COUNTDOWN_MINUTES = 7; countdownMs = COUNTDOWN_MINUTES * 60 * 1000; remainingTime = countdownMs; updateCountdown(); return true; } } } // Listen for metadata loaded event on any audio that appears document.addEventListener('DOMNodeInserted', function(e) { if (e.target.tagName === 'AUDIO' || (e.target.querySelector && e.target.querySelector('audio'))) { const audio = e.target.tagName === 'AUDIO' ? e.target : e.target.querySelector('audio'); if (audio) { audio.addEventListener('loadedmetadata', function() { if (!isRunning && !isPaused && retryCount < MAX_RETRIES) { // If timer hasn't started yet and we're still in retry phase tryGetDuration(); } }); } } }); // Start the first attempt tryGetDuration(); } // Function to monitor playback rate changes function setupPlaybackRateMonitor() { // Check localStorage every 2 seconds for playback rate changes const playbackRateCheckInterval = setInterval(() => { const currentPlaybackRate = parseFloat(localStorage.getItem('audio_playback_rate')) || 1.0; const storedPlaybackRate = parseFloat(localStorage.getItem('auto_next_last_playback_rate')) || currentPlaybackRate; // Update playback rate display regardless of whether it changed playbackRateText.textContent = `Playback Speed: ${currentPlaybackRate.toFixed(1)}x`; // If playback rate has changed significantly (more than 0.01 difference) if (Math.abs(currentPlaybackRate - storedPlaybackRate) > 0.01) { console.log(`[Auto-Next] Playback rate changed from ${storedPlaybackRate}x to ${currentPlaybackRate}x, adjusting timer...`); // Store new rate localStorage.setItem('auto_next_last_playback_rate', currentPlaybackRate.toString()); // Only adjust timer if it's running if (isRunning && !isPaused) { // Calculate ratio of change const ratioChange = storedPlaybackRate / currentPlaybackRate; // Adjust remaining time proportionally // If playback is faster, time should decrease; if slower, time should increase const currentRemainingTime = endTime - Date.now(); const adjustedRemainingTime = currentRemainingTime * ratioChange; // Update end time based on adjusted remaining time endTime = Date.now() + adjustedRemainingTime; remainingTime = adjustedRemainingTime; // Update display immediately updateCountdown(); console.log(`[Auto-Next] Timer adjusted to ${Math.floor(remainingTime / 60000)}:${Math.floor((remainingTime % 60000) / 1000).toString().padStart(2, '0')}`); } } }, 2000); // Check every 2 seconds // Store initial playback rate const initialPlaybackRate = parseFloat(localStorage.getItem('audio_playback_rate')) || 1.0; localStorage.setItem('auto_next_last_playback_rate', initialPlaybackRate.toString()); } // Function to set up audio state listeners function setupAudioStateListeners() { const audioElements = document.querySelectorAll('audio'); if (audioElements.length > 0) { const audio = audioElements[0]; // Listen for play events audio.addEventListener('play', function() { console.log('[Auto-Next] Audio play event detected'); // Start timer if not running yet if (!isRunning && !isPaused) { startTimer(); console.log('[Auto-Next] Starting timer because audio is playing'); } // Resume timer if it was paused else if (isPaused && !isRunning) { resumeTimer(); console.log('[Auto-Next] Resuming timer because audio is playing'); } // Reset ended state if playing again audioHasEnded = false; }); // Listen for pause events - only pause timer if it's not ended audio.addEventListener('pause', function() { console.log('[Auto-Next] Audio pause event detected'); // Check if this is from the ended event if (audioHasEnded) { console.log('[Auto-Next] Ignoring pause event because audio has ended naturally'); return; // Don't pause the timer if audio ended naturally } // Only pause timer if it was manually paused if (isRunning && !isPaused) { pauseTimer(); console.log('[Auto-Next] Pausing timer because audio is paused manually'); } }); // Listen for ended events - CRITICAL: don't pause timer when audio ends naturally audio.addEventListener('ended', function() { console.log('[Auto-Next] Audio ended event detected'); // Mark that audio ended naturally audioHasEnded = true; // IMPORTANT: We DO NOT pause the timer here console.log('[Auto-Next] Audio ended naturally, timer continues running'); // If timer isn't running for some reason, start it if (!isRunning && !isPaused) { startTimer(); console.log('[Auto-Next] Starting timer after audio ended'); } // Force update the countdown to ensure it's still running updateCountdown(); }); // Set initial state based on audio if (audio.paused && !audioHasEnded && isRunning) { pauseTimer(); } } } // Function to handle clicks outside the control panel function setupOutsideClickHandler() { document.addEventListener('click', function(event) { if (!isMinimized) { // Check if click is outside the panel and not on the bubble view if (!expandedView.contains(event.target) && event.target !== expandedView && event.target !== bubbleView && !bubbleView.contains(event.target)) { // Minimize the panel toggleView(); } } }); // Prevent clicks inside the panel from bubbling up expandedView.addEventListener('click', function(event) { event.stopPropagation(); }); } // Function to toggle settings panel function toggleSettingsPanel() { settingsPanelOpen = !settingsPanelOpen; settingsPanel.style.display = settingsPanelOpen ? 'block' : 'none'; // Reset input values to current settings timerInput.value = COUNTDOWN_MINUTES; maxChaptersInput.value = MAX_CHAPTERS; } // Function to save settings function saveSettings() { // Get and validate timer minutes (ensure it's a valid number) const newTimerMinutes = parseFloat(timerInput.value); if (isNaN(newTimerMinutes) || newTimerMinutes < 0.5 || newTimerMinutes > 60) { alert('Please enter a valid time between 0.5 and 60 minutes.'); return; } // Get and validate max chapters const newMaxChapters = parseInt(maxChaptersInput.value); if (isNaN(newMaxChapters) || newMaxChapters < 1 || newMaxChapters > 20) { alert('Please enter a valid number of chapters between 1 and 20.'); return; } // Update settings COUNTDOWN_MINUTES = newTimerMinutes; countdownMs = COUNTDOWN_MINUTES * 60 * 1000; // Update max chapters MAX_CHAPTERS = newMaxChapters; chapterCounterText.textContent = `Chapters: ${chaptersNavigated}/${MAX_CHAPTERS}`; // If timer is not running, update the remaining time if (!isRunning) { remainingTime = countdownMs; updateCountdown(); } // Save to localStorage userSettings.timerMinutes = COUNTDOWN_MINUTES; userSettings.maxChapters = MAX_CHAPTERS; localStorage.setItem('autoNextSettings', JSON.stringify(userSettings)); // Close settings panel toggleSettingsPanel(); // Reset inactivity timer after settings change resetInactivityTimer(); } // Settings button click handler settingsButton.addEventListener('click', toggleSettingsPanel); // Save button click handler saveButton.addEventListener('click', saveSettings); // Cancel button click handler cancelButton.addEventListener('click', toggleSettingsPanel); // Function to start the timer function startTimer() { isRunning = true; isPaused = false; startTime = Date.now(); endTime = startTime + remainingTime; // Store in session storage that we've started sessionStorage.setItem('auto_next_started', 'true'); // Update button actionButton.textContent = 'Pause'; actionButton.style.backgroundColor = '#4CAF50'; // Start the countdown interval if (!countdownInterval) { countdownInterval = setInterval(updateCountdown, 1000); } // Reset inactivity timer when starting resetInactivityTimer(); } // Function to pause the timer function pauseTimer() { isPaused = true; isRunning = false; // Store the remaining time when paused remainingTime = Math.max(0, endTime - Date.now()); // Update button actionButton.textContent = 'Resume'; actionButton.style.backgroundColor = '#f44336'; // Reset inactivity timer when pausing resetInactivityTimer(); } // Function to resume the timer function resumeTimer() { isPaused = false; isRunning = true; // Recalculate the end time based on the remaining time endTime = Date.now() + remainingTime; // Update button actionButton.textContent = 'Pause'; actionButton.style.backgroundColor = '#4CAF50'; // Reset inactivity timer when resuming resetInactivityTimer(); } // Update the countdown display function updateCountdown() { if (isRunning && !isPaused) { remainingTime = Math.max(0, endTime - Date.now()); } const minutesLeft = Math.floor(remainingTime / 60000); const secondsLeft = Math.floor((remainingTime % 60000) / 1000); const formattedTime = `${minutesLeft}:${secondsLeft.toString().padStart(2, '0')}`; // Update the timer text in expanded view timerText.textContent = `Next chapter in: ${formattedTime}`; // Update the timer text in bubble view if (bubbleTimer) { bubbleTimer.textContent = formattedTime; } if (remainingTime <= 0 && isRunning && !isPaused) { clearInterval(countdownInterval); countdownInterval = null; clickNextChapter(); } // Reset inactivity timer when updating display (user is watching) resetInactivityTimer(); } // Button click handler - cycles through Start, Pause, Resume actionButton.addEventListener('click', function() { if (!isRunning && !isPaused) { // Start the timer startTimer(); } else if (isRunning && !isPaused) { // Pause the timer pauseTimer(); } else if (!isRunning && isPaused) { // Resume the timer resumeTimer(); } // Reset inactivity timer after button click resetInactivityTimer(); }); // Add 30 seconds to the timer plusButton.addEventListener('click', function() { // Only allow adjustment if timer is running or paused if (isRunning || isPaused) { // If paused, just adjust the remaining time if (isPaused) { remainingTime += 30000; // 30 seconds in milliseconds } else { // If running, adjust the end time endTime += 30000; } // Update the display immediately updateCountdown(); } // Reset inactivity timer after button click resetInactivityTimer(); }); // Subtract 30 seconds from the timer minusButton.addEventListener('click', function() { // Only allow adjustment if timer is running or paused if (isRunning || isPaused) { if (isPaused) { // Don't let it go below zero remainingTime = Math.max(0, remainingTime - 30000); } else { // Adjust end time but don't let it go below current time endTime = Math.max(Date.now(), endTime - 30000); // Recalculate remaining time remainingTime = Math.max(0, endTime - Date.now()); } // Update the display immediately updateCountdown(); // If we reduced to zero, trigger next chapter if (remainingTime <= 0 && isRunning) { clearInterval(countdownInterval); countdownInterval = null; clickNextChapter(); } } // Reset inactivity timer after button click resetInactivityTimer(); }); // Function to check if we should stop due to chapter limit function checkChapterLimit() { if (chaptersNavigated >= MAX_CHAPTERS) { // Update the UI to show we've reached the limit timerText.textContent = `Reached limit of ${MAX_CHAPTERS} chapters`; chapterCounterText.textContent = `Chapters: ${chaptersNavigated}/${MAX_CHAPTERS} - Limit reached!`; chapterCounterText.style.color = '#ff6666'; // Reset the chapter counter after reaching the limit chaptersNavigated = 0; sessionStorage.setItem('auto_next_chapters_count', '0'); // Stop the timer stopPermanently(); return true; } return false; } // Function to click the next chapter button function clickNextChapter() { // Increment chapter counter before checking chaptersNavigated++; sessionStorage.setItem('auto_next_chapters_count', chaptersNavigated.toString()); // Update chapter counter display chapterCounterText.textContent = `Chapters: ${chaptersNavigated}/${MAX_CHAPTERS}`; // Check if we've reached the limit if (checkChapterLimit()) { // We've reached the limit, don't proceed return; } // Update the timer text timerText.textContent = 'Moving to next chapter...'; // Try to find the next chapter button const nextButton = document.querySelector(NEXT_BUTTON_SELECTOR); if (nextButton) { // Highlight the button being clicked const originalBackground = nextButton.style.backgroundColor; const originalTransition = nextButton.style.transition; nextButton.style.transition = 'background-color 0.3s ease'; nextButton.style.backgroundColor = 'yellow'; // Click after a short delay to show the highlight setTimeout(() => { nextButton.click(); // If for some reason we're still on the page after clicking setTimeout(() => { nextButton.style.backgroundColor = originalBackground; nextButton.style.transition = originalTransition; timerText.textContent = 'Click failed or redirecting...'; }, 1000); }, 500); } else { timerText.textContent = 'Next button not found! Adjust the selector in the script.'; // Error message will remain visible } } // Function to permanently stop the script function stopPermanently() { // Clear any running intervals if (countdownInterval) { clearInterval(countdownInterval); countdownInterval = null; } // Reset states isRunning = false; isPaused = false; // Reset chapter counter chaptersNavigated = 0; sessionStorage.setItem('auto_next_chapters_count', '0'); // Update UI timerText.textContent = 'Timer stopped permanently'; chapterCounterText.textContent = `Chapters: ${chaptersNavigated}/${MAX_CHAPTERS}`; chapterCounterText.style.color = '#ffcc00'; // Reset color if (bubbleTimer) { bubbleTimer.textContent = 'Stopped'; } // Disable timer control buttons but NOT navigation buttons actionButton.disabled = true; minusButton.disabled = true; plusButton.disabled = true; stopButton.disabled = true; // Keep navigation buttons enabled // Change button appearances for timer controls only actionButton.style.backgroundColor = '#999'; minusButton.style.backgroundColor = '#999'; plusButton.style.backgroundColor = '#999'; stopButton.style.backgroundColor = '#999'; stopButton.textContent = 'Stopped'; } // Function to navigate to previous chapter function goToPrevChapter() { const prevButton = document.querySelector(PREV_BUTTON_SELECTOR); if (prevButton) { // Highlight the button being clicked const originalBackground = prevButton.style.backgroundColor; const originalTransition = prevButton.style.transition; prevButton.style.transition = 'background-color 0.3s ease'; prevButton.style.backgroundColor = 'yellow'; // Click after a short delay to show the highlight setTimeout(() => { prevButton.click(); }, 300); } else { timerText.textContent = 'Previous chapter button not found!'; } // Reset inactivity timer after navigation resetInactivityTimer(); } // Function to navigate to next chapter immediately function goToNextChapter() { const nextButton = document.querySelector(NEXT_BUTTON_SELECTOR); if (nextButton) { // Increment chapter counter (just like the auto-next function) chaptersNavigated++; sessionStorage.setItem('auto_next_chapters_count', chaptersNavigated.toString()); // Update chapter counter display chapterCounterText.textContent = `Chapters: ${chaptersNavigated}/${MAX_CHAPTERS}`; // Check if we've reached the limit before navigating if (checkChapterLimit()) { // We've reached the limit, don't proceed return; } // Highlight the button being clicked const originalBackground = nextButton.style.backgroundColor; const originalTransition = nextButton.style.transition; nextButton.style.transition = 'background-color 0.3s ease'; nextButton.style.backgroundColor = 'yellow'; // Click after a short delay to show the highlight setTimeout(() => { nextButton.click(); }, 300); } else { timerText.textContent = 'Next chapter button not found!'; } // Reset inactivity timer after navigation resetInactivityTimer(); } // Add stop button click handler stopButton.addEventListener('click', stopPermanently); // Add click handlers for navigation buttons prevChapterButton.addEventListener('click', goToPrevChapter); nextChapterButton.addEventListener('click', goToNextChapter); // Initialize - try to get audio duration first function initialize() { console.log('[Auto-Next] Initializing script v1.5'); // Set timer based on audio duration setTimerFromAudioDuration(); // Set up audio state listeners setupAudioStateListeners(); // Set up playback rate change monitor setupPlaybackRateMonitor(); // Set up outside click handler setupOutsideClickHandler(); // Reset inactivity timer when panel is expanded resetInactivityTimer(); // Add event listeners for interactivity expandedView.addEventListener('mouseenter', resetInactivityTimer); expandedView.addEventListener('mousemove', resetInactivityTimer); expandedView.addEventListener('click', resetInactivityTimer); expandedView.addEventListener('touchstart', resetInactivityTimer); // Check if we've already reached the chapter limit if (chaptersNavigated >= MAX_CHAPTERS) { checkChapterLimit(); } else { // Set initial view state based on preference updateViewState(); // Don't automatically start timer - wait for audio play event instead // Update countdown display initially updateCountdown(); } } // If DOM is already loaded, initialize immediately if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { initialize(); } // Try again after full page load (helps with audio elements loaded dynamically) window.addEventListener('load', function() { // Check if audio elements are now available const audioElements = document.querySelectorAll('audio'); if (audioElements.length > 0 && !isRunning && !isPaused) { // Re-set timer if needed setTimerFromAudioDuration(); setupAudioStateListeners(); } }); })();