// ==UserScript== // @name [COMIC] AUTO SCROLL // @namespace Violentmonkey Scripts // @icon https://comix.to/assets/uploads/35595e3de3c99889c1bd2c56f3e3714fc0c457.png // @version 1.0.0 // @updateURL https://raw.githubusercontent.com/downloaddoctor/userscripts/main/%5BCOMIC%5D%20AUTO%20SCROLL.user.js // @downloadURL https://raw.githubusercontent.com/downloaddoctor/userscripts/main/%5BCOMIC%5D%20AUTO%20SCROLL.user.js // @match https://comix.to/* // @grant none // @author - // @description Optimized auto scroll for comic reader // ==/UserScript== // ============ CONFIGURATION ============ const CONFIG = { PAUSE_DURATION: 500, LOAD_TIMEOUT: 15000, MIN_SPEED: 1, MAX_SPEED: 20, DEFAULT_SPEED: 3, SPEED_PRESETS: [1, 2, 3, 4, 5, 8, 10, 15, 20], CHECK_INTERVAL: 100, CONTROLS_CHECK_INTERVAL: 500, MAX_CONTROLS_ATTEMPTS: 50, } // ============ STATE MANAGEMENT ============ const state = { scrolling: false, speed: CONFIG.DEFAULT_SPEED, animationFrame: null, isRestarting: false, scrollElement: null, scrollStartPosition: 0, currentPageHeight: 0, } // ============ THEME ============ const blackTheme = { '--bg': '#000000', '--bg-rgb': '0 0 0', '--bg-2': '#050505', '--bg-2-rgb': '5 5 5', '--surface': '#0a0a0a', '--surface-rgb': '10 10 10', '--surface-2': '#121212', '--surface-2-rgb': '18 18 18', '--surface-3': '#1a1a1a', '--surface-3-rgb': '26 26 26', } // ============ DOM CACHE ============ const DOM = { root: document.documentElement, autoScrollBtn: null, speedDisplay: null, pages: () => document.querySelectorAll('.rpage-page'), floatCtl: () => document.querySelector('.rpage-floatctl__col'), } // ============ UTILITY FUNCTIONS ============ const debounce = (fn, delay) => { let timeoutId return (...args) => { clearTimeout(timeoutId) timeoutId = setTimeout(() => fn(...args), delay) } } // ============ THEME APPLICATION ============ function applyTheme() { const entries = Object.entries(blackTheme) requestAnimationFrame(() => { entries.forEach(([property, value]) => { DOM.root.style.setProperty(property, value) }) }) } // ============ PAGE MANAGEMENT ============ function getCurrentVisiblePage() { const pages = DOM.pages() if (!pages.length) return null let mostVisiblePage = null let maxVisibility = 0 const viewportHeight = window.innerHeight for (let i = 0; i < pages.length; i++) { const rect = pages[i].getBoundingClientRect() const visibleTop = Math.max(0, rect.top) const visibleBottom = Math.min(viewportHeight, rect.bottom) const visibleHeight = Math.max(0, visibleBottom - visibleTop) if (visibleHeight > maxVisibility) { maxVisibility = visibleHeight mostVisiblePage = pages[i] } } if (mostVisiblePage) { state.currentPageHeight = mostVisiblePage.getBoundingClientRect().height } return mostVisiblePage } function hasButtonToClick(page) { return page?.querySelector('button, [role="button"]') !== null } function clickButtonInPage(page) { const button = page?.querySelector('button, [role="button"]') if (button) { button.click() return true } return false } function isPageFullyLoaded(page) { if (!page) return true const children = page.children if (children.length !== 1) return false const tagName = children[0].tagName.toLowerCase() return tagName === 'img' || tagName === 'canvas' } // ============ SCROLL MANAGEMENT ============ function getScrollElement() { if (state.scrollElement && state.scrollElement.isConnected) { return state.scrollElement } const fullscreen = document.fullscreenElement if (fullscreen?.scrollHeight > fullscreen?.clientHeight) { state.scrollElement = fullscreen return fullscreen } let el = document.elementFromPoint( window.innerWidth / 2, window.innerHeight / 2, ) while (el && el !== document.body) { const style = getComputedStyle(el) if ( (style.overflowY === 'auto' || style.overflowY === 'scroll') && el.scrollHeight > el.clientHeight ) { state.scrollElement = el return el } el = el.parentElement } state.scrollElement = document.scrollingElement || document.documentElement return state.scrollElement } function scrollToNextPage() { const pages = DOM.pages() if (!pages.length || state.currentPageIndex >= pages.length - 1) { return false } const nextPage = pages[state.currentPageIndex + 1] if (nextPage) { nextPage.scrollIntoView({ behavior: 'smooth', block: 'start' }) return true } return false } // ============ PAGE LOAD WAITING ============ function waitForCurrentPageLoad() { return new Promise((resolve) => { const startTime = Date.now() const checkPage = () => { if (Date.now() - startTime > CONFIG.LOAD_TIMEOUT) { resolve(true) return } const currentPage = getCurrentVisiblePage() if (isPageFullyLoaded(currentPage)) { resolve(true) return } if (hasButtonToClick(currentPage)) { clickButtonInPage(currentPage) } setTimeout(checkPage, CONFIG.CHECK_INTERVAL) } checkPage() }) } // ============ AUTO SCROLL ============ async function scrollLoop() { if (!state.scrolling) return const currentPage = getCurrentVisiblePage() if (hasButtonToClick(currentPage)) { clickButtonInPage(currentPage) await waitForCurrentPageLoad() if (!state.scrolling) return } if (!isPageFullyLoaded(currentPage)) { await waitForCurrentPageLoad() if (!state.scrolling) return } const el = getScrollElement() const currentPosition = el ? el.scrollTop : window.scrollY if ( Math.abs(currentPosition - state.scrollStartPosition) >= state.currentPageHeight ) { state.scrolling = false updateButtonState() cancelAnimationFrame(state.animationFrame) if (!state.isRestarting) { state.isRestarting = true setTimeout(() => { state.isRestarting = false if (!state.scrolling) { state.scrolling = true state.scrollStartPosition = getScrollElement()?.scrollTop ?? window.scrollY updateButtonState() scrollLoop() } }, CONFIG.PAUSE_DURATION) } return } if (el) { el.scrollTop += state.speed } else { window.scrollBy(0, state.speed) } state.animationFrame = requestAnimationFrame(scrollLoop) } function startAutoScroll() { if (state.scrolling) return state.scrolling = true updateButtonState() const el = getScrollElement() state.scrollStartPosition = el ? el.scrollTop : window.scrollY scrollLoop() } function stopAutoScroll() { state.scrolling = false state.isRestarting = false updateButtonState() cancelAnimationFrame(state.animationFrame) } async function toggleAutoScroll() { if (state.isRestarting) { state.isRestarting = false } if (state.scrolling) { stopAutoScroll() } else { const currentPage = getCurrentVisiblePage() if (hasButtonToClick(currentPage)) { clickButtonInPage(currentPage) } if (!isPageFullyLoaded(currentPage)) { await waitForCurrentPageLoad() } if (!state.scrolling) { const el = getScrollElement() state.scrollStartPosition = el ? el.scrollTop : window.scrollY startAutoScroll() } } } // ============ SPEED CONTROL ============ function changeSpeed(delta) { state.speed = Math.max( CONFIG.MIN_SPEED, Math.min(CONFIG.MAX_SPEED, state.speed + delta), ) updateSpeedDisplay() } function cycleSpeed() { const currentIndex = CONFIG.SPEED_PRESETS.indexOf(state.speed) const nextIndex = (currentIndex + 1) % CONFIG.SPEED_PRESETS.length state.speed = CONFIG.SPEED_PRESETS[nextIndex] updateSpeedDisplay() } // ============ CHAPTER NAVIGATION ============ function goToChapter(direction) { if (state.scrolling) { stopAutoScroll() } const buttons = document.querySelectorAll('.rpage-floatctl__btn') const label = direction === 'prev' ? 'Previous chapter' : 'Next chapter' const targetButton = Array.from(buttons).find( (btn) => btn.getAttribute('aria-label') === label || btn.title === label, ) if (targetButton) { targetButton.click() } } // ============ UI UPDATES ============ function updateButtonState() { if (!DOM.autoScrollBtn) return const icon = state.scrolling ? `` : `` DOM.autoScrollBtn.innerHTML = icon const label = state.scrolling ? 'Stop auto scroll (Space)' : 'Start auto scroll (Space)' DOM.autoScrollBtn.setAttribute('aria-label', label) DOM.autoScrollBtn.title = label } function updateSpeedDisplay() { if (DOM.speedDisplay) { DOM.speedDisplay.textContent = `${state.speed}x` } } // ============ UI INJECTION ============ function injectUI() { const floatCtl = DOM.floatCtl() if (!floatCtl || (DOM.autoScrollBtn && DOM.autoScrollBtn.isConnected)) return if (DOM.autoScrollBtn && !DOM.autoScrollBtn.isConnected) { DOM.autoScrollBtn = null DOM.speedDisplay = null } DOM.autoScrollBtn = document.createElement('button') DOM.autoScrollBtn.type = 'button' DOM.autoScrollBtn.id = 'autoScrollBtn' DOM.autoScrollBtn.className = 'rpage-floatctl__btn' DOM.autoScrollBtn.setAttribute('aria-label', 'Start auto scroll (Space)') DOM.autoScrollBtn.title = 'Start auto scroll (Space)' DOM.autoScrollBtn.innerHTML = `` DOM.speedDisplay = document.createElement('span') DOM.speedDisplay.id = 'autoScrollSpeed' DOM.speedDisplay.className = 'mono' DOM.speedDisplay.style.cssText = ` font-size: 11px; color: inherit; padding: 0 4px; user-select: none; cursor: pointer; min-width: 30px; text-align: center; ` DOM.speedDisplay.textContent = `${state.speed}x` DOM.speedDisplay.title = 'Click to cycle speed (1x-20x) / Use +/- keys' DOM.autoScrollBtn.addEventListener('click', (e) => { e.preventDefault() e.stopPropagation() toggleAutoScroll() }) DOM.speedDisplay.addEventListener('click', cycleSpeed) const settingsBtn = floatCtl.querySelector('[aria-label="Settings"]') if (settingsBtn) { floatCtl.insertBefore(DOM.speedDisplay, settingsBtn) floatCtl.insertBefore(DOM.autoScrollBtn, DOM.speedDisplay) } else { floatCtl.appendChild(DOM.autoScrollBtn) floatCtl.appendChild(DOM.speedDisplay) } } // ============ CONTROLS WAITING ============ function waitForControls() { let attempts = 0 const checkForControls = () => { const floatCtl = DOM.floatCtl() attempts++ if (floatCtl) { injectUI() } else if (attempts < CONFIG.MAX_CONTROLS_ATTEMPTS) { setTimeout(checkForControls, CONFIG.CONTROLS_CHECK_INTERVAL) } } checkForControls() } // ============ FULLSCREEN ============ function autoFullscreen() { if ( document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement ) { return } const element = document.documentElement if (element.requestFullscreen) { element.requestFullscreen().catch(() => {}) } else if (element.webkitRequestFullscreen) { element.webkitRequestFullscreen() } else if (element.msRequestFullscreen) { element.msRequestFullscreen() } } // ============ EVENT HANDLERS ============ document.addEventListener( 'keydown', (e) => { // Ignore if user is typing if ( e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable ) { return } switch (e.key) { case ' ': case 'Spacebar': e.preventDefault() e.stopPropagation() e.stopImmediatePropagation() toggleAutoScroll() autoFullscreen() break case '+': case '=': e.preventDefault() changeSpeed(1) break case '-': case '_': e.preventDefault() changeSpeed(-1) break case 'ArrowLeft': e.preventDefault() goToChapter('prev') break case 'ArrowRight': e.preventDefault() goToChapter('next') break } }, true, ) // Use capture phase // Prevent space keyup default behavior window.addEventListener( 'keyup', (e) => { if (e.code === 'Space' || e.key === ' ' || e.keyCode === 32) { e.preventDefault() e.stopPropagation() e.stopImmediatePropagation() } }, true, ) // ============ INITIALIZATION ============ function init() { applyTheme() waitForControls() const observer = new MutationObserver( debounce(() => { if (!DOM.autoScrollBtn || !DOM.autoScrollBtn.isConnected) { DOM.autoScrollBtn = null DOM.speedDisplay = null injectUI() } state.scrollElement = null }, 250), ) observer.observe(document.body, { childList: true, subtree: true, }) // Auto-fullscreen autoFullscreen() document.addEventListener('click', autoFullscreen, { once: true }) console.log( 'Auto scroll controls integrated. Controls: Space=Start/Stop, +/-=Speed, ←/→=Chapters', ) } // Start the script init()