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

})();