Smart Flow for WebNovel

A Script that lets you modify the formatting of webnovel stories. You can customise font type, font size, text width & line height. + Plus more in future updates

// ==UserScript==
// @name         Smart Flow for WebNovel
// @description  A Script that lets you modify the formatting of webnovel stories. You can customise font type, font size, text width & line height. + Plus more in future updates
// @version      1.1
// @author       Grimlock7
// @license      MIT
// @namespace    smartflow-webnovel
// @match        https://www.webnovel.com/book/*/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    let smartFlowEnabled = false;
    let dragEnabled = true;

    const defaultSettings = {
        fontFamily: 'Georgia',
        fontSize: '22px',
        lineHeight: '1.8',
        maxWidth: '1800px',
        autoEnable: false
    };

    const loadSettings = () => {
        const saved = localStorage.getItem('smartflow-settings');
        return saved ? JSON.parse(saved) : { ...defaultSettings };
    };

    const saveSettings = (settings) => {
        localStorage.setItem('smartflow-settings', JSON.stringify(settings));
    };

    const updateStyles = (settings) => {
    const style = document.getElementById('smartflow-style');
    if (style) style.remove();

    const newStyle = document.createElement('style');
    newStyle.id = 'smartflow-style';
    newStyle.textContent = `
        .smartflow-container {
            max-width: ${settings.maxWidth};
            margin: 0 auto;
            padding: 0 20px;
            font-family: ${settings.fontFamily};
        }
        .smartflow-paragraph {
            font-size: ${settings.fontSize};
            line-height: ${settings.lineHeight};
            word-break: break-word;
            hyphens: auto;
            white-space: normal;
            text-align: justify;
            margin-bottom: 1em;
        }
    `;
    document.head.appendChild(newStyle);
};
        const updateLiveStyles = (settings) => {
    updateStyles(settings);

    document.querySelectorAll('.smartflow-container').forEach(container => {
        container.style.maxWidth = settings.maxWidth;
        container.style.fontFamily = settings.fontFamily;
    });

    document.querySelectorAll('.smartflow-paragraph').forEach(p => {
        p.style.fontSize = settings.fontSize;
        p.style.lineHeight = settings.lineHeight;
        p.style.fontFamily = settings.fontFamily;
    });
};

    const detectStoryContainer = () => {
        return Array.from(document.querySelectorAll('div'))
            .find(div => div.innerText.length > 500 && div.offsetHeight > 300);
    };

    const applySmartFlow = (div, settings) => {
    if (!div || div.classList.contains('smartflow-container')) return;

    div.classList.add('smartflow-container');
    updateStyles(settings);

    const blocks = div.querySelectorAll('p, div');
    blocks.forEach(el => {
        el.classList.add('smartflow-paragraph');
        el.style.fontSize = settings.fontSize;
        el.style.lineHeight = settings.lineHeight;
        el.style.fontFamily = settings.fontFamily;
    });
};

    const removeSmartFlow = (div) => {
    if (!div) return;

    // Remove SmartFlow container class
    div.classList.remove('smartflow-container');

    // Remove SmartFlow paragraph class and inline styles
    const blocks = div.querySelectorAll('.smartflow-paragraph');
    blocks.forEach(el => {
        el.classList.remove('smartflow-paragraph');
        el.style.fontSize = '';
        el.style.lineHeight = '';
        el.style.fontFamily = '';
        el.style.maxWidth = '';
    });

    // Remove injected style block
    const style = document.getElementById('smartflow-style');
    if (style) style.remove();
};
    const updatePanelPosition = () => {
        const btn = document.getElementById('smartflow-toggle-btn');
        const panel = document.getElementById('smartflow-panel');
        if (!btn || !panel) return;

        const rect = btn.getBoundingClientRect();
        panel.style.top = rect.bottom + 10 + 'px';
        panel.style.right = (window.innerWidth - rect.right) + 'px';
    };

    const createSettingsPanel = () => {
        const settings = loadSettings();

        const panel = document.createElement('div');
        panel.id = 'smartflow-panel';
        panel.style.position = 'fixed';
        panel.style.zIndex = '9999';
        panel.style.background = '#fff';
        panel.style.border = '1px solid #ccc';
        panel.style.borderRadius = '6px';
        panel.style.padding = '12px';
        panel.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
        panel.style.display = 'none';
        panel.style.minWidth = '240px';

        panel.innerHTML = `
            <label>Font:
                <select id="sf-font">
                    <option value="Arial">Arial</option>
                    <option value="Georgia">Georgia</option>
                    <option value="sans-serif">Sans-serif</option>
                    <option value="serif">Serif</option>
                    <option value="Times New Roman">Times New Roman</option>
                </select>
            </label><br><br>
            <label>Font Size:
                <input type="range" id="sf-size" min="12" max="36" value="${parseInt(settings.fontSize)}">
                <span id="sf-size-label">${settings.fontSize}</span>
            </label><br><br>
            <label>Line Height:
                <input type="range" id="sf-line" min="1.0" max="3.0" step="0.1" value="${parseFloat(settings.lineHeight)}">
                <span id="sf-line-label">${settings.lineHeight}</span>
            </label><br><br>
            <label>Text Width:
                <input type="range" id="sf-width" min="800" max="1800" step="50" value="${parseInt(settings.maxWidth)}">
                <span id="sf-width-label">${settings.maxWidth}</span> px
            </label><br><br>
            <label>
                <input type="checkbox" id="sf-auto" ${settings.autoEnable ? 'checked' : ''}>
                Auto Enable SmartFlow
            </label><br><br>
            <button id="sf-toggle">SmartFlow OFF </button><br><br>
            </label><br><br>
            <button id="sf-reset-pos">Reset Button Position</button>        `;

        document.body.appendChild(panel);
        document.getElementById('sf-font').value = settings.fontFamily;

        const getSettings = () => ({
            fontFamily: document.getElementById('sf-font').value,
            fontSize: document.getElementById('sf-size').value + 'px',
            lineHeight: document.getElementById('sf-line').value,
            maxWidth: document.getElementById('sf-width').value + 'px',
            autoEnable: document.getElementById('sf-auto').checked
        });
        const toggleBtn = document.getElementById('sf-toggle');
          toggleBtn.style.backgroundColor = smartFlowEnabled ? 'green' : 'red';
          toggleBtn.style.color = '#fff';
          toggleBtn.style.border = 'none';
          toggleBtn.style.padding = '6px 10px';
          toggleBtn.style.borderRadius = '4px';
          toggleBtn.style.cursor = 'pointer';


toggleBtn.addEventListener('click', () => {
    const storyDiv = detectStoryContainer();
    if (!storyDiv) return;

    smartFlowEnabled = !smartFlowEnabled;

    if (smartFlowEnabled) {
        applySmartFlow(storyDiv, loadSettings());
        toggleBtn.textContent = 'SmartFlow ON';
        toggleBtn.style.backgroundColor = 'green';
    } else {
        removeSmartFlow(storyDiv);
        toggleBtn.textContent = 'SmartFlow OFF';
        toggleBtn.style.backgroundColor = 'red';
    }
});

        const handleSettingChange = () => {
            const newSettings = getSettings();
            saveSettings(newSettings);
            if (smartFlowEnabled) {
                updateLiveStyles(newSettings);
            }
            document.getElementById('sf-size-label').textContent = document.getElementById('sf-size').value + 'px';
            document.getElementById('sf-line-label').textContent = document.getElementById('sf-line').value;
            document.getElementById('sf-width-label').textContent = document.getElementById('sf-width').value;
        };

        ['sf-font', 'sf-size', 'sf-line', 'sf-width', 'sf-auto'].forEach(id => {
            document.getElementById(id).addEventListener('input', handleSettingChange);
        });

        document.getElementById('sf-reset-pos').addEventListener('click', () => {
            localStorage.removeItem('smartflow-button-pos');
            const btn = document.getElementById('smartflow-toggle-btn');
            if (btn) {
                btn.style.top = '10px';
                btn.style.left = 'auto';
                btn.style.right = '460px';
                updatePanelPosition();
            }
        });
    };

    const createToggleButton = () => {
        const savedPos = JSON.parse(localStorage.getItem('smartflow-button-pos')) || {
            top: 10,
            anchor: 'right',
            offset: 460
        };

        const btn = document.createElement('button');
        btn.textContent = 'SmartFlow';
        btn.id = 'smartflow-toggle-btn';
        btn.style.position = 'fixed';
        btn.style.top = savedPos.top + 'px';
        btn.style.zIndex = '9999';
        btn.style.padding = '8px 12px';
        btn.style.fontSize = '14px';
        btn.style.background = '#222';
        btn.style.color = '#fff';
        btn.style.border = 'none';
        btn.style.borderRadius = '4px';
        btn.style.cursor = 'move';
        btn.style.boxShadow = '0 2px 6px rgba(0,0,0,0.3)';
        btn.style.userSelect = 'none';
        btn.style.whiteSpace = 'normal';
        btn.style.wordBreak = 'break-word';
        btn.style.maxWidth = '160px';

        if (savedPos.anchor === 'left') {
            btn.style.left = savedPos.offset + 'px';
            btn.style.right = 'auto';
        } else {
            btn.style.right = savedPos.offset + 'px';
            btn.style.left = 'auto';
        }

        let isDragging = false;
        let offsetX = 0, offsetY = 0;

                btn.addEventListener('mousedown', (e) => {
            if (!dragEnabled) {
                const panel = document.getElementById('smartflow-panel');
                if (panel) {
                    panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
                    updatePanelPosition();
                }
                return;
            }

            isDragging = false;
            offsetX = e.clientX;
            offsetY = e.clientY;

            const startTop = btn.offsetTop;
            const startLeft = btn.offsetLeft;
            const startRight = window.innerWidth - btn.offsetLeft - btn.offsetWidth;

            const onMouseMove = (e) => {
                const deltaX = e.clientX - offsetX;
                const deltaY = e.clientY - offsetY;

                if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) {
                    isDragging = true;
                }

                const newTop = Math.min(Math.max(startTop + deltaY, 0), window.innerHeight - btn.offsetHeight);
                btn.style.top = newTop + 'px';

                let anchor, offset;
                if (e.clientX < window.innerWidth / 2) {
                    anchor = 'left';
                    offset = Math.min(Math.max(startLeft + deltaX, 0), window.innerWidth - btn.offsetWidth);
                    btn.style.left = offset + 'px';
                    btn.style.right = 'auto';
                } else {
                    anchor = 'right';
                    offset = Math.min(Math.max(startRight - deltaX, 0), window.innerWidth - btn.offsetWidth);
                    btn.style.right = offset + 'px';
                    btn.style.left = 'auto';
                }

                localStorage.setItem('smartflow-button-pos', JSON.stringify({ top: newTop, anchor, offset }));
                updatePanelPosition();
            };

            const onMouseUp = () => {
                document.removeEventListener('mousemove', onMouseMove);
                document.removeEventListener('mouseup', onMouseUp);

                if (!isDragging) {
                    const panel = document.getElementById('smartflow-panel');
                    if (panel) {
                        panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
                        updatePanelPosition();
                    }
                }
            };

            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
        });

        document.body.appendChild(btn);
        updatePanelPosition();
    };

    const watchUrlChange = () => {
        let lastUrl = location.href;
        setInterval(() => {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                const storyDiv = detectStoryContainer();
                if (storyDiv && smartFlowEnabled) {
                    applySmartFlow(storyDiv, loadSettings());
                }
            }
        }, 1000);
    };

    // Initialize everything
    createSettingsPanel();
    createToggleButton();
    watchUrlChange();

    const settings = loadSettings();
    if (settings.autoEnable) {
        const storyDiv = detectStoryContainer();
        if (storyDiv) {
            smartFlowEnabled = true;
            applySmartFlow(storyDiv, settings);
            document.getElementById('smartflow-panel').style.display = 'block';
        }
    }
})();