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      3.4
// @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
// @connect      fonts.googleapis.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' },
        blockAddTerm: { key: 'wtr_lab_block_add_term', selector: '.floating-add-term-btn', defaultState: false, label: 'Block "Add Term" Button' },
        hideBookBtn: { key: 'wtr_lab_hide_book_btn', selector: 'div.btn-group button:has(svg use[href="/icons/sprite.svg#book"])', defaultState: false, label: 'Book', iconHTML: '<svg><use href="/icons/sprite.svg#book"></use></svg>' },
        hideTextFieldsBtn: { key: 'wtr_lab_hide_text_fields_btn', selector: 'div.btn-group button:has(svg use[href="/icons/sprite.svg#text_fields"])', defaultState: false, label: 'Text', iconHTML: '<svg><use href="/icons/sprite.svg#text_fields"></use></svg>' },
        hideTtsBtn: { key: 'wtr_lab_hide_tts_btn', selector: 'div.btn-group button:has(svg use[href="/icons/sprite.svg#tts"])', defaultState: false, label: 'TTS', iconHTML: '<svg><use href="/icons/sprite.svg#tts"></use></svg>' },
        hideCogBtn: { key: 'wtr_lab_hide_cog_btn', selector: 'div.btn-group button:has(svg use[href="/icons/sprite.svg#cog-outline"])', defaultState: false, label: 'Settings', iconHTML: '<svg><use href="/icons/sprite.svg#cog-outline"></use></svg>' },
        hideListBtn: { key: 'wtr_lab_hide_list_btn', selector: 'div.btn-group button:has(svg use[href="/icons/sprite.svg#list"])', defaultState: false, label: 'List', iconHTML: '<svg><use href="/icons/sprite.svg#list"></use></svg>' }
    };

    // --- 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 saveValue = (key, value) => GM_setValue(key, value);
    const loadValue = (key, defaultValue) => GM_getValue(key, defaultValue);

    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; }`;
        } else if (styleElement) { styleElement.remove(); }
    };

    const updateBlockAddTerm = () => {
        const isBlocked = loadValue(configs.blockAddTerm.key, configs.blockAddTerm.defaultState);
        const styleId = 'custom-block-add-term-styler';
        let styleElement = document.getElementById(styleId);
        if (isBlocked) {
            if (!styleElement) { styleElement = document.createElement('style'); styleElement.id = styleId; document.head.appendChild(styleElement); }
            styleElement.textContent = `${configs.blockAddTerm.selector} { display: none !important; }`;
        } else if (styleElement) { styleElement.remove(); }
    };

    const updateButtonVisibilityStyles = () => {
        const styleId = 'custom-button-visibility-styler';
        let styleElement = document.getElementById(styleId);
        const buttonConfigs = [configs.hideBookBtn, configs.hideTextFieldsBtn, configs.hideTtsBtn, configs.hideCogBtn, configs.hideListBtn];
        const selectorsToHide = buttonConfigs.filter(config => loadValue(config.key, config.defaultState)).map(config => config.selector);
        if (!styleElement && selectorsToHide.length > 0) { styleElement = document.createElement('style'); styleElement.id = styleId; document.head.appendChild(styleElement); }
        if (styleElement) { styleElement.textContent = selectorsToHide.length > 0 ? `${selectorsToHide.join(', ')} { display: none !important; }` : ''; }
    };

    const fetchFonts = () => new Promise(resolve => GM_xmlhttpRequest({ method: 'GET', url: FONTS_API_URL, onload: r => { try { const d = JSON.parse(r.responseText); const rec = [...RECOMMENDED_FONTS.serif, ...RECOMMENDED_FONTS.sansSerif]; resolve({ recommendedSerif: RECOMMENDED_FONTS.serif, recommendedSansSerif: RECOMMENDED_FONTS.sansSerif, other: d.map(f => f.family).filter(f => !rec.includes(f)).sort() }); } catch (e) { resolve(getFallbackFonts()); } }, onerror: () => resolve(getFallbackFonts()) }));
    const getFallbackFonts = () => ({ recommendedSerif: RECOMMENDED_FONTS.serif, recommendedSansSerif: RECOMMENDED_FONTS.sansSerif, other: ['Georgia', 'Times New Roman', 'Arial', 'Verdana'] });

    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; }`;
    };

    // --- 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');

        // Section 1: Layout & Sizing
        const layoutSection = document.createElement('div');
        layoutSection.className = 'wtr-config-section';
        layoutSection.innerHTML = `<label class="wtr-section-title">Layout & Sizing</label>
            <div class="wtr-control-group">
                <label for="wtr-reader-width-input">${configs.reader.label} (px)</label>
                <div class="wtr-config-controls"><button id="wtr-reader-decrease-btn" class="wtr-config-button control">-</button><input type="number" id="wtr-reader-width-input" min="${MIN_WIDTH}" step="10"><button id="wtr-reader-increase-btn" class="wtr-config-button control">+</button><button id="wtr-reader-reset-btn" class="wtr-config-button reset">Reset</button></div>
            </div>
            <div class="wtr-control-group">
                <label for="wtr-nav-width-input">${configs.nav.label} (px)</label>
                <div class="wtr-config-controls"><button id="wtr-nav-decrease-btn" class="wtr-config-button control">-</button><input type="number" id="wtr-nav-width-input" min="${MIN_WIDTH}" step="10"><button id="wtr-nav-increase-btn" class="wtr-config-button control">+</button><button id="wtr-nav-reset-btn" class="wtr-config-button reset">Reset</button></div>
            </div>`;
        sectionsContainer.appendChild(layoutSection);

        // Section 2: Font Customization
        const fontSection = document.createElement('div');
        fontSection.className = 'wtr-config-section';
        fontSection.innerHTML = `<label class="wtr-section-title">Font Customization</label>
            <div class="wtr-config-controls checkbox-control"><input type="checkbox" id="wtr-fontToggle-toggle"><label for="wtr-fontToggle-toggle">${configs.fontToggle.label}</label></div>
            <div class="wtr-control-group">
                <label for="wtr-font-select">${configs.font.label}</label>
                <div class="wtr-config-controls font-controls"><select id="wtr-font-select"></select></div>
                <div class="wtr-config-controls font-controls"><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>`;
        sectionsContainer.appendChild(fontSection);

        // Section 3: Element Visibility
        const visibilitySection = document.createElement('div');
        visibilitySection.className = 'wtr-config-section';
        visibilitySection.innerHTML = `<label class="wtr-section-title">Element Visibility</label>
            <div class="wtr-config-controls checkbox-control"><input type="checkbox" id="wtr-navConstraint-toggle"><label for="wtr-navConstraint-toggle">${configs.navConstraint.label}</label></div>
            <div class="wtr-config-controls checkbox-control"><input type="checkbox" id="wtr-blockAddTerm-toggle"><label for="wtr-blockAddTerm-toggle">${configs.blockAddTerm.label}</label></div>
            <div class="wtr-control-group"><label class="wtr-subsection-title">Hide Toolbar Buttons</label><div class="wtr-button-hide-controls"></div></div>`;
        const buttonControlsContainer = visibilitySection.querySelector('.wtr-button-hide-controls');
        Object.entries(configs).filter(([n]) => n.startsWith('hide')).forEach(([name, config]) => {
            buttonControlsContainer.insertAdjacentHTML('beforeend', `<div class="wtr-config-controls checkbox-control icon-checkbox"><input type="checkbox" id="wtr-${name}-toggle"><label for="wtr-${name}-toggle">${config.iconHTML}<span>${config.label}</span></label></div>`);
        });
        sectionsContainer.appendChild(visibilitySection);

        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 (Serif)', recommendedSansSerif: 'Recommended (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);
        }
        fontSelect.value = Object.values(fontGroups).flat().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(); }
            else if (configName === 'blockAddTerm') { updateBlockAddTerm(); }
            else if (configName.startsWith('hide')) { updateButtonVisibilityStyles(); }
            else { 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 (['fontToggle', 'navConstraint', 'blockAddTerm'].includes(name) || name.startsWith('hide')) {
                const toggle = document.getElementById(`wtr-${name}-toggle`);
                if (toggle) toggle.addEventListener('change', () => updateSetting(name, toggle.checked));
            } else if (['reader', 'nav'].includes(name)) {
                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) => { ['wtr-font-select', 'wtr-font-refresh-btn', 'wtr-font-reset-btn'].forEach(id => { document.getElementById(id).disabled = !isEnabled; }); };

    const showConfigPanel = () => {
        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 (['navConstraint', 'blockAddTerm'].includes(name) || name.startsWith('hide')) { const toggle = document.getElementById(`wtr-${name}-toggle`); if (toggle) toggle.checked = loadValue(config.key, config.defaultState); }
            else if (['reader', 'nav'].includes(name)) { 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(var(--bs-black-rgb), 0.7); z-index: 9999; display: flex; justify-content: center; align-items: center; }
        #wtr-config-panel { background: var(--bs-component-bg); color: var(--bs-body-color); padding: 25px; border-radius: var(--bs-border-radius-lg); width: 90%; max-width: 550px; box-shadow: var(--bs-box-shadow-lg); font-family: var(--bs-font-sans-serif); display: flex; flex-direction: column; gap: 20px; max-height: 90vh; }
        #wtr-config-panel h2 { margin: 0 0 10px 0; text-align: center; font-weight: 500; flex-shrink: 0; }
        #wtr-config-panel #wtr-config-sections { overflow-y: auto; flex-grow: 1; display: flex; flex-direction: column; gap: 15px; padding-right: 10px; margin-right: -10px; }
        #wtr-config-panel .wtr-config-section { display: flex; flex-direction: column; gap: 15px; padding: 15px; border: 1px solid var(--bs-border-color); border-radius: var(--bs-border-radius); }
        #wtr-config-panel .wtr-control-group { display: flex; flex-direction: column; gap: 8px; }
        #wtr-config-panel .wtr-config-controls { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
        #wtr-config-panel .wtr-config-controls.font-controls { flex-wrap: nowrap; }
        #wtr-config-panel .wtr-config-controls.checkbox-control { justify-content: flex-start; cursor: pointer; padding: 5px 0; }
        #wtr-config-panel .wtr-config-controls.checkbox-control label { user-select: none; }
        #wtr-config-panel .wtr-config-controls.checkbox-control input { margin-right: 8px; }
        #wtr-config-panel input[type="number"], #wtr-config-panel select { flex-grow: 1; min-width: 100px; text-align: center; background: var(--bs-tertiary-bg); color: var(--bs-body-color); border: 1px solid var(--bs-border-color); border-radius: var(--bs-border-radius-sm); padding: 8px; }
        #wtr-config-panel select:disabled { background: var(--bs-secondary-bg); color: var(--bs-secondary-color); cursor: not-allowed; }
        #wtr-config-panel .wtr-button-group { display: flex; gap: 10px; flex-shrink: 0; margin-left: auto; }
        #wtr-config-panel .wtr-config-button { padding: 8px 12px; border: none; border-radius: var(--bs-border-radius-sm); cursor: pointer; background-color: var(--bs-primary); color: var(--bs-white); font-weight: bold; flex-shrink: 0; transition: filter 0.2s; }
        #wtr-config-panel .wtr-config-button:hover { filter: brightness(0.9); }
        #wtr-config-panel .wtr-config-button:disabled { background-color: var(--bs-secondary); cursor: not-allowed; }
        #wtr-config-panel .wtr-config-button.control { width: 40px; }
        #wtr-config-panel .wtr-config-button.reset { background-color: var(--bs-danger); }
        #wtr-config-panel #wtr-config-close-btn { background-color: var(--bs-secondary); align-self: center; width: 100px; flex-shrink: 0; }
        #wtr-config-panel .wtr-section-title { font-weight: 500; text-align: center; margin-bottom: 5px; display: block; font-size: 1.1em; }
        #wtr-config-panel .wtr-subsection-title { font-weight: 500; text-align: left; margin-top: 10px; display: block; border-top: 1px solid var(--bs-border-color); padding-top: 15px; }
        #wtr-config-panel .wtr-button-hide-controls { display: flex; flex-wrap: wrap; justify-content: flex-start; gap: 10px 20px; }
        #wtr-config-panel .icon-checkbox label { display: flex; align-items: center; gap: 8px; }
        #wtr-config-panel .icon-checkbox svg { width: 20px; height: 20px; stroke: currentColor; fill: none; }
        #wtr-config-panel .icon-checkbox svg:has(use[href*="text_fields"], use[href*="tts"], use[href*="list"]) { fill: currentColor; stroke: none; }
    `;

    // --- INITIALIZATION ---
    const init = async () => {
        log('Initializing script...');
        createConfigPanel();
        applyWidthStyle('reader', loadValue(configs.reader.key, configs.reader.defaultWidth));
        applyWidthStyle('nav', loadValue(configs.nav.key, configs.nav.defaultWidth));
        updateNavConstraint();
        updateBlockAddTerm();
        updateButtonVisibilityStyles();
        window.addEventListener('resize', updateNavConstraint);
        const fontGroups = await fetchFonts();
        const allAvailableFonts = Object.values(fontGroups).flat();
        let initialFont = loadValue(configs.font.key, configs.font.defaultFont);
        if (!allAvailableFonts.includes(initialFont)) { initialFont = configs.font.defaultFont; saveValue(configs.font.key, initialFont); }
        applyFontStyle(initialFont);
        populateFontDropdown(fontGroups);
        GM_registerMenuCommand('Configure Settings', showConfigPanel);
        GM_registerMenuCommand('Toggle Debug Logging', toggleDebugLogging);
        log('Initialization complete.');
    };

    init();
})();