// ==UserScript==
// @name WTR-Lab Reader & UI Enhancer
// @namespace http://tampermonkey.net/
// @version 2.9
// @description Adds a responsive configuration panel to adjust reader width, navigation width, and optionally override font style on wtr-lab.com.
// @author MasuRii
// @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 RECOMMENDED_FONTS = {
serif: ['Merriweather', 'Lora', 'Crimson Text', 'Libre Baskerville', 'Spectral', 'EB Garamond', 'Noto Serif'],
sansSerif: ['Roboto', 'Open Sans', 'Source Sans Pro']
};
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'
},
fontToggle: {
key: 'wtr_lab_font_style_enabled',
defaultState: false,
label: 'Enable Custom Font Style'
},
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 recommended = [...RECOMMENDED_FONTS.serif, ...RECOMMENDED_FONTS.sansSerif];
const otherFonts = data.map(f => f.family).filter(f => !recommended.includes(f)).sort();
resolve({
recommendedSerif: RECOMMENDED_FONTS.serif,
recommendedSansSerif: RECOMMENDED_FONTS.sansSerif,
other: otherFonts
});
} catch (error) {
resolve(getFallbackFonts());
}
},
onerror: () => resolve(getFallbackFonts())
});
});
};
const getFallbackFonts = () => {
log('Using fallback font list.');
return {
recommendedSerif: RECOMMENDED_FONTS.serif,
recommendedSansSerif: RECOMMENDED_FONTS.sansSerif,
other: ['Georgia', 'Times New Roman', 'Arial', 'Verdana'] // Basic system fonts
};
};
const removeFontStyle = () => {
log('Removing custom font style elements.');
const styleElement = document.getElementById('custom-font-styler');
if (styleElement) styleElement.remove();
const linkElement = document.getElementById('userscript-font-link');
if (linkElement) linkElement.remove();
};
const applyFontStyle = (fontFamily) => {
const isEnabled = loadValue(configs.fontToggle.key, configs.fontToggle.defaultState);
if (!isEnabled) {
removeFontStyle();
return;
}
log(`Applying font: ${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`;
let linkElement = document.getElementById('userscript-font-link');
if (!linkElement) {
linkElement = document.createElement('link');
linkElement.id = 'userscript-font-link';
linkElement.rel = 'stylesheet';
document.head.appendChild(linkElement);
}
linkElement.href = fontUrl;
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 === 'fontToggle') {
continue;
} else if (name === 'font') {
sectionHTML = `
<div class="wtr-config-section">
<div class="wtr-config-controls checkbox-control">
<input type="checkbox" id="wtr-fontToggle-toggle">
<label for="wtr-fontToggle-toggle">${configs.fontToggle.label}</label>
</div>
<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 = {
recommendedSerif: 'Recommended for Reading (Serif)',
recommendedSansSerif: 'Recommended for Reading (Sans-serif)',
other: 'All Other Fonts'
};
for (const groupKey in fontGroups) {
if (fontGroups[groupKey].length === 0) continue;
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 === 'fontToggle') {
updateFontControlsState(value);
applyFontStyle(loadValue(configs.font.key, configs.font.defaultFont));
} 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);
if (configName === 'nav') {
updateNavConstraint();
}
}
};
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 === 'fontToggle') {
const toggle = document.getElementById('wtr-fontToggle-toggle');
toggle.addEventListener('change', () => updateSetting(name, toggle.checked));
} else if (name === 'navConstraint') {
const toggle = document.getElementById('wtr-navConstraint-toggle');
toggle.addEventListener('change', () => updateSetting(name, toggle.checked));
} else { // reader, nav
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 updateFontControlsState = (isEnabled) => {
document.getElementById('wtr-font-select').disabled = !isEnabled;
document.getElementById('wtr-font-refresh-btn').disabled = !isEnabled;
document.getElementById('wtr-font-reset-btn').disabled = !isEnabled;
};
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 === 'fontToggle') {
const isEnabled = loadValue(config.key, config.defaultState);
document.getElementById('wtr-fontToggle-toggle').checked = isEnabled;
updateFontControlsState(isEnabled);
} else if (name === 'navConstraint') {
document.getElementById('wtr-navConstraint-toggle').checked = loadValue(config.key, config.defaultState);
} else if (configs[name].selector) { // reader, nav
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; }
.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-config-controls select:disabled { background: #3a3a3a; color: #888; cursor: not-allowed; }
.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();
window.addEventListener('resize', updateNavConstraint);
// Fetch fonts, validate saved font, and apply if enabled
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); // This will respect the enable/disable toggle
populateFontDropdown(fontGroups);
GM_registerMenuCommand('Configure Settings', showConfigPanel);
GM_registerMenuCommand('Toggle Debug Logging', toggleDebugLogging);
log('Initialization complete.');
};
init();
})();