Greasy Fork is available in English.

WTR-Lab Reader & UI Enhancer

Adds a responsive configuration panel to adjust reader width, navigation width, and optionally override font style on wtr-lab.com.

// ==UserScript==
// @name         WTR-Lab Reader & UI Enhancer
// @namespace    http://tampermonkey.net/
// @version      2.9
// @description  Adds a responsive configuration panel to adjust reader width, navigation width, and optionally override font style on wtr-lab.com.
// @author       MasuRii
// @license      MIT
// @match        https://wtr-lab.com/en/novel/*/*/chapter-*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @connect      gwfh.mranftl.com
// @icon         https://www.google.com/s2/favicons?sz=64&domain=wtr-lab.com
// ==/UserScript==

(function() {
    'use strict';

    // --- CONFIGURATION ---
    const DEBUG_KEY = 'wtr_lab_enhancer_debug';
    const STEP_WIDTH = 50;
    const MIN_WIDTH = 300;
    const FONTS_API_URL = 'https://gwfh.mranftl.com/api/fonts';

    const RECOMMENDED_FONTS = {
        serif: ['Merriweather', 'Lora', 'Crimson Text', 'Libre Baskerville', 'Spectral', 'EB Garamond', 'Noto Serif'],
        sansSerif: ['Roboto', 'Open Sans', 'Source Sans Pro']
    };

    const configs = {
        reader: {
            key: 'wtr_lab_reader_width',
            selector: '.fix-size.card',
            defaultWidth: 760,
            label: 'Reader Content Width'
        },
        nav: {
            key: 'wtr_lab_nav_width',
            selector: 'nav.bottom-reader-nav .fix-size',
            defaultWidth: 760,
            label: 'Bottom Navigator Width'
        },
        navConstraint: {
            key: 'wtr_lab_nav_constraint',
            selector: 'nav.bottom-reader-nav',
            defaultState: false,
            label: 'Constrain Navigator Background'
        },
        fontToggle: {
            key: 'wtr_lab_font_style_enabled',
            defaultState: false,
            label: 'Enable Custom Font Style'
        },
        font: {
            key: 'wtr_lab_font_family',
            selector: '.chapter-body',
            defaultFont: 'Merriweather',
            label: 'Font Style'
        }
    };

    // --- DEBUG LOGGER ---
    let isDebugEnabled = GM_getValue(DEBUG_KEY, false);
    const log = (...args) => {
        if (isDebugEnabled) console.log('[WTR-Lab Enhancer]', ...args);
    };
    const toggleDebugLogging = () => {
        isDebugEnabled = !isDebugEnabled;
        GM_setValue(DEBUG_KEY, isDebugEnabled);
        alert(`Debug logging is now ${isDebugEnabled ? 'ENABLED' : 'DISABLED'}.`);
    };

    // --- CORE LOGIC ---

    const applyWidthStyle = (configName, width) => {
        const styleId = `custom-width-styler-${configName}`;
        let styleElement = document.getElementById(styleId);
        if (!styleElement) {
            styleElement = document.createElement('style');
            styleElement.id = styleId;
            document.head.appendChild(styleElement);
        }
        styleElement.textContent = `${configs[configName].selector} { max-width: ${width}px !important; }`;
    };

    const updateNavConstraint = () => {
        const isConstrained = loadValue(configs.navConstraint.key, configs.navConstraint.defaultState);
        const styleId = 'custom-nav-constraint-styler';
        let styleElement = document.getElementById(styleId);

        if (isConstrained) {
            if (!styleElement) {
                styleElement = document.createElement('style');
                styleElement.id = styleId;
                document.head.appendChild(styleElement);
            }
            const navContentWidth = loadValue(configs.nav.key, configs.nav.defaultWidth);
            const marginValue = Math.max(0, (window.innerWidth - navContentWidth) / 2);
            styleElement.textContent = `${configs.navConstraint.selector} { margin-left: ${marginValue}px !important; margin-right: ${marginValue}px !important; }`;
            log(`Navigator constrained with ${marginValue}px margins.`);
        } else {
            if (styleElement) {
                styleElement.remove();
                log('Navigator constraint removed.');
            }
        }
    };

    const fetchFonts = () => {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: FONTS_API_URL,
                onload: (response) => {
                    try {
                        if (response.status !== 200) throw new Error(`HTTP error! Status: ${response.status}`);
                        const data = JSON.parse(response.responseText);
                        const recommended = [...RECOMMENDED_FONTS.serif, ...RECOMMENDED_FONTS.sansSerif];
                        const otherFonts = data.map(f => f.family).filter(f => !recommended.includes(f)).sort();
                        resolve({
                            recommendedSerif: RECOMMENDED_FONTS.serif,
                            recommendedSansSerif: RECOMMENDED_FONTS.sansSerif,
                            other: otherFonts
                        });
                    } catch (error) {
                        resolve(getFallbackFonts());
                    }
                },
                onerror: () => resolve(getFallbackFonts())
            });
        });
    };

    const getFallbackFonts = () => {
        log('Using fallback font list.');
        return {
            recommendedSerif: RECOMMENDED_FONTS.serif,
            recommendedSansSerif: RECOMMENDED_FONTS.sansSerif,
            other: ['Georgia', 'Times New Roman', 'Arial', 'Verdana'] // Basic system fonts
        };
    };

    const removeFontStyle = () => {
        log('Removing custom font style elements.');
        const styleElement = document.getElementById('custom-font-styler');
        if (styleElement) styleElement.remove();

        const linkElement = document.getElementById('userscript-font-link');
        if (linkElement) linkElement.remove();
    };

    const applyFontStyle = (fontFamily) => {
        const isEnabled = loadValue(configs.fontToggle.key, configs.fontToggle.defaultState);
        if (!isEnabled) {
            removeFontStyle();
            return;
        }

        log(`Applying font: ${fontFamily}`);
        const primaryFont = fontFamily.split(',')[0].trim();
        const styleId = 'custom-font-styler';
        let styleElement = document.getElementById(styleId);
        if (!styleElement) {
            styleElement = document.createElement('style');
            styleElement.id = styleId;
            document.head.appendChild(styleElement);
        }

        const fontUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(primaryFont)}&display=swap`;
        let linkElement = document.getElementById('userscript-font-link');
        if (!linkElement) {
            linkElement = document.createElement('link');
            linkElement.id = 'userscript-font-link';
            linkElement.rel = 'stylesheet';
            document.head.appendChild(linkElement);
        }
        linkElement.href = fontUrl;

        styleElement.textContent = `${configs.font.selector} { font-family: "${primaryFont}", serif, sans-serif !important; }`;
    };

    const saveValue = (key, value) => GM_setValue(key, value);
    const loadValue = (key, defaultValue) => GM_getValue(key, defaultValue);

    // --- UI CONFIGURATION PANEL ---

    const createConfigPanel = () => {
        const panelHTML = `<div id="wtr-config-overlay" style="display: none;"><div id="wtr-config-panel"><h2>WTR-Lab Enhancer Settings</h2><div id="wtr-config-sections"></div><button id="wtr-config-close-btn" class="wtr-config-button">Close</button></div></div>`;
        document.body.insertAdjacentHTML('beforeend', panelHTML);
        GM_addStyle(getPanelCSS());

        const sectionsContainer = document.getElementById('wtr-config-sections');
        for (const [name, config] of Object.entries(configs)) {
            let sectionHTML;
            if (name === 'fontToggle') {
                continue;
            } else if (name === 'font') {
                sectionHTML = `
                    <div class="wtr-config-section">
                        <div class="wtr-config-controls checkbox-control">
                            <input type="checkbox" id="wtr-fontToggle-toggle">
                            <label for="wtr-fontToggle-toggle">${configs.fontToggle.label}</label>
                        </div>
                        <label for="wtr-font-select">${config.label}</label>
                        <div class="wtr-config-controls font-controls">
                            <select id="wtr-font-select"></select>
                            <div class="wtr-button-group">
                                <button id="wtr-font-refresh-btn" class="wtr-config-button">Refresh</button>
                                <button id="wtr-font-reset-btn" class="wtr-config-button reset">Reset</button>
                            </div>
                        </div>
                    </div>`;
            } else if (name === 'navConstraint') {
                sectionHTML = `<div class="wtr-config-section"><div class="wtr-config-controls checkbox-control"><input type="checkbox" id="wtr-navConstraint-toggle"><label for="wtr-navConstraint-toggle">${config.label}</label></div></div>`;
            } else { // reader, nav
                sectionHTML = `<div class="wtr-config-section"><label for="wtr-${name}-width-input">${config.label} (px)</label><div class="wtr-config-controls"><button id="wtr-${name}-decrease-btn" class="wtr-config-button control">-</button><input type="number" id="wtr-${name}-width-input" min="${MIN_WIDTH}" step="10"><button id="wtr-${name}-increase-btn" class="wtr-config-button control">+</button><button id="wtr-${name}-reset-btn" class="wtr-config-button reset">Reset</button></div></div>`;
            }
            sectionsContainer.insertAdjacentHTML('beforeend', sectionHTML);
        }
        populateFontDropdown(getFallbackFonts());
        attachPanelEventListeners();
    };

    const populateFontDropdown = async (initialFontGroups = null) => {
        const fontSelect = document.getElementById('wtr-font-select');
        if (!fontSelect) return;
        const currentFont = loadValue(configs.font.key, configs.font.defaultFont);
        fontSelect.innerHTML = '';
        const fontGroups = initialFontGroups || await fetchFonts();

        const groupLabels = {
            recommendedSerif: 'Recommended for Reading (Serif)',
            recommendedSansSerif: 'Recommended for Reading (Sans-serif)',
            other: 'All Other Fonts'
        };

        for (const groupKey in fontGroups) {
            if (fontGroups[groupKey].length === 0) continue;
            const optgroup = document.createElement('optgroup');
            optgroup.label = groupLabels[groupKey] || 'Fonts';
            fontGroups[groupKey].forEach(font => {
                const option = document.createElement('option');
                option.value = font;
                option.textContent = font;
                optgroup.appendChild(option);
            });
            fontSelect.appendChild(optgroup);
        }
        const allFonts = Object.values(fontGroups).flat();
        fontSelect.value = allFonts.includes(currentFont) ? currentFont : configs.font.defaultFont;
    };

    const attachPanelEventListeners = () => {
        document.getElementById('wtr-config-overlay').addEventListener('click', (e) => {
            if (e.target.id === 'wtr-config-overlay') hideConfigPanel();
        });
        document.getElementById('wtr-config-close-btn').addEventListener('click', hideConfigPanel);

        const updateSetting = (configName, value) => {
            const config = configs[configName];
            saveValue(config.key, value);

            if (configName === 'font') {
                applyFontStyle(value);
                document.getElementById('wtr-font-select').value = value;
            } else if (configName === 'fontToggle') {
                updateFontControlsState(value);
                applyFontStyle(loadValue(configs.font.key, configs.font.defaultFont));
            } else if (configName === 'navConstraint') {
                updateNavConstraint();
                document.getElementById('wtr-navConstraint-toggle').checked = value;
            } else { // width controls
                const validatedWidth = Math.max(MIN_WIDTH, parseInt(value, 10));
                if (isNaN(validatedWidth)) return;
                applyWidthStyle(configName, validatedWidth);
                document.getElementById(`wtr-${configName}-width-input`).value = validatedWidth;
                saveValue(config.key, validatedWidth);
                if (configName === 'nav') {
                    updateNavConstraint();
                }
            }
        };

        for (const [name, config] of Object.entries(configs)) {
            if (name === 'font') {
                const select = document.getElementById('wtr-font-select');
                select.addEventListener('change', () => updateSetting(name, select.value));
                document.getElementById('wtr-font-reset-btn').addEventListener('click', () => updateSetting(name, config.defaultFont));
                const refreshBtn = document.getElementById('wtr-font-refresh-btn');
                refreshBtn.addEventListener('click', async () => {
                    refreshBtn.textContent = 'Fetching...';
                    refreshBtn.disabled = true;
                    await populateFontDropdown();
                    refreshBtn.textContent = 'Refresh';
                    refreshBtn.disabled = false;
                });
            } else if (name === 'fontToggle') {
                const toggle = document.getElementById('wtr-fontToggle-toggle');
                toggle.addEventListener('change', () => updateSetting(name, toggle.checked));
            } else if (name === 'navConstraint') {
                const toggle = document.getElementById('wtr-navConstraint-toggle');
                toggle.addEventListener('change', () => updateSetting(name, toggle.checked));
            } else { // reader, nav
                const input = document.getElementById(`wtr-${name}-width-input`);
                document.getElementById(`wtr-${name}-increase-btn`).addEventListener('click', () => updateSetting(name, parseInt(input.value, 10) + STEP_WIDTH));
                document.getElementById(`wtr-${name}-decrease-btn`).addEventListener('click', () => updateSetting(name, parseInt(input.value, 10) - STEP_WIDTH));
                document.getElementById(`wtr-${name}-reset-btn`).addEventListener('click', () => updateSetting(name, config.defaultWidth));
                input.addEventListener('change', () => updateSetting(name, input.value));
            }
        }
    };

    const updateFontControlsState = (isEnabled) => {
        document.getElementById('wtr-font-select').disabled = !isEnabled;
        document.getElementById('wtr-font-refresh-btn').disabled = !isEnabled;
        document.getElementById('wtr-font-reset-btn').disabled = !isEnabled;
    };

    const showConfigPanel = () => {
        log('Showing configuration panel.');
        for (const [name, config] of Object.entries(configs)) {
            if (name === 'font') {
                document.getElementById('wtr-font-select').value = loadValue(config.key, config.defaultFont);
            } else if (name === 'fontToggle') {
                const isEnabled = loadValue(config.key, config.defaultState);
                document.getElementById('wtr-fontToggle-toggle').checked = isEnabled;
                updateFontControlsState(isEnabled);
            } else if (name === 'navConstraint') {
                document.getElementById('wtr-navConstraint-toggle').checked = loadValue(config.key, config.defaultState);
            } else if (configs[name].selector) { // reader, nav
                document.getElementById(`wtr-${name}-width-input`).value = loadValue(config.key, config.defaultWidth);
            }
        }
        document.getElementById('wtr-config-overlay').style.display = 'flex';
    };

    const hideConfigPanel = () => document.getElementById('wtr-config-overlay').style.display = 'none';

    const getPanelCSS = () => `
        #wtr-config-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); z-index: 9999; display: flex; justify-content: center; align-items: center; }
        #wtr-config-panel { background: #2c2c2c; color: #f1f1f1; padding: 25px; border-radius: 8px; width: 90%; max-width: 500px; box-shadow: 0 5px 15px rgba(0,0,0,0.5); font-family: sans-serif; display: flex; flex-direction: column; gap: 20px; }
        #wtr-config-panel h2 { margin: 0 0 10px 0; text-align: center; }
        .wtr-config-section { display: flex; flex-direction: column; gap: 8px; padding: 15px; border: 1px solid #444; border-radius: 5px; }
        .wtr-config-controls { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
        .wtr-config-controls.font-controls { flex-wrap: nowrap; }
        .wtr-config-controls.checkbox-control { flex-direction: row; justify-content: center; cursor: pointer; }
        .wtr-config-controls.checkbox-control label { user-select: none; }
        .wtr-config-controls.checkbox-control input { margin-right: 8px; }
        .wtr-config-controls input[type="number"], .wtr-config-controls select { flex-grow: 1; min-width: 100px; text-align: center; background: #444; color: #fff; border: 1px solid #666; border-radius: 4px; padding: 8px; }
        .wtr-config-controls select:disabled { background: #3a3a3a; color: #888; cursor: not-allowed; }
        .wtr-button-group { display: flex; gap: 10px; flex-shrink: 0; margin-left: auto; }
        .wtr-config-button { padding: 8px 12px; border: none; border-radius: 4px; cursor: pointer; background-color: #007bff; color: white; font-weight: bold; flex-shrink: 0; transition: background-color 0.2s; }
        .wtr-config-button:disabled { background-color: #555; cursor: not-allowed; }
        .wtr-config-button.control { width: 40px; }
        .wtr-config-button.reset { background-color: #dc3545; }
        #wtr-config-close-btn { background-color: #6c757d; align-self: center; width: 100px; }
    `;

    // --- INITIALIZATION ---
    const init = async () => {
        log('Initializing script...');
        createConfigPanel();

        // Apply initial styles
        applyWidthStyle('reader', loadValue(configs.reader.key, configs.reader.defaultWidth));
        applyWidthStyle('nav', loadValue(configs.nav.key, configs.nav.defaultWidth));
        updateNavConstraint();

        window.addEventListener('resize', updateNavConstraint);

        // Fetch fonts, validate saved font, and apply if enabled
        const fontGroups = await fetchFonts();
        const allAvailableFonts = Object.values(fontGroups).flat();
        let initialFont = loadValue(configs.font.key, configs.font.defaultFont);
        if (!allAvailableFonts.includes(initialFont)) {
            log(`Saved font "${initialFont}" not found. Reverting to default.`);
            initialFont = configs.font.defaultFont;
            saveValue(configs.font.key, initialFont);
        }
        applyFontStyle(initialFont); // This will respect the enable/disable toggle
        populateFontDropdown(fontGroups);

        GM_registerMenuCommand('Configure Settings', showConfigPanel);
        GM_registerMenuCommand('Toggle Debug Logging', toggleDebugLogging);
        log('Initialization complete.');
    };

    init();
})();