// ==UserScript==
// @name WTR-Lab Reader & UI Enhancer
// @namespace http://tampermonkey.net/
// @version 2.7
// @description Adds a responsive configuration panel to adjust reader width, navigation width, and font style on wtr-lab.com.
// @author MasuRiii
// @license MIT
// @match https://wtr-lab.com/en/novel/*/*/chapter-*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// @connect gwfh.mranftl.com
// @icon https://www.google.com/s2/favicons?sz=64&domain=wtr-lab.com
// ==/UserScript==
(function() {
'use strict';
// --- CONFIGURATION ---
const DEBUG_KEY = 'wtr_lab_enhancer_debug';
const STEP_WIDTH = 50;
const MIN_WIDTH = 300;
const FONTS_API_URL = 'https://gwfh.mranftl.com/api/fonts';
const configs = {
reader: {
key: 'wtr_lab_reader_width',
selector: '.fix-size.card',
defaultWidth: 760,
label: 'Reader Content Width'
},
nav: {
key: 'wtr_lab_nav_width',
selector: 'nav.bottom-reader-nav .fix-size',
defaultWidth: 760,
label: 'Bottom Navigator Width'
},
navConstraint: {
key: 'wtr_lab_nav_constraint',
selector: 'nav.bottom-reader-nav',
defaultState: false,
label: 'Constrain Navigator Background'
},
font: {
key: 'wtr_lab_font_family',
selector: '.chapter-body',
defaultFont: 'Merriweather',
label: 'Font Style'
}
};
// --- DEBUG LOGGER ---
let isDebugEnabled = GM_getValue(DEBUG_KEY, false);
const log = (...args) => {
if (isDebugEnabled) console.log('[WTR-Lab Enhancer]', ...args);
};
const toggleDebugLogging = () => {
isDebugEnabled = !isDebugEnabled;
GM_setValue(DEBUG_KEY, isDebugEnabled);
alert(`Debug logging is now ${isDebugEnabled ? 'ENABLED' : 'DISABLED'}.`);
};
// --- CORE LOGIC ---
const applyWidthStyle = (configName, width) => {
const styleId = `custom-width-styler-${configName}`;
let styleElement = document.getElementById(styleId);
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = styleId;
document.head.appendChild(styleElement);
}
styleElement.textContent = `${configs[configName].selector} { max-width: ${width}px !important; }`;
};
const updateNavConstraint = () => {
const isConstrained = loadValue(configs.navConstraint.key, configs.navConstraint.defaultState);
const styleId = 'custom-nav-constraint-styler';
let styleElement = document.getElementById(styleId);
if (isConstrained) {
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = styleId;
document.head.appendChild(styleElement);
}
const navContentWidth = loadValue(configs.nav.key, configs.nav.defaultWidth);
const marginValue = Math.max(0, (window.innerWidth - navContentWidth) / 2);
styleElement.textContent = `${configs.navConstraint.selector} { margin-left: ${marginValue}px !important; margin-right: ${marginValue}px !important; }`;
log(`Navigator constrained with ${marginValue}px margins.`);
} else {
if (styleElement) {
styleElement.remove();
log('Navigator constraint removed.');
}
}
};
const fetchFonts = () => {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'GET',
url: FONTS_API_URL,
onload: (response) => {
try {
if (response.status !== 200) throw new Error(`HTTP error! Status: ${response.status}`);
const data = JSON.parse(response.responseText);
const topSerifFonts = data.filter(f => f.category === 'serif').slice(0, 10).map(f => f.family);
const otherFonts = data.filter(f => !topSerifFonts.includes(f.family)).map(f => f.family).sort();
resolve({ serif: topSerifFonts, other: otherFonts });
} catch (error) {
resolve(getFallbackFonts());
}
},
onerror: () => resolve(getFallbackFonts())
});
});
};
const getFallbackFonts = () => {
log('Using fallback font list.');
return {
serif: ['Merriweather', 'Lora', 'Source Serif Pro', 'EB Garamond', 'Georgia'],
other: ['Roboto', 'Open Sans', 'Lato']
};
};
const applyFontStyle = (fontFamily) => {
const primaryFont = fontFamily.split(',')[0].trim();
const styleId = 'custom-font-styler';
let styleElement = document.getElementById(styleId);
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = styleId;
document.head.appendChild(styleElement);
}
const fontUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(primaryFont)}&display=swap`;
if (!document.querySelector(`link[href="${fontUrl}"]`)) {
const linkElement = document.createElement('link');
linkElement.rel = 'stylesheet';
linkElement.href = fontUrl;
document.head.appendChild(linkElement);
}
styleElement.textContent = `${configs.font.selector} { font-family: "${primaryFont}", serif, sans-serif !important; }`;
};
const saveValue = (key, value) => GM_setValue(key, value);
const loadValue = (key, defaultValue) => GM_getValue(key, defaultValue);
// --- UI CONFIGURATION PANEL ---
const createConfigPanel = () => {
const panelHTML = `<div id="wtr-config-overlay" style="display: none;"><div id="wtr-config-panel"><h2>WTR-Lab Enhancer Settings</h2><div id="wtr-config-sections"></div><button id="wtr-config-close-btn" class="wtr-config-button">Close</button></div></div>`;
document.body.insertAdjacentHTML('beforeend', panelHTML);
GM_addStyle(getPanelCSS());
const sectionsContainer = document.getElementById('wtr-config-sections');
for (const [name, config] of Object.entries(configs)) {
let sectionHTML;
if (name === 'font') {
sectionHTML = `<div class="wtr-config-section"><label for="wtr-font-select">${config.label}</label><div class="wtr-config-controls font-controls"><select id="wtr-font-select"></select><div class="wtr-button-group"><button id="wtr-font-refresh-btn" class="wtr-config-button">Refresh</button><button id="wtr-font-reset-btn" class="wtr-config-button reset">Reset</button></div></div></div>`;
} else if (name === 'navConstraint') {
sectionHTML = `<div class="wtr-config-section"><div class="wtr-config-controls checkbox-control"><input type="checkbox" id="wtr-navConstraint-toggle"><label for="wtr-navConstraint-toggle">${config.label}</label></div></div>`;
} else { // reader, nav
sectionHTML = `<div class="wtr-config-section"><label for="wtr-${name}-width-input">${config.label} (px)</label><div class="wtr-config-controls"><button id="wtr-${name}-decrease-btn" class="wtr-config-button control">-</button><input type="number" id="wtr-${name}-width-input" min="${MIN_WIDTH}" step="10"><button id="wtr-${name}-increase-btn" class="wtr-config-button control">+</button><button id="wtr-${name}-reset-btn" class="wtr-config-button reset">Reset</button></div></div>`;
}
sectionsContainer.insertAdjacentHTML('beforeend', sectionHTML);
}
populateFontDropdown(getFallbackFonts());
attachPanelEventListeners();
};
const populateFontDropdown = async (initialFontGroups = null) => {
const fontSelect = document.getElementById('wtr-font-select');
if (!fontSelect) return;
const currentFont = loadValue(configs.font.key, configs.font.defaultFont);
fontSelect.innerHTML = '';
const fontGroups = initialFontGroups || await fetchFonts();
const groupLabels = { serif: 'Top 10 for Reading (Serif)', other: 'All Other Fonts' };
for (const groupKey in fontGroups) {
const optgroup = document.createElement('optgroup');
optgroup.label = groupLabels[groupKey] || 'Fonts';
fontGroups[groupKey].forEach(font => {
const option = document.createElement('option');
option.value = font;
option.textContent = font;
optgroup.appendChild(option);
});
fontSelect.appendChild(optgroup);
}
const allFonts = Object.values(fontGroups).flat();
fontSelect.value = allFonts.includes(currentFont) ? currentFont : configs.font.defaultFont;
};
const attachPanelEventListeners = () => {
document.getElementById('wtr-config-overlay').addEventListener('click', (e) => {
if (e.target.id === 'wtr-config-overlay') hideConfigPanel();
});
document.getElementById('wtr-config-close-btn').addEventListener('click', hideConfigPanel);
const updateSetting = (configName, value) => {
const config = configs[configName];
saveValue(config.key, value);
if (configName === 'font') {
applyFontStyle(value);
document.getElementById('wtr-font-select').value = value;
} else if (configName === 'navConstraint') {
updateNavConstraint();
document.getElementById('wtr-navConstraint-toggle').checked = value;
} else { // width controls
const validatedWidth = Math.max(MIN_WIDTH, parseInt(value, 10));
if (isNaN(validatedWidth)) return;
applyWidthStyle(configName, validatedWidth);
document.getElementById(`wtr-${configName}-width-input`).value = validatedWidth;
saveValue(config.key, validatedWidth); // save validated value
if (configName === 'nav') {
updateNavConstraint(); // Recalculate margins if nav width changes
}
}
};
for (const [name, config] of Object.entries(configs)) {
if (name === 'font') {
const select = document.getElementById('wtr-font-select');
select.addEventListener('change', () => updateSetting(name, select.value));
document.getElementById('wtr-font-reset-btn').addEventListener('click', () => updateSetting(name, config.defaultFont));
const refreshBtn = document.getElementById('wtr-font-refresh-btn');
refreshBtn.addEventListener('click', async () => {
refreshBtn.textContent = 'Fetching...';
refreshBtn.disabled = true;
await populateFontDropdown();
refreshBtn.textContent = 'Refresh';
refreshBtn.disabled = false;
});
} else if (name === 'navConstraint') {
const toggle = document.getElementById('wtr-navConstraint-toggle');
toggle.addEventListener('change', () => updateSetting(name, toggle.checked));
} else {
const input = document.getElementById(`wtr-${name}-width-input`);
document.getElementById(`wtr-${name}-increase-btn`).addEventListener('click', () => updateSetting(name, parseInt(input.value, 10) + STEP_WIDTH));
document.getElementById(`wtr-${name}-decrease-btn`).addEventListener('click', () => updateSetting(name, parseInt(input.value, 10) - STEP_WIDTH));
document.getElementById(`wtr-${name}-reset-btn`).addEventListener('click', () => updateSetting(name, config.defaultWidth));
input.addEventListener('change', () => updateSetting(name, input.value));
}
}
};
const showConfigPanel = () => {
log('Showing configuration panel.');
for (const [name, config] of Object.entries(configs)) {
if (name === 'font') {
document.getElementById('wtr-font-select').value = loadValue(config.key, config.defaultFont);
} else if (name === 'navConstraint') {
document.getElementById('wtr-navConstraint-toggle').checked = loadValue(config.key, config.defaultState);
} else {
document.getElementById(`wtr-${name}-width-input`).value = loadValue(config.key, config.defaultWidth);
}
}
document.getElementById('wtr-config-overlay').style.display = 'flex';
};
const hideConfigPanel = () => document.getElementById('wtr-config-overlay').style.display = 'none';
const getPanelCSS = () => `
#wtr-config-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); z-index: 9999; display: flex; justify-content: center; align-items: center; }
#wtr-config-panel { background: #2c2c2c; color: #f1f1f1; padding: 25px; border-radius: 8px; width: 90%; max-width: 500px; box-shadow: 0 5px 15px rgba(0,0,0,0.5); font-family: sans-serif; display: flex; flex-direction: column; gap: 20px; }
#wtr-config-panel h2 { margin: 0 0 10px 0; text-align: center; }
.wtr-config-section { display: flex; flex-direction: column; gap: 8px; padding: 15px; border: 1px solid #444; border-radius: 5px; }
.wtr-config-controls { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.wtr-config-controls.font-controls { flex-wrap: nowrap; } /* <-- FIX: Prevents wrapping for font controls */
.wtr-config-controls.checkbox-control { flex-direction: row; justify-content: center; cursor: pointer; }
.wtr-config-controls.checkbox-control label { user-select: none; }
.wtr-config-controls.checkbox-control input { margin-right: 8px; }
.wtr-config-controls input[type="number"], .wtr-config-controls select { flex-grow: 1; min-width: 100px; text-align: center; background: #444; color: #fff; border: 1px solid #666; border-radius: 4px; padding: 8px; }
.wtr-button-group { display: flex; gap: 10px; flex-shrink: 0; margin-left: auto; }
.wtr-config-button { padding: 8px 12px; border: none; border-radius: 4px; cursor: pointer; background-color: #007bff; color: white; font-weight: bold; flex-shrink: 0; transition: background-color 0.2s; }
.wtr-config-button:disabled { background-color: #555; cursor: not-allowed; }
.wtr-config-button.control { width: 40px; }
.wtr-config-button.reset { background-color: #dc3545; }
#wtr-config-close-btn { background-color: #6c757d; align-self: center; width: 100px; }
`;
// --- INITIALIZATION ---
const init = async () => {
log('Initializing script...');
createConfigPanel();
// Apply initial styles
applyWidthStyle('reader', loadValue(configs.reader.key, configs.reader.defaultWidth));
applyWidthStyle('nav', loadValue(configs.nav.key, configs.nav.defaultWidth));
updateNavConstraint(); // Initial calculation for nav margins
// Add resize listener for responsive nav margins
window.addEventListener('resize', updateNavConstraint);
// Fetch fonts, validate saved font, and apply
const fontGroups = await fetchFonts();
const allAvailableFonts = Object.values(fontGroups).flat();
let initialFont = loadValue(configs.font.key, configs.font.defaultFont);
if (!allAvailableFonts.includes(initialFont)) {
log(`Saved font "${initialFont}" not found. Reverting to default.`);
initialFont = configs.font.defaultFont;
saveValue(configs.font.key, initialFont);
}
applyFontStyle(initialFont);
populateFontDropdown(fontGroups);
GM_registerMenuCommand('Configure Settings', showConfigPanel);
GM_registerMenuCommand('Toggle Debug Logging', toggleDebugLogging);
log('Initialization complete.');
};
init();
})();