RoyalRoad Leads to Chapter Filter

Customizable chapter filter with user input, link check, and setup.

// ==UserScript==
// @name         RoyalRoad Leads to Chapter Filter
// @namespace    http://tampermonkey.net/
// @version      0.6.3
// @description  Customizable chapter filter with user input, link check, and setup.
// @author       Byakuran
// @match        https://www.royalroad.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=royalroad.com
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function() {
    'use strict';

    // Configuration object with defaults
    const DEFAULT_CONFIG = {
        showField: null,
        optionalTimeout: 5,
        keywords: ['chapter-1', 'chapter-one', 'prologue', 'amazon', 'www.audible.com', 'podiumentertainment', '/chapter/', '%2Fchapter%2F', '%2Fgeni.us%2F'],
        replacements: ['Leads to chapter 1', 'Leads to chapter 1', 'Leads to prologue', 'Leads to amazon.', 'Leads to Audible', 'Leads to Podium Entertainment', 'Leads to a chapter', 'Leads to a chapter', 'Amazon'],
        showAdButton: false,
        darkMode: false,
        maxHistorySessions: 5, // Default number of sessions to save
        saveHistory: true, // Whether to save history at all
        lastPosition: { x: 20, y: 20 }
    };

    // Helper functions for settings management
    const settings = {
        get: function(key) {
            return GM_getValue(key, DEFAULT_CONFIG[key]);
        },

        set: function(key, value) {
            GM_setValue(key, value);
        },

        reset: function() {
            Object.keys(DEFAULT_CONFIG).forEach(key => {
                this.set(key, DEFAULT_CONFIG[key]);
            });
            return DEFAULT_CONFIG;
        },

        getAll: function() {
            const config = {};
            Object.keys(DEFAULT_CONFIG).forEach(key => {
                config[key] = this.get(key);
            });
            return config;
        }
    };

    // Initialize config from settings
    let config = settings.getAll();

    // Register Tampermonkey menu commands
    GM_registerMenuCommand('Open Settings', showSetupWizard);
    GM_registerMenuCommand('Reset Settings', () => {
        if (confirm('Are you sure you want to reset all settings to default?')) {
            config = settings.reset();
            alert('Settings have been reset. Refreshing page...');
            location.reload();
        }
    });

    // Add keyboard shortcut handler
    document.addEventListener('keydown', function(e) {
        if (e.ctrlKey && e.shiftKey && e.key === 'S') {
            e.preventDefault();
            showSetupWizard();
        }
    });

    // Modified showSetupWizard function
    function showSetupWizard() {
        const wizard = document.createElement('div');
        wizard.style.cssText = `
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        background-color: #f0f0f0;
        padding: 20px;
        border-radius: 8px;
        box-shadow: 0 0 20px rgba(0,0,0,0.3);
        z-index: 10000;
        width: 400px;
        color: #000000;
        font-weight: 500;
    `;

        wizard.innerHTML = `
        <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
            <h2 style="margin: 0; color: #000000;">RoyalRoad Filter Setup</h2>
            <button id="closeSetup" style="
                background: none;
                border: none;
                color: #000000;
                cursor: pointer;
                font-size: 20px;
                padding: 0 5px;
                line-height: 1;
            ">×</button>
        </div>
        <p style="color: #000000;">How would you like to display the filter input field?</p>
        <div style="display: flex; flex-direction: column; gap: 10px; color: #000000;">
            <label style="color: #000000;">
                <input type="radio" name="displayMode" value="always"> Always show
            </label>
            <label style="color: #000000;">
                <input type="radio" name="displayMode" value="optional"> Show toggle button
                <input type="number" id="timeout" value="${config.optionalTimeout}" min="1" style="width: 60px; margin-left: 10px;"> seconds
            </label>
            <label style="color: #000000;">
                <input type="radio" name="displayMode" value="never"> Never show
                <span style="font-size: 0.8em; margin-left: 10px;">(Use Ctrl+Shift+S to reopen)</span>
            </label>
            <label style="margin-top: 10px; color: #000000;">
                <input type="checkbox" id="showAdButton" ${config.showAdButton ? 'checked' : ''}> Enable "Show Ad" button
            </label>
            <label style="margin-top: 10px; color: #000000;">
                <input type="checkbox" id="darkMode" ${config.darkMode ? 'checked' : ''}> Enable dark mode
            </label>
        </div>
		<div style="margin-top: 15px; border-top: 1px solid #ccc; padding-top: 15px;">
			<h3 style="margin: 0 0 10px 0; color: #000000;">History Settings</h3>
			<label style="color: #000000;">
				<input type="checkbox" id="saveHistory" ${config.saveHistory ? 'checked' : ''}>
				Enable history saving
			</label>
			<div style="margin-top: 10px;">
				<label style="color: #000000;">
					Number of sessions to save:
					<input type="number" id="maxHistorySessions"
						   value="${config.maxHistorySessions}"
						   min="1" max="20" style="width: 60px; margin-left: 10px;">
				</label>
			</div>
		</div>
        <div style="display: flex; gap: 10px; margin-top: 20px;">
            <button id="saveSetup" style="
                padding: 8px 16px;
                background-color: #4CAF50;
                color: white;
                border: none;
                border-radius: 4px;
                cursor: pointer;
            ">Save Settings</button>
            <button id="resetSetup" style="
                padding: 8px 16px;
                background-color: #f44336;
                color: white;
                border: none;
                border-radius: 4px;
                cursor: pointer;
            ">Reset to Default</button>
        </div>
        <p style="margin-top: 15px; font-size: 0.8em; color: #666;">
            Tip: Access settings anytime with Ctrl+Shift+S or through the Tampermonkey menu
        </p>
    `;

        document.body.appendChild(wizard);

        // Set the initial radio button state
        if (config.showField) {
            const radio = wizard.querySelector(`input[value="${config.showField}"]`);
            if (radio) radio.checked = true;
        }

        // Add event listeners
        document.getElementById('closeSetup').addEventListener('click', () => {
            wizard.remove();
        });

        document.getElementById('saveSetup').addEventListener('click', () => {
            const displayMode = document.querySelector('input[name="displayMode"]:checked').value;
            const timeout = document.getElementById('timeout').value;
            const darkMode = document.getElementById('darkMode').checked;
            const showAdButton = document.getElementById('showAdButton').checked; // New line

            // Update config and save settings
            config.showField = displayMode;
            config.optionalTimeout = parseInt(timeout);
            config.darkMode = darkMode;
            config.showAdButton = showAdButton; // New line

            Object.keys(config).forEach(key => {
                settings.set(key, config[key]);
            });

            wizard.remove();
            initializeUI();
        });

        document.getElementById('resetSetup').addEventListener('click', () => {
            if (confirm('Are you sure you want to reset all settings to default?')) {
                config = settings.reset();
                wizard.remove();
                alert('Settings have been reset. Refreshing page...');
                location.reload();
            }
        });
    }

    // Enhanced UI creation function with filter list
    function createUI() {
        const container = document.createElement('div');
        container.style.cssText = `
            position: fixed;
            bottom: ${config.lastPosition.y}px;
            right: ${config.lastPosition.x}px;
            background-color: ${config.darkMode ? '#333' : '#f0f0f0'};
            color: ${config.darkMode ? '#fff' : '#000'};
            padding: 10px;
            border: 1px solid ${config.darkMode ? '#555' : '#ccc'};
            border-radius: 4px;
            z-index: 1000;
            box-shadow: 2px 2px 5px rgba(0,0,0,0.1);
            min-width: 220px;
        `;

        // Make container draggable
        let isDragging = false;
        let currentX;
        let currentY;
        let initialX;
        let initialY;

        container.addEventListener('mousedown', e => {
            if (e.target === container) {
                isDragging = true;
                initialX = e.clientX - container.offsetLeft;
                initialY = e.clientY - container.offsetTop;
            }
        });

        document.addEventListener('mousemove', e => {
            if (isDragging) {
                e.preventDefault();
                currentX = e.clientX - initialX;
                currentY = e.clientY - initialY;
                container.style.right = `${window.innerWidth - currentX - container.offsetWidth}px`;
                container.style.bottom = `${window.innerHeight - currentY - container.offsetHeight}px`;
            }
        });

        document.addEventListener('mouseup', () => {
            if (isDragging) {
                isDragging = false;
                config.lastPosition = {
                    x: parseInt(container.style.right),
                    y: parseInt(container.style.bottom)
                };
                GM_setValue('lastPosition', config.lastPosition);
            }
        });

        const header = document.createElement('div');
        header.style.cssText = `
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 10px;
        `;

        const title = document.createElement('span');
        title.textContent = 'Filter Settings';
        title.style.fontWeight = 'bold';

        const buttonContainer = document.createElement('div');
        buttonContainer.style.cssText = `
            display: flex;
            gap: 5px;
        `;

        const settingsButton = document.createElement('button');
        settingsButton.innerHTML = '⚙️';
        settingsButton.title = 'Settings';
        settingsButton.style.cssText = `
            background: none;
            border: none;
            cursor: pointer;
            font-size: 16px;
            padding: 0 5px;
        `;

        const closeButton = document.createElement('button');
        closeButton.textContent = '×';
        closeButton.style.cssText = `
            background: none;
            border: none;
            color: ${config.darkMode ? '#fff' : '#000'};
            cursor: pointer;
            font-size: 16px;
            padding: 0 5px;
        `;

        // Create filter list container
        const filterList = document.createElement('div');
        filterList.style.cssText = `
            margin-bottom: 10px;
            max-height: 150px;
            overflow-y: auto;
            border: 1px solid ${config.darkMode ? '#555' : '#ccc'};
            border-radius: 3px;
            padding: 5px;
            background-color: ${config.darkMode ? '#444' : '#fff'};
        `;

		const historyButtonContainer = document.createElement('div');
		historyButtonContainer.style.cssText = `
			display: flex;
			gap: 2px;
		`;

		const backButton = document.createElement('button');
		backButton.innerHTML = '↩️';
		backButton.title = 'Previous State';
		backButton.style.cssText = `
			background: none;
			border: none;
			cursor: pointer;
			font-size: 16px;
			padding: 0 5px;
		`;

		const forwardButton = document.createElement('button');
		forwardButton.innerHTML = '↪️';
		forwardButton.title = 'Next State';
		forwardButton.style.cssText = backButton.style.cssText;
		forwardButton.style.display = 'none'; // Initially hidden

		backButton.addEventListener('click', () => {
			const previousState = historyManager.loadPreviousState();
			if (previousState) {
				const elements = document.querySelectorAll('.img-creat');
				elements.forEach((el, index) => {
					if (previousState.elements[index]) {
						el.innerHTML = previousState.elements[index].html;
						el.setAttribute('data-original-content',
									  previousState.elements[index].originalHtml);
					}
				});
				// Show forward button when we have a state to go forward to
				forwardButton.style.display = 'block';
			}

			// Hide back button if we're at index 0
			if (historyManager.currentHistoryIndex === 0) {
				backButton.style.display = 'none';
			}
		});

		forwardButton.addEventListener('click', () => {
			const nextState = historyManager.loadNextState();
			if (nextState) {
				const elements = document.querySelectorAll('.img-creat');
				elements.forEach((el, index) => {
					if (nextState.elements[index]) {
						el.innerHTML = nextState.elements[index].html;
						el.setAttribute('data-original-content',
									  nextState.elements[index].originalHtml);
					}
				});
                backButton.style.display = 'block';
			}
            else {
				alert('No next state available');
				forwardButton.style.display = 'none';
			}
		});

        // Function to update filter list
        function updateFilterList() {
            filterList.innerHTML = '';
            config.keywords.forEach((keyword, index) => {
                const filterItem = document.createElement('div');
                filterItem.style.cssText = `
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    padding: 3px;
                    border-bottom: 1px solid ${config.darkMode ? '#555' : '#eee'};
                `;

                const filterText = document.createElement('span');
                filterText.style.cssText = `
                    color: ${config.darkMode ? '#fff' : '#000'};
                    font-size: 0.9em;
                    overflow: hidden;
                    text-overflow: ellipsis;
                    white-space: nowrap;
                `;
                filterText.textContent = `${keyword} → ${config.replacements[index]}`;

                const removeButton = document.createElement('button');
                removeButton.textContent = '×';
                removeButton.style.cssText = `
                    background: none;
                    border: none;
                    color: ${config.darkMode ? '#fff' : '#000'};
                    cursor: pointer;
                    padding: 0 5px;
                    font-size: 14px;
                `;

                removeButton.addEventListener('click', () => {
                    config.keywords.splice(index, 1);
                    config.replacements.splice(index, 1);
                    saveSettings();
                    updateFilterList();
                    filterChapterAds();
                });

                filterItem.appendChild(filterText);
                filterItem.appendChild(removeButton);
                filterList.appendChild(filterItem);
            });

            if (config.keywords.length === 0) {
                const emptyMessage = document.createElement('div');
                emptyMessage.style.cssText = `
                    padding: 5px;
                    color: ${config.darkMode ? '#aaa' : '#666'};
                    text-align: center;
                    font-style: italic;
                `;
                emptyMessage.textContent = 'No filters added yet';
                filterList.appendChild(emptyMessage);
            }
        }

        const inputs = document.createElement('div');
        inputs.style.cssText = `
            display: flex;
            flex-direction: column;
            gap: 5px;
        `;

        const keywordInput = document.createElement('input');
        keywordInput.type = 'text';
        keywordInput.placeholder = 'Keyword (e.g., chapter-2)';
        keywordInput.style.cssText = `
            width: 200px;
            padding: 5px;
            border: 1px solid ${config.darkMode ? '#555' : '#ccc'};
            background-color: ${config.darkMode ? '#444' : '#fff'};
            color: ${config.darkMode ? '#fff' : '#000'};
            border-radius: 3px;
        `;

        const replacementInput = document.createElement('input');
        replacementInput.type = 'text';
        replacementInput.placeholder = 'Replacement Text';
        replacementInput.style.cssText = keywordInput.style.cssText;

        const addButton = document.createElement('button');
        addButton.textContent = 'Add';
        addButton.style.cssText = `
            padding: 5px 10px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 3px;
            cursor: pointer;
            margin-top: 5px;
        `;

        settingsButton.addEventListener('click', () => {
            showSetupWizard();
        });

        addButton.addEventListener('click', () => {
            const keyword = keywordInput.value.trim();
            const replacement = replacementInput.value.trim();
            if (keyword && replacement) {
                addKeywordReplacement(keyword, replacement);
                keywordInput.value = '';
                replacementInput.value = '';
                updateFilterList();
            }
        });

        closeButton.addEventListener('click', () => {
            container.remove();
            if (config.showField === 'optional') {
                createToggleButton();
            }
        });

        historyButtonContainer.appendChild(backButton);
        historyButtonContainer.appendChild(forwardButton);
        buttonContainer.appendChild(historyButtonContainer);
        buttonContainer.appendChild(settingsButton);
        buttonContainer.appendChild(closeButton);
        header.appendChild(title);
        header.appendChild(buttonContainer);
        inputs.appendChild(keywordInput);
        inputs.appendChild(replacementInput);
        inputs.appendChild(addButton);

        container.appendChild(header);
        container.appendChild(filterList);
        container.appendChild(inputs);
        document.body.appendChild(container);

        // Initialize the filter list
        updateFilterList();
    }

    // Rest of the functions remain the same...
    function createToggleButton() {
        const button = document.createElement('div');
        button.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            background-color: ${config.darkMode ? '#333' : '#f0f0f0'};
            color: ${config.darkMode ? '#fff' : '#000'};
            padding: 8px;
            border-radius: 4px;
            cursor: pointer;
            z-index: 1000;
            box-shadow: 2px 2px 5px rgba(0,0,0,0.1);
        `;
        button.textContent = 'Show Filter';
        button.addEventListener('click', () => {
            createUI();
            button.remove();
        });

        document.body.appendChild(button);

        if (config.showField === 'optional') {
            setTimeout(() => {
                if (button.parentNode) {
                    button.remove();
                }
            }, config.optionalTimeout * 1000);
        }
    }

    function addKeywordReplacement(keyword, replacement) {
        config.keywords.push(keyword);
        config.replacements.push(replacement);
        saveSettings();
        filterChapterAds();
    }

    function filterChapterAds() {
        const elements = document.querySelectorAll('.img-creat');
        elements.forEach(element => {

            if (!element.hasAttribute('data-original-content')) {
            element.setAttribute('data-original-content', element.innerHTML);
            }

            const aTag = element.querySelector('a');
            if (aTag) {
                const href = aTag.getAttribute('href');
                if (href) {
                    const index = config.keywords.findIndex(keyword => href.includes(keyword));
                    if (index !== -1) {
                        // Store the original content
                        const originalContent = element.innerHTML;

                        // Clear and set new content
                        element.innerHTML = '';
                        const container = document.createElement('div');
                        container.style.display = 'flex';
                        container.style.flexDirection = 'column';
                        container.style.alignItems = 'center';
                        container.style.gap = '5px';

                        const textSpan = document.createElement('span');
                        textSpan.textContent = config.replacements[index];
                        container.appendChild(textSpan);

                        if (config.showAdButton) {
                            const showButton = document.createElement('button');
                            showButton.textContent = 'Show Ad';
                            showButton.style.cssText = `
                            padding: 2px 6px;
                            font-size: 12px;
                            background-color: ${config.darkMode ? '#444' : '#eee'};
                            border: 1px solid ${config.darkMode ? '#666' : '#ccc'};
                            border-radius: 3px;
                            cursor: pointer;
                            color: ${config.darkMode ? '#fff' : '#000'};
                            margin-top: 3px;
                        `;

                            showButton.addEventListener('click', (e) => {
                                e.preventDefault();
                                element.innerHTML = originalContent;
                            });

                            container.appendChild(showButton);
                        }

                        element.appendChild(container);
                    }
                }
            }
        });

        historyManager.saveState(Array.from(elements));
    }

    function saveSettings() {
        GM_setValue('keywords', config.keywords);
        GM_setValue('replacements', config.replacements);
    }

    function initializeUI() {
        if (config.showField === 'always') {
            createUI();
        } else if (config.showField === 'optional') {
            createToggleButton();
        }
    }

    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            if (mutation.addedNodes.length > 0) {
                mutation.addedNodes.forEach(node => {
                    if (node.querySelectorAll) {
                        const newElements = node.querySelectorAll('.img-creat');
                        if (newElements.length > 0) {
                            filterChapterAds();
                        }
                    }
                });
            }
        });
    });

	const historyManager = {
		currentSession: null,
		currentHistoryIndex: -1,

		init: function() {
			this.currentSession = Date.now();
			const savedSessions = GM_getValue('historySessions', {});

			// Clean up old sessions if exceeding max limit
			const sessions = Object.keys(savedSessions).sort();
			while (sessions.length > config.maxHistorySessions) {
				delete savedSessions[sessions[0]];
				sessions.shift();
			}

			// Set initial index to the last session
			this.currentHistoryIndex = sessions.length > 0 ? sessions.length - 1 : 0;
			GM_setValue('historySessions', savedSessions);
		},

		saveState: function(elements) {
			if (!config.saveHistory) return;

			const savedSessions = GM_getValue('historySessions', {});
			savedSessions[this.currentSession] = {
				timestamp: new Date().toISOString(),
				elements: elements.map(el => ({
					html: el.innerHTML,
					originalHtml: el.getAttribute('data-original-content')
				}))
			};

			GM_setValue('historySessions', savedSessions);
			this.currentHistoryIndex = Object.keys(savedSessions).sort().length - 1;
		},

		loadPreviousState: function() {
			const savedSessions = GM_getValue('historySessions', {});
			const sessions = Object.keys(savedSessions).sort();

			// Adjust index to skip current state on first click
			if (this.currentHistoryIndex === sessions.length - 1) {
				this.currentHistoryIndex--;
			}

			if (this.currentHistoryIndex > 0) {
				this.currentHistoryIndex--;
				return savedSessions[sessions[this.currentHistoryIndex]];
			}

			// Hide back button when reaching index 0
			if (this.currentHistoryIndex === 0) {
				const backButton = document.querySelector('[title="Previous State"]');
				if (backButton) backButton.style.display = 'none';
			}

			return null;
		},

		loadNextState: function() {
			const savedSessions = GM_getValue('historySessions', {});
            const sessions = Object.keys(savedSessions).sort();
            const length = sessions.length - 1;

			if (this.currentHistoryIndex < length) {
				this.currentHistoryIndex++;
                if (this.currentHistoryIndex >= length-1) {
					const forwardButton = document.querySelector('[title="Next State"]');
					if (forwardButton) forwardButton.style.display = 'none';
				}
				return savedSessions[sessions[this.currentHistoryIndex]];
			}
			return null;
		}
	};

    const observerConfig = { childList: true, subtree: true };
    observer.observe(document.body, observerConfig);

    if (config.showField === null) {
        showSetupWizard();
    } else {
        initializeUI();
    }
    filterChapterAds();
    if (config.saveHistory === true){
        historyManager.init();
    }

})();