Holotower Auto Scroll by Claude

Only scroll to new posts when user is already at bottom of page (with persistent state)

// ==UserScript==
// @name         Holotower Auto Scroll by Claude
// @namespace    http://tampermonkey.net/
// @version      1.3
// @author       You
// @license      MIT
// @description  Only scroll to new posts when user is already at bottom of page (with persistent state)
// @match        *://boards.holotower.org/*
// @match        *://holotower.org/*
// @grant        none
// @icon         https://boards.holotower.org/favicon.gif
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // Configuration
    const VISIBILITY_THRESHOLD = 5; // pixels of post that need to be visible
    const STORAGE_KEY = 'holotower_auto_scroll_enabled';

    let originalScrollCheckbox = null;
    let autoScrollCheckbox = null;
    let lastPostElements = [];
    let observer = null;

    // Function to save auto scroll state
    function saveAutoScrollState(enabled) {
        try {
            localStorage.setItem(STORAGE_KEY, enabled ? 'true' : 'false');
            console.log('Auto scroll state saved:', enabled);
        } catch (e) {
            console.warn('Could not save auto scroll state:', e);
        }
    }

    // Function to load auto scroll state
    function loadAutoScrollState() {
        try {
            const saved = localStorage.getItem(STORAGE_KEY);
            if (saved === null) {
                return false; // Default to disabled
            }
            const enabled = saved === 'true';
            console.log('Auto scroll state loaded:', enabled);
            return enabled;
        } catch (e) {
            console.warn('Could not load auto scroll state:', e);
            return false;
        }
    }

    // Function to check if an element is in viewport
    function isElementInView(element) {
        if (!element) {
            console.log('Debug: No element to check');
            return false;
        }

        const rect = element.getBoundingClientRect();
        const windowHeight = window.innerHeight || document.documentElement.clientHeight;

        console.log('Debug: Element rect:', {
            top: rect.top,
            bottom: rect.bottom,
            windowHeight: windowHeight,
            elementVisible: rect.bottom - Math.max(rect.top, 0)
        });

        // Check if at least VISIBILITY_THRESHOLD pixels of the element are visible
        const isVisible = (
            rect.top < windowHeight &&
            rect.bottom > 0 &&
            (rect.bottom - Math.max(rect.top, 0)) >= VISIBILITY_THRESHOLD
        );

        console.log('Debug: Element is visible:', isVisible);
        return isVisible;
    }

    // Alternative: Check if user is near bottom of page
    function isNearBottom() {
        const scrollPosition = window.innerHeight + window.scrollY;
        const documentHeight = document.documentElement.scrollHeight;
        const distanceFromBottom = documentHeight - scrollPosition;

        console.log('Debug: Distance from bottom:', distanceFromBottom);
        return distanceFromBottom <= 200; // Within 200px of bottom
    }

    // Function to get all current posts
    function getCurrentPosts() {
        // Get all post containers (the p.intro elements contain post info)
        return Array.from(document.querySelectorAll('p.intro'));
    }

    // Function to get the last post element before new posts were added
    function getLastKnownPost() {
        if (lastPostElements.length === 0) return null;
        return lastPostElements[lastPostElements.length - 1];
    }

    // Function to check if the last known post is in view
    function isLastPostInView() {
        const lastPost = getLastKnownPost();
        if (!lastPost) {
            console.log('Debug: No last post found');
            return false;
        }

        console.log('Debug: Checking last post visibility');
        return isElementInView(lastPost);
    }

    // Function to check if user should scroll (using both methods)
    function shouldScroll() {
        const lastPostVisible = isLastPostInView();
        const nearBottom = isNearBottom();

        console.log('Debug: Last post visible:', lastPostVisible, 'Near bottom:', nearBottom);

        // Use either method - if last post is visible OR user is near bottom
        return lastPostVisible || nearBottom;
    }

    // Function to scroll to bottom smoothly
    function scrollToBottom() {
        window.scrollTo({
            top: document.documentElement.scrollHeight,
            behavior: 'instant'
        });
    }

    // Function to update our record of current posts
    function updatePostRecord() {
        lastPostElements = getCurrentPosts();
        console.log('Updated post record, now tracking', lastPostElements.length, 'posts');
    }

    // Function to detect if new posts were added
    function checkForNewPosts() {
        const currentPosts = getCurrentPosts();
        const hadNewPosts = currentPosts.length > lastPostElements.length;

        if (hadNewPosts) {
            console.log('New posts detected:', currentPosts.length - lastPostElements.length, 'new posts');
            return true;
        }

        return false;
    }

    // Function to find the original scroll checkbox
    function findOriginalScrollCheckbox() {
        return document.querySelector('input.auto-scroll');
    }

    // Function to create auto scroll checkbox
    function createAutoScrollCheckbox() {
        const originalCheckbox = originalScrollCheckbox;
        if (!originalCheckbox) return null;

        // Hide the original checkbox
        originalCheckbox.style.display = 'none';

        // Find and hide the text node that says " Scroll to New posts)"
        let nextSibling = originalCheckbox.nextSibling;
        while (nextSibling) {
            if (nextSibling.nodeType === Node.TEXT_NODE &&
                nextSibling.textContent.includes('Scroll to New posts')) {
                // Create a span to wrap the text so we can hide it
                const span = document.createElement('span');
                span.style.display = 'none';
                span.textContent = nextSibling.textContent;
                nextSibling.parentNode.replaceChild(span, nextSibling);
                break;
            }
            nextSibling = nextSibling.nextSibling;
        }

        // Create the auto scroll checkbox element
        const autoCheckbox = document.createElement('input');
        autoCheckbox.type = 'checkbox';
        autoCheckbox.id = 'auto-scroll-claude';
        autoCheckbox.className = 'auto-scroll-claude';

        // Load and apply saved state
        const savedState = loadAutoScrollState();
        autoCheckbox.checked = savedState;

        // Create label text with closing parenthesis
        const labelText = document.createTextNode(' Auto Scroll)');

        // Find the parent container and replace the original checkbox
        const parentContainer = originalCheckbox.parentElement;

        // Insert the new checkbox in the same position as the original
        parentContainer.insertBefore(autoCheckbox, originalCheckbox);
        parentContainer.insertBefore(labelText, originalCheckbox);

        return autoCheckbox;
    }

    // Function to handle new posts detected
    function handleNewPosts() {
        // IMPORTANT: Check if we should scroll BEFORE updating our post record
        const shouldScrollToNew = shouldScroll();

        // Update our record of posts first
        updatePostRecord();

        // Check if auto scroll is enabled
        if (autoScrollCheckbox && autoScrollCheckbox.checked) {
            // Use the scroll decision we made before updating
            if (shouldScrollToNew) {
                console.log('Auto Scroll: Should scroll - scrolling to show new posts...');
                scrollToBottom();
            } else {
                console.log('Auto Scroll: Should not scroll - user not viewing latest content');
            }
        }
    }

    // Function to monitor for new posts
    function monitorForNewPosts() {
        if (checkForNewPosts()) {
            handleNewPosts();
        }
    }

    // Function to disable original scroll when auto scroll is active
    function manageScrollBehavior() {
        if (!originalScrollCheckbox || !autoScrollCheckbox) return;

        // When auto scroll is checked, uncheck the original scroll
        if (autoScrollCheckbox.checked && originalScrollCheckbox.checked) {
            originalScrollCheckbox.checked = false;
            console.log('Auto Scroll enabled: Original scroll disabled');
        }
    }

    // Function to set up checkbox event listeners
    function setupCheckboxListeners() {
        if (autoScrollCheckbox) {
            autoScrollCheckbox.addEventListener('change', function() {
                const isEnabled = this.checked;

                // Save the state
                saveAutoScrollState(isEnabled);

                if (isEnabled) {
                    console.log('Auto scroll enabled - will only scroll when at bottom');
                    // Disable original scroll to prevent conflicts
                    if (originalScrollCheckbox && originalScrollCheckbox.checked) {
                        originalScrollCheckbox.checked = false;
                    }
                } else {
                    console.log('Auto scroll disabled');
                }
            });
        }

        if (originalScrollCheckbox) {
            originalScrollCheckbox.addEventListener('change', function() {
                if (this.checked) {
                    // Disable auto scroll to prevent conflicts
                    if (autoScrollCheckbox && autoScrollCheckbox.checked) {
                        autoScrollCheckbox.checked = false;
                        // Save the disabled state
                        saveAutoScrollState(false);
                        console.log('Original scroll enabled: Auto scroll disabled');
                    }
                }
            });
        }
    }

    // Function to set up DOM observer for new posts
    function setupObserver() {
        // Disconnect existing observer
        if (observer) {
            observer.disconnect();
        }

        // Create new observer to watch for DOM changes
        observer = new MutationObserver(function(mutations) {
            let shouldCheck = false;

            mutations.forEach(function(mutation) {
                // Check if nodes were added
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    // Check if any added nodes might be posts
                    for (let node of mutation.addedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            // Check if it's a post or contains posts (holotower specific)
                            if (node.matches && (
                                node.matches('p.intro, div.post, .post_no') ||
                                node.querySelector('p.intro, div.post, .post_no')
                            )) {
                                shouldCheck = true;
                                break;
                            }
                        }
                    }
                }
            });

            if (shouldCheck) {
                // Delay slightly to allow DOM to settle
                setTimeout(monitorForNewPosts, 100);
            }
        });

        // Start observing
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    // Function to initialize the script
    function initialize() {
        console.log('Holotower Auto Scroll: Initializing...');

        // Remove any existing Smart Scroll elements from previous versions
        const existingSmartScrollElements = document.querySelectorAll('#smart-scroll, .smart-scroll');
        existingSmartScrollElements.forEach(el => {
            const parent = el.parentElement;
            if (parent && parent.tagName === 'SPAN') {
                parent.remove(); // Remove the entire span wrapper
            } else {
                el.remove(); // Remove just the element
            }
        });

        // Find the original scroll checkbox
        originalScrollCheckbox = findOriginalScrollCheckbox();

        if (!originalScrollCheckbox) {
            console.log('Holotower Auto Scroll: Could not find original scroll checkbox');
            return;
        }

        console.log('Holotower Auto Scroll: Found original scroll checkbox');

        // Create the auto scroll checkbox (this will load and apply saved state)
        autoScrollCheckbox = createAutoScrollCheckbox();

        if (!autoScrollCheckbox) {
            console.log('Holotower Auto Scroll: Could not create auto scroll checkbox');
            return;
        }

        console.log('Holotower Auto Scroll: Created auto scroll checkbox with saved state:', autoScrollCheckbox.checked);

        // Initialize post record
        updatePostRecord();
        console.log('Holotower Auto Scroll: Initial post count:', lastPostElements.length);

        // Set up checkbox event listeners
        setupCheckboxListeners();

        // Set up observer for new posts
        setupObserver();

        // Apply initial state management
        manageScrollBehavior();

        console.log('Holotower Auto Scroll: Initialization complete');
    }

    // Wait for page to load
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        // DOM already loaded
        setTimeout(initialize, 500); // Small delay to ensure page is fully rendered
    }

    // Also try to re-initialize if page content changes significantly
    let reinitTimeout;
    const reinitializeIfNeeded = function() {
        clearTimeout(reinitTimeout);
        reinitTimeout = setTimeout(function() {
            if (!originalScrollCheckbox || !document.contains(originalScrollCheckbox) ||
                !autoScrollCheckbox || !document.contains(autoScrollCheckbox)) {
                console.log('Holotower Auto Scroll: Checkboxes lost, reinitializing...');
                initialize();
            }
        }, 1000);
    };

    // Watch for major page changes
    if (typeof MutationObserver !== 'undefined') {
        const pageObserver = new MutationObserver(reinitializeIfNeeded);
        pageObserver.observe(document.body, { childList: true, subtree: false });
    }

})();