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