WTR-Lab Reader Content Width

Control the max-width of the main content area on wtr-lab.com (chapter reader, library, and more). Opens a small panel via the Tampermonkey menu or the floating button.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         WTR-Lab Reader Content Width
// @description  Control the max-width of the main content area on wtr-lab.com (chapter reader, library, and more). Opens a small panel via the Tampermonkey menu or the floating button.
// @version      1.2.0
// @author       Extracted from WTR-Lab Reader & UI Enhancer by MasuRii
// @namespace    http://tampermonkey.net/
// @match        https://wtr-lab.com/en/novel/*/*/chapter-*
// @include      https://wtr-lab.com/en/library*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // ── Constants ────────────────────────────────────────────────────────────
    const STORAGE_KEY   = 'wtr_lab_reader_width';
    // Covers:
    //   • Chapter pages  → .fix-size[data-slot="card"]  /  .fix-size.chapter-theme
    //   • Library page   → .fix-size.fix-edge (the shared content-width wrapper)
    // All three match the same reader/content container depending on the page type.
    // Nav inner elements also carry fix-size but not these combinations, so they
    // are unaffected.
    const SELECTOR      = '.fix-size[data-slot="card"], .fix-size.chapter-theme, .fix-size.fix-edge';
    const DEFAULT_WIDTH = 760;
    const MIN_WIDTH     = 300;
    const STEP          = 50;

    // ── Styles ───────────────────────────────────────────────────────────────
    GM_addStyle(`
        /* ---- floating trigger button ---- */
        #wtrw-btn {
            position: fixed;
            bottom: 18px;
            right: 18px;
            z-index: 99998;
            background: #0d6efd;
            color: #fff;
            border: none;
            border-radius: 50%;
            width: 42px;
            height: 42px;
            font-size: 18px;
            cursor: pointer;
            box-shadow: 0 2px 8px rgba(0,0,0,.35);
            display: flex;
            align-items: center;
            justify-content: center;
            transition: background .15s;
            line-height: 1;
        }
        #wtrw-btn:hover { background: #0b5ed7; }

        /* ---- overlay ---- */
        #wtrw-overlay {
            display: none;
            position: fixed;
            inset: 0;
            background: rgba(0,0,0,.65);
            backdrop-filter: blur(4px);
            z-index: 99999;
            align-items: center;
            justify-content: center;
        }
        #wtrw-overlay.open { display: flex; }

        /* ---- panel ---- */
        #wtrw-panel {
            background: var(--bs-component-bg, #fff);
            color: var(--bs-body-color, #212529);
            border: 1px solid var(--bs-border-color, #dee2e6);
            border-radius: 10px;
            padding: 28px 24px 20px;
            width: 320px;
            max-width: 94vw;
            box-shadow: 0 20px 40px rgba(0,0,0,.18);
            display: flex;
            flex-direction: column;
            gap: 18px;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
        }

        #wtrw-panel h2 {
            margin: 0;
            font-size: 1.05rem;
            font-weight: 600;
            text-align: center;
            color: inherit;
        }

        /* ---- control row ---- */
        #wtrw-controls {
            display: flex;
            align-items: center;
            gap: 8px;
        }

        #wtrw-controls label {
            display: block;
            font-size: .8rem;
            margin-bottom: 6px;
            font-weight: 500;
            color: inherit;
        }

        .wtrw-label-row {
            display: flex;
            flex-direction: column;
            width: 100%;
        }

        #wtrw-input {
            flex: 1;
            min-width: 0;
            text-align: center;
            padding: 8px 6px;
            border: 1px solid var(--bs-border-color, #dee2e6);
            border-radius: 6px;
            background: var(--bs-tertiary-bg, #f8f9fa);
            color: inherit;
            font-size: .9rem;
            height: 40px;
        }

        .wtrw-btn {
            height: 40px;
            min-width: 40px;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-weight: 600;
            font-size: .9rem;
            background: #0d6efd;
            color: #fff;
            transition: background .15s;
            display: flex;
            align-items: center;
            justify-content: center;
            flex-shrink: 0;
        }
        .wtrw-btn:hover  { background: #0b5ed7; }
        .wtrw-btn.reset  { background: #dc3545; }
        .wtrw-btn.reset:hover { background: #bb2d3b; }
        .wtrw-btn.close  { background: #6c757d; width: 100%; }
        .wtrw-btn.close:hover { background: #5a6268; }

        #wtrw-hint {
            font-size: .75rem;
            color: var(--bs-secondary-color, #6c757d);
            text-align: center;
        }
    `);

    // ── State ─────────────────────────────────────────────────────────────────
    let currentWidth = Math.max(MIN_WIDTH, parseInt(GM_getValue(STORAGE_KEY, DEFAULT_WIDTH), 10));

    // ── Apply width to page ───────────────────────────────────────────────────
    function applyWidth(width) {
        currentWidth = Math.max(MIN_WIDTH, parseInt(width, 10));
        if (isNaN(currentWidth)) currentWidth = DEFAULT_WIDTH;

        let el = document.getElementById('wtrw-style');
        if (!el) {
            el = document.createElement('style');
            el.id = 'wtrw-style';
            document.head.appendChild(el);
        }
        el.textContent = `${SELECTOR} { max-width: ${currentWidth}px !important; }`;
        GM_setValue(STORAGE_KEY, currentWidth);
    }

    // ── Build UI ──────────────────────────────────────────────────────────────
    function buildUI() {
        // Floating button
        const btn = document.createElement('button');
        btn.id = 'wtrw-btn';
        btn.title = 'Reader Width';
        btn.textContent = '↔';
        document.body.appendChild(btn);

        // Overlay
        const overlay = document.createElement('div');
        overlay.id = 'wtrw-overlay';
        overlay.innerHTML = `
            <div id="wtrw-panel">
                <h2>Reader Content Width</h2>
                <div class="wtrw-label-row">
                    <label for="wtrw-input">Width (px)</label>
                    <div id="wtrw-controls">
                        <button class="wtrw-btn" id="wtrw-dec">−</button>
                        <input type="number" id="wtrw-input" min="${MIN_WIDTH}" step="${STEP}" value="${currentWidth}">
                        <button class="wtrw-btn" id="wtrw-inc">+</button>
                        <button class="wtrw-btn reset" id="wtrw-reset">Reset</button>
                    </div>
                </div>
                <div id="wtrw-hint">Default: ${DEFAULT_WIDTH} px · Min: ${MIN_WIDTH} px · Step: ${STEP} px</div>
                <button class="wtrw-btn close" id="wtrw-close">Close</button>
            </div>
        `;
        document.body.appendChild(overlay);

        // ── Event wiring ──────────────────────────────────────────────────────
        const input = overlay.querySelector('#wtrw-input');

        function setAndSync(val) {
            const v = Math.max(MIN_WIDTH, parseInt(val, 10) || DEFAULT_WIDTH);
            input.value = v;
            applyWidth(v);
        }

        btn.addEventListener('click', openPanel);
        overlay.querySelector('#wtrw-close').addEventListener('click', closePanel);
        overlay.addEventListener('click', e => { if (e.target === overlay) closePanel(); });

        overlay.querySelector('#wtrw-inc').addEventListener('click', () =>
            setAndSync(currentWidth + STEP));

        overlay.querySelector('#wtrw-dec').addEventListener('click', () =>
            setAndSync(currentWidth - STEP));

        overlay.querySelector('#wtrw-reset').addEventListener('click', () =>
            setAndSync(DEFAULT_WIDTH));

        input.addEventListener('change', () => setAndSync(input.value));

        // Keyboard: Escape closes
        document.addEventListener('keydown', e => {
            if (e.key === 'Escape' && overlay.classList.contains('open')) closePanel();
        });
    }

    function openPanel() {
        const input = document.getElementById('wtrw-input');
        if (input) input.value = currentWidth;
        document.getElementById('wtrw-overlay').classList.add('open');
    }

    function closePanel() {
        document.getElementById('wtrw-overlay').classList.remove('open');
    }

    // ── Re-apply on Next.js client-side navigation ───────────────────────────
    // Next.js swaps page content without a full reload; a MutationObserver on
    // the body re-injects the style whenever the DOM changes significantly.
    function watchForNavigation() {
        let lastHref = location.href;
        const observer = new MutationObserver(() => {
            if (location.href !== lastHref) {
                lastHref = location.href;
                // Small delay so Next.js finishes rendering the new page
                setTimeout(() => applyWidth(currentWidth), 300);
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // ── Init ──────────────────────────────────────────────────────────────────
    function init() {
        applyWidth(currentWidth);
        buildUI();
        GM_registerMenuCommand('Reader Content Width — Open Settings', openPanel);
        watchForNavigation();
    }

    // Wait for body (usually already present on chapter/library pages)
    if (document.body) {
        init();
    } else {
        document.addEventListener('DOMContentLoaded', init);
    }

})();