Weebcentral on Steroids(extra features)

Improved navigation for Pages and Chapters. Keyboard shortcuts(page:A/D | chapter:W/S). Added a progress bar and Page Counter.

// ==UserScript==
// @name         Weebcentral on Steroids(extra features)
// @namespace    http://tampermonkey.net/
// @version      56.0
// @description  Improved navigation for Pages and Chapters. Keyboard shortcuts(page:A/D | chapter:W/S). Added a progress bar and Page Counter.
// @author       Nirmal Bhasarkar
// @license      MIT
// @match        https://weebcentral.com/chapters/*
// @icon         https://weebcentral.com/static/images/apple-touch-icon.png
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // ====================================================================================
    // --- CONFIGURATION ---
    // This section allows you to customize all features of the script.
    // ====================================================================================

    const CONFIG = {
        // --- Feature Toggles ---
        ENABLE_PROGRESS_BAR: true,
        ENABLE_PAGE_COUNTER: true,
        ENABLE_CLICK_NAVIGATE: true,
        ENABLE_KEYBOARD_SHORTCUTS: true,    // Use keyboard for page & chapter navigation

        // --- General Settings ---
        SCRIPT_PREFIX: 'wcps', // A prefix for all element IDs and classes to prevent conflicts.

        // --- Keyboard Shortcuts ---
        keyboardShortcuts: {
            // Page Navigation
            KEY_PREVIOUS_PAGE: 'd',
            KEY_NEXT_PAGE: 'a',
            // Chapter Navigation
            KEY_PREV_CHAPTER: 's',
            KEY_NEXT_CHAPTER: 'w',
            KEY_PREV_CHAPTER_ALT: 'arrowdown',
            KEY_NEXT_CHAPTER_ALT: 'arrowup'
        },

        // --- Vertical Progress Bar ---
        progressBar: {
            // Sizing and Positioning
            BAR_WIDTH: '30px',
            BAR_LEFT_OFFSET: '0px',
            TRIGGER_WIDTH: '10px',
            TOP_OFFSET: '45px',
            BOTTOM_OFFSET: '45px',
            // Layout Behavior
            SCROLL_THRESHOLD: 18, // Pages above this number will make the bar scrollable.
            // General Style
            BACKGROUND_COLOR: 'rgba(73, 83, 89, 1.0)',
            FONT_FAMILY: 'Inter, sans-serif',
            BAR_BORDER_RADIUS: '15px',
            // Clickable Page Box Colors
            BOX_BORDER_RADIUS: '9999px',
            BAR_FONT_SIZE: '17px',
            TEXT_COLOR: 'rgba(255, 255, 255, 0.6)',
            TEXT_HOVER_COLOR: 'rgba(255, 255, 255, 1)',
            ACTIVE_PAGE_COLOR: '#ffffff',
            ACTIVE_PAGE_BG_COLOR: 'rgba(56, 189, 248, 0.2)',
            HOVER_BORDER: '0.3px solid rgba(1, 1, 12, 1.0)',
            // Page Counter (Bottom Left)
            COUNTER_FONT_SIZE: '17px',
            COUNTER_FONT_WEIGHT: '525',
            COUNTER_TEXT_COLOR: 'rgba(255, 255, 255, 0.5)',
            COUNTER_POSITION: { bottom: '2px', left: '5px' }
        },

        // --- Click to Navigate (for Pages) ---
        clickNavigate: {
            // Sizing
            FIXED_OVERLAY_WIDTH: 520, // in pixels
            // Behavior
            DEBOUNCE_DELAY: 150,      // ms, delay for repositioning to save resources
            CURSOR_IDLE_TIME: 2000,   // ms, before cursor hides
        }
    };

    // ====================================================================================
    // --- GLOBAL VARIABLES & CONSTANTS ---
    // ====================================================================================

    const SCRIPT_NAME = "[Weebcentral Power Suite v56.0]";
    let prevChapterUrl = null;
    let nextChapterUrl = null;

    // ====================================================================================
    // --- HELPER & UTILITY FUNCTIONS ---
    // General-purpose functions used throughout the script.
    // ====================================================================================

    /**
     * Debounces a function to limit the rate at which it gets called.
     * @param {Function} func The function to debounce.
     * @param {number} delay The debounce delay in milliseconds.
     * @returns {Function} The debounced function.
     */
    function debounce(func, delay) {
        let timeout;
        return (...args) => {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), delay);
        };
    }

    /**
     * Finds the main manga content element, which contains the images.
     * @returns {HTMLElement|null} The manga content element or null if not found.
     */
    function findMangaContentElement() {
        // This selector targets the specific section containing the manga pages.
        const el = document.querySelector('main > section:nth-of-type(3)');
        // A simple check to ensure it's a valid element with content.
        return (el && el.clientHeight > 200) ? el : null;
    }

    /**
     * Reads the current and total page numbers from hidden input fields on the page.
     * @returns {{current: number|null, total: number|null}} An object with page data.
     */
    function getPageData() {
        let current = null, total = null;
        try {
            const maxPageInput = document.getElementById('max_page');
            if (maxPageInput?.value) total = parseInt(maxPageInput.value, 10);
            const pageButton = document.querySelector("button[x-text=\"'Page ' + page\"]");
            if (pageButton?.textContent) {
                const match = pageButton.textContent.match(/(\d+)/);
                if (match?.[1]) current = parseInt(match[1], 10);
            }
        } catch (e) { /* Suppress errors as this runs frequently */ }
        return { current, total };
    }

    // ====================================================================================
    // --- NAVIGATION LOGIC (PAGE & CHAPTER) ---
    // Functions responsible for handling page and chapter changes.
    // ====================================================================================

    /**
     * Scrolls the main manga image into the vertical center of the viewport.
     * This is triggered after navigating via the progress bar.
     */
    function scrollToMangaContent() {
        // A short delay allows the new page image to render before we scroll to it.
        setTimeout(() => {
            const mangaContent = findMangaContentElement();
            if (mangaContent) {
                mangaContent.scrollIntoView({
                    behavior: 'auto', // 'auto' for an instant jump, 'smooth' for animation.
                    block: 'center'
                });
            }
        }, 100);
    }

    /**
     * Navigates to a specific page number by interacting with the website's page select modal.
     * @param {number} pageNum The page number to navigate to.
     */
    function goToPage(pageNum) {
        try {
            // 1. Open the page selection modal.
            document.querySelector("button[onclick*='page_select_modal.showModal()']")?.click();
            // 2. In the next frame, find and click the correct page button.
            requestAnimationFrame(() => {
                const pageSelectModal = document.getElementById('page_select_modal');
                if (!pageSelectModal) return;
                const pageButtons = pageSelectModal.querySelectorAll('button.btn');
                const targetButton = [...pageButtons].find(btn => btn.textContent.trim() == pageNum);
                if (targetButton) {
                    targetButton.click();
                    // 3. After clicking, scroll the new page into view.
                    scrollToMangaContent();
                } else {
                    // If the page isn't found, close the modal to avoid getting stuck.
                    pageSelectModal.querySelector("form[method='dialog'] button")?.click();
                }
            });
        } catch (e) {
            console.error(`${SCRIPT_NAME} Error in goToPage:`, e);
        }
    }

    /**
     * Simulates a keyboard press event on the document body.
     * This is used to trigger the website's default page navigation.
     * @param {string} key The key to simulate (e.g., 'ArrowLeft', 'ArrowRight').
     */
    function simulateKeyPress(key) {
        document.body.dispatchEvent(new KeyboardEvent('keydown', { key, code: key, bubbles: true, cancelable: true }));
    }

    /**
     * Updates the website's "PREV" and "NEXT" buttons to navigate between chapters.
     */
    function updateChapterButtons() {
        const allButtons = document.querySelectorAll('main button');
        const prevPageButtons = Array.from(allButtons).filter(btn => btn.textContent.trim().includes('PREV'));
        const nextPageButtons = Array.from(allButtons).filter(btn => btn.textContent.trim().includes('NEXT'));

        const configureButton = (button, url, text) => {
            if (url) {
                // Change the button's action to navigate to the new chapter URL.
                button.onclick = (e) => { e.preventDefault(); e.stopPropagation(); window.location.href = url; };
                // Update the button's text.
                const textSpan = button.querySelector('span');
                if (textSpan) textSpan.textContent = text;
                else button.textContent = text;
            } else {
                // If there's no next/prev chapter, hide the button.
                button.style.display = 'none';
            }
        };

        prevPageButtons.forEach(btn => configureButton(btn, prevChapterUrl, 'Prev Chapter'));
        nextPageButtons.forEach(btn => configureButton(btn, nextChapterUrl, 'Next Chapter'));
    }

    /**
     * Fetches the chapter list, finds the current chapter, and determines the URLs for the
     * previous and next chapters.
     */
    async function setupChapterNavigation() {
        try {
            const chapterSelectButton = document.querySelector('button[hx-get*="/chapter-select"]');
            if (!chapterSelectButton) return;

            // Fetch the HTML content of the chapter selection modal.
            const chapterListUrl = new URL(chapterSelectButton.getAttribute('hx-get'), window.location.origin).href;
            const response = await fetch(chapterListUrl);
            const html = await response.text();
            const doc = new DOMParser().parseFromString(html, 'text/html');

            // Find the currently selected chapter in the list.
            const selectedChapter = doc.getElementById('selected_chapter');
            if (!selectedChapter) return;

            // The website lists chapters from newest to oldest.
            // So the 'next' sibling is the previous chapter, and vice-versa.
            const prevLink = selectedChapter.nextElementSibling;
            const nextLink = selectedChapter.previousElementSibling;

            prevChapterUrl = prevLink?.tagName === 'A' ? prevLink.href : null;
            nextChapterUrl = nextLink?.tagName === 'A' ? nextLink.href : null;

            updateChapterButtons();
        } catch (error) {
            console.error(`${SCRIPT_NAME} Chapter Nav Error:`, error);
        }
    }

    // ====================================================================================
    // --- UI CREATION & STYLING ---
    // Functions that create and style the script's visual elements.
    // ====================================================================================

    /**
     * Injects a single stylesheet for all the script's custom UI.
     */
    function injectCSS() {
        if (document.getElementById(`${CONFIG.SCRIPT_PREFIX}-styles`)) return;
        const style = document.createElement('style');
        style.id = `${CONFIG.SCRIPT_PREFIX}-styles`;
        style.textContent = `
            /* Hide scrollbar on the progress bar */
            #${CONFIG.SCRIPT_PREFIX}-page-list::-webkit-scrollbar { display: none; }
            #${CONFIG.SCRIPT_PREFIX}-page-list { scrollbar-width: none; }

            /* Basic styling for the navigation overlay */
            .${CONFIG.SCRIPT_PREFIX}-nav-overlay {
                position: absolute; display: none; z-index: 9999; cursor: pointer;
            }
            .${CONFIG.SCRIPT_PREFIX}-nav-overlay.${CONFIG.SCRIPT_PREFIX}-cursor-hidden {
                cursor: none;
            }

            /* Prevent text selection highlight on the page counter */
            #${CONFIG.SCRIPT_PREFIX}-page-counter {
                user-select: none;
            }
        `;
        document.head.appendChild(style);
    }

    /**
     * Creates the DOM elements for the vertical progress bar.
     */
    function createProgressBarUI() {
        if (!CONFIG.ENABLE_PROGRESS_BAR || document.getElementById(`${CONFIG.SCRIPT_PREFIX}-progress-bar-container`)) return;

        const p = { ...CONFIG.progressBar };
        const container = document.createElement('div');
        container.id = `${CONFIG.SCRIPT_PREFIX}-progress-bar-container`;
        Object.assign(container.style, {
            position: 'fixed', top: p.TOP_OFFSET, bottom: p.BOTTOM_OFFSET,
            left: `-${p.BAR_WIDTH}`, width: p.BAR_WIDTH,
            backgroundColor: p.BACKGROUND_COLOR, backdropFilter: 'blur(5px)',
            transition: 'left 0.3s ease-in-out', zIndex: '2147483646',
            display: 'flex', justifyContent: 'center', boxSizing: 'border-box',
            borderRadius: p.BAR_BORDER_RADIUS
        });
        container.addEventListener('mouseleave', () => container.style.left = `-${p.BAR_WIDTH}`);
        container.addEventListener('wheel', (e) => e.stopPropagation());

        const pageList = document.createElement('div');
        pageList.id = `${CONFIG.SCRIPT_PREFIX}-page-list`;
        Object.assign(pageList.style, {
            position: 'relative', width: '100%', height: '100%',
            display: 'flex', flexDirection: 'column', boxSizing: 'border-box'
        });
        container.appendChild(pageList);
        document.body.append(container);

        document.body.addEventListener('mousemove', (e) => {
            const triggerZone = {
                left: parseInt(p.BAR_LEFT_OFFSET, 10),
                width: parseInt(p.TRIGGER_WIDTH, 10),
                top: parseInt(p.TOP_OFFSET, 10),
                bottom: window.innerHeight - parseInt(p.BOTTOM_OFFSET, 10)
            };
            if (e.clientX >= triggerZone.left && e.clientX < triggerZone.left + triggerZone.width &&
                e.clientY >= triggerZone.top && e.clientY < triggerZone.bottom) {
                container.style.left = p.BAR_LEFT_OFFSET;
            }
        });
    }

    /**
     * Creates the DOM element for the page counter.
     */
    function createPageCounterUI() {
        if (!CONFIG.ENABLE_PAGE_COUNTER || document.getElementById(`${CONFIG.SCRIPT_PREFIX}-page-counter`)) return;

        const p = { ...CONFIG.progressBar };
        const pageCounter = document.createElement('div');
        pageCounter.id = `${CONFIG.SCRIPT_PREFIX}-page-counter`;
        Object.assign(pageCounter.style, {
            position: 'fixed', ...p.COUNTER_POSITION,
            padding: '5px 7px', color: p.COUNTER_TEXT_COLOR, zIndex: '99999',
            fontSize: p.COUNTER_FONT_SIZE, fontFamily: p.FONT_FAMILY,
            fontWeight: p.COUNTER_FONT_WEIGHT, pointerEvents: 'none',
            opacity: '0', transition: 'opacity 0.2s'
        });
        document.body.append(pageCounter);
    }

    /**
     * Creates the invisible overlay for click-to-navigate functionality.
     */
    function createClickNavUI() {
        if (!CONFIG.ENABLE_CLICK_NAVIGATE || document.getElementById(`${CONFIG.SCRIPT_PREFIX}-nav-overlay`)) return;

        const overlay = document.createElement('div');
        overlay.id = `${CONFIG.SCRIPT_PREFIX}-nav-overlay`;
        overlay.className = `${CONFIG.SCRIPT_PREFIX}-nav-overlay`;

        overlay.addEventListener('click', (e) => {
            e.preventDefault(); e.stopImmediatePropagation();
            simulateKeyPress('ArrowRight');
        }, true);

        overlay.addEventListener('contextmenu', (e) => {
            e.preventDefault(); e.stopImmediatePropagation();
            simulateKeyPress('ArrowLeft');
        }, true);

        let idleTimer;
        const cursorHiddenClass = `${CONFIG.SCRIPT_PREFIX}-cursor-hidden`;
        const idleTime = CONFIG.clickNavigate.CURSOR_IDLE_TIME;
        const startTimer = () => {
            clearTimeout(idleTimer);
            idleTimer = setTimeout(() => overlay.classList.add(cursorHiddenClass), idleTime);
        };
        const resetTimer = () => {
            overlay.classList.remove(cursorHiddenClass);
            startTimer();
        };
        overlay.addEventListener('mouseenter', startTimer);
        overlay.addEventListener('mousemove', resetTimer);
        overlay.addEventListener('mouseleave', () => {
            clearTimeout(idleTimer);
            overlay.classList.remove(cursorHiddenClass);
        });

        document.body.appendChild(overlay);
    }

    // ====================================================================================
    // --- EVENT LISTENERS & OBSERVERS ---
    // Functions that listen for user input and page changes.
    // ====================================================================================

    /**
     * Sets up keyboard shortcuts for both page and chapter navigation.
     */
    function setupKeyboardShortcuts() {
        if (!CONFIG.ENABLE_KEYBOARD_SHORTCUTS) return;

        document.body.addEventListener('keydown', (e) => {
            const targetTagName = e.target.tagName.toLowerCase();
            if (['input', 'textarea', 'select'].includes(targetTagName)) {
                return;
            }

            const key = e.key.toLowerCase();
            const shortcuts = CONFIG.keyboardShortcuts;

            if (key === shortcuts.KEY_PREVIOUS_PAGE.toLowerCase()) {
                simulateKeyPress('ArrowLeft');
            } else if (key === shortcuts.KEY_NEXT_PAGE.toLowerCase()) {
                simulateKeyPress('ArrowRight');
            }

            const prevChapterKeys = [shortcuts.KEY_PREV_CHAPTER.toLowerCase(), shortcuts.KEY_PREV_CHAPTER_ALT.toLowerCase()];
            const nextChapterKeys = [shortcuts.KEY_NEXT_CHAPTER.toLowerCase(), shortcuts.KEY_NEXT_CHAPTER_ALT.toLowerCase()];

            if (prevChapterKeys.includes(key) && prevChapterUrl) {
                e.preventDefault();
                window.location.href = prevChapterUrl;
            } else if (nextChapterKeys.includes(key) && nextChapterUrl) {
                e.preventDefault();
                window.location.href = nextChapterUrl;
            }
        });
    }

    // ====================================================================================
    // --- DYNAMIC UI UPDATES ---
    // Functions that are called repeatedly to keep the UI in sync with the page.
    // ====================================================================================

    /**
     * Updates the progress bar's page numbers, active page highlight.
     */
    function updateProgressBarUI() {
        if (!CONFIG.ENABLE_PROGRESS_BAR) return;

        const { current, total } = getPageData();
        const p = { ...CONFIG.progressBar };
        const pageListElement = document.getElementById(`${CONFIG.SCRIPT_PREFIX}-page-list`);

        if (!pageListElement) return;

        if (total && pageListElement.children.length !== total) {
            pageListElement.innerHTML = '';
            const requiresScrolling = total > p.SCROLL_THRESHOLD;
            pageListElement.style.justifyContent = requiresScrolling ? 'flex-start' : 'space-between';
            pageListElement.style.overflowY = requiresScrolling ? 'auto' : 'hidden';

            for (let i = 1; i <= total; i++) {
                const pageNumDiv = document.createElement('div');
                Object.assign(pageNumDiv.style, {
                    textAlign: 'center', width: '100%', padding: '7px 0',
                    borderRadius: p.BOX_BORDER_RADIUS, boxSizing: 'border-box',
                    border: '0.3px solid transparent', color: p.TEXT_COLOR,
                    fontFamily: p.FONT_FAMILY, fontSize: p.BAR_FONT_SIZE, fontWeight: '500',
                    cursor: 'pointer', transition: 'all 0.2s'
                });
                pageNumDiv.dataset.pageNum = i;
                pageNumDiv.textContent = i;
                pageNumDiv.addEventListener('click', (e) => { e.stopPropagation(); goToPage(i); });
                pageNumDiv.addEventListener('mouseenter', () => {
                    pageNumDiv.style.color = p.TEXT_HOVER_COLOR;
                    if (!pageNumDiv.classList.contains('active-page')) pageNumDiv.style.border = p.HOVER_BORDER;
                });
                pageNumDiv.addEventListener('mouseleave', () => {
                    pageNumDiv.style.border = '0.3px solid transparent';
                    pageNumDiv.style.color = pageNumDiv.classList.contains('active-page') ? p.ACTIVE_PAGE_COLOR : p.TEXT_COLOR;
                });
                pageListElement.appendChild(pageNumDiv);
            }
        }

        if (current) {
            const prevActive = pageListElement.querySelector('.active-page');
            if (prevActive) {
                prevActive.classList.remove('active-page');
                Object.assign(prevActive.style, { backgroundColor: 'transparent', color: p.TEXT_COLOR, fontWeight: '400' });
            }
            const newActive = pageListElement.querySelector(`[data-page-num='${current}']`);
            if (newActive) {
                newActive.classList.add('active-page');
                Object.assign(newActive.style, { backgroundColor: p.ACTIVE_PAGE_BG_COLOR, color: p.ACTIVE_PAGE_COLOR, fontWeight: '800' });
                if (pageListElement.style.overflowY === 'auto') {
                    newActive.scrollIntoView({ behavior: 'smooth', block: 'center' });
                }
            }
        }
    }

    /**
     * Updates the page counter text.
     */
    function updatePageCounterUI() {
        if (!CONFIG.ENABLE_PAGE_COUNTER) return;

        const { current, total } = getPageData();
        const pageCounterElement = document.getElementById(`${CONFIG.SCRIPT_PREFIX}-page-counter`);

        if (pageCounterElement) {
            pageCounterElement.textContent = (current !== null && total !== null) ? `${current}/${total}` : '';
            pageCounterElement.style.opacity = (current !== null && total !== null) ? '1' : '0';
        }
    }

    /**
     * Updates the position and size of the click-to-navigate overlay to match the manga image.
     */
    function positionClickNavOverlay() {
        if (!CONFIG.ENABLE_CLICK_NAVIGATE) return;

        const mangaContent = findMangaContentElement();
        const navOverlay = document.getElementById(`${CONFIG.SCRIPT_PREFIX}-nav-overlay`);
        if (!mangaContent || !navOverlay) return;

        const contentRect = mangaContent.getBoundingClientRect();
        const overlayWidth = CONFIG.clickNavigate.FIXED_OVERLAY_WIDTH;
        const showOverlay = contentRect.height >= 100 && contentRect.width > overlayWidth;

        Object.assign(navOverlay.style, {
            top: `${contentRect.top + window.scrollY}px`,
            left: `${contentRect.right + window.scrollX - overlayWidth}px`,
            width: `${overlayWidth}px`,
            height: `${contentRect.height}px`,
            display: showOverlay ? 'block' : 'none',
        });
    }


    // ====================================================================================
    // --- INITIALIZATION ---
    // Sets up the script and observers when the page loads.
    // ====================================================================================

    /**
     * Sets up a MutationObserver to watch for the chapter button, which is loaded dynamically.
     */
    function initializeChapterNavigation() {
        const observer = new MutationObserver((mutations, obs) => {
            const chapterButton = document.querySelector('button[hx-get*="/chapter-select"]');
            if (chapterButton) {
                setupChapterNavigation();
                obs.disconnect(); // Stop observing once the element is found.
            }
        });
        observer.observe(document.documentElement, { childList: true, subtree: true });
    }

    /**
     * Sets up a MutationObserver to watch for page changes and keep the UI updated.
     */
    function startPageObserver() {
        const debouncedPositioner = debounce(positionClickNavOverlay, CONFIG.clickNavigate.DEBOUNCE_DELAY);
        const observerTarget = document.querySelector('main');

        if (!observerTarget) {
            console.error(`${SCRIPT_NAME} Could not find <main> element. Falling back to timer.`);
            setInterval(() => {
                updateProgressBarUI();
                updatePageCounterUI();
                positionClickNavOverlay();
            }, 750);
            return;
        }

        const observer = new MutationObserver(() => {
            updateProgressBarUI();
            updatePageCounterUI();
            debouncedPositioner();
        });

        observer.observe(observerTarget, { childList: true, subtree: true });
    }

    /**
     * Main execution function, called when the window has loaded.
     */
    function main() {
        console.log(`${SCRIPT_NAME} Initializing.`);
        injectCSS();
        createProgressBarUI();
        createPageCounterUI();
        createClickNavUI();
        setupKeyboardShortcuts();
        initializeChapterNavigation();

        // Perform initial UI setup and position checks.
        updateProgressBarUI();
        updatePageCounterUI();
        setTimeout(positionClickNavOverlay, 500); // Delay to allow images to load.
        window.addEventListener('resize', debounce(positionClickNavOverlay, CONFIG.clickNavigate.DEBOUNCE_DELAY));

        startPageObserver();
    }

    // Start the main execution after the page has fully loaded.
    window.addEventListener('load', main);

})();