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.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

})();