RoyalRoad Leads to Chapter Filter

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

})();