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