// ==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();
}
});
})();