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, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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);
    }

})();