Transform any webnovel site into a premium reading experience. Distraction-free, with translation support, auto-save progress, customizable themes (Sepia/Dark), and adjustable typography. Perfect for binge-readers.
// ==UserScript==
// @name PureReader (ZenMode)
// @namespace https://gitlab.com/wandersons13/purereader
// @version 0.5.1
// @description Transform any webnovel site into a premium reading experience. Distraction-free, with translation support, auto-save progress, customizable themes (Sepia/Dark), and adjustable typography. Perfect for binge-readers.
// @author wandersons13
// @match *://*/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=readnovelfull.com
// @license GNU
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
let settings = JSON.parse(localStorage.getItem('gm_reader_settings')) || {
theme: 'dark',
fontSize: 22,
lineHeight: 1.8,
maxWidth: 800,
fontIndex: 0,
isActive: false,
autoHide: false
};
const saveSettings = () => localStorage.setItem('gm_reader_settings', JSON.stringify(settings));
const globalStyle = document.createElement('style');
globalStyle.innerHTML = `
#gm-trigger-bar {
position: fixed !important;
bottom: 20px !important;
left: 50% !important;
transform: translateX(-50%) !important;
width: 320px !important;
height: 50px !important;
z-index: 2147483647 !important;
background: #2b2b2b !important;
border: 1px solid #444 !important;
border-radius: 8px !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
padding: 0 15px !important;
color: #e0e0e0 !important;
font-family: sans-serif !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.4) !important;
box-sizing: border-box !important;
transition: opacity 0.3s ease !important;
}
.gm-bar-btn {
border: none !important;
color: #fff !important;
border-radius: 4px !important;
padding: 6px 12px !important;
cursor: pointer !important;
font-size: 13px !important;
font-weight: 600 !important;
}
#gm-btn-act { background: #4a9eff !important; }
#gm-btn-no { background: #555 !important; }
@media screen and (max-width: 768px) {
.sidebar-btn svg {
width: 20px !important;
height: 20px !important;
}
.sidebar-btn {
width: 48px !important;
height: 48px !important;
margin: 2px 0 !important;
}
#gm-reader-sidebar.auto-hide-active {
opacity: 0.05 !important;
}
#gm-reader-sidebar.auto-hide-active:hover {
opacity: 1.0 !important;
}
}
`;
document.head.appendChild(globalStyle);
const isReadingModeEnabled = () => {
const globalBlacklist = [
'youtube.com', 'youtu.be', 'google.com', 'bing.com', 'facebook.com',
'instagram.com', 'twitter.com', 'x.com', 'linkedin.com', 'tiktok.com',
'wikipedia.org', 'reddit.com', 'baidu.com', 'qq.com', 'weibo.com',
'zhihu.com', 'bilibili.com', 'naver.com', 'daum.net', 'vk.com', 'yandex.ru'
];
const url = window.location.href.toLowerCase();
const domain = window.location.hostname.toLowerCase();
if (globalBlacklist.some(d => domain.includes(d))) return false;
const keywords = [
'chapter', 'capitulo', 'capítulo', 'capitolo', 'chapitre', 'kapitel', 'part', 'volume', 'episodio', 'episódio',
'章', '章节', '卷', '话', '回', '話', '第', '節', '화', '장', '권', '편',
'глава', 'часть', 'том', 'فصل', 'פרק', 'story', 'arc'
];
const hasUrlPattern = /\/(chapter|capitulo|capitulo|c|ep|v)\d+/i.test(url);
const title = document.title.toLowerCase();
const hasKeyword = keywords.some(word => title.includes(word));
const isHome = window.location.pathname === '/' || window.location.pathname.length < 5;
return (hasKeyword || hasUrlPattern) && !isHome;
};
const showTriggerBar = () => {
if (document.getElementById('gm-trigger-bar')) return;
const bar = document.createElement('div');
bar.id = 'gm-trigger-bar';
bar.innerHTML = `
<span style="font-size: 13px; font-weight: bold;">${settings.isActive ? 'Disable' : 'Enable'} PureReader?</span>
<div style="display: flex; gap: 8px;">
<button id="gm-btn-act" class="gm-bar-btn">Yes</button>
<button id="gm-btn-no" class="gm-bar-btn">No</button>
</div>
`;
document.body.appendChild(bar);
document.getElementById('gm-btn-act').onclick = () => {
settings.isActive = !settings.isActive;
saveSettings();
location.reload();
};
document.getElementById('gm-btn-no').onclick = () => bar.remove();
setTimeout(() => {
if (bar && bar.parentNode) bar.remove();
}, 5000);
};
if (isReadingModeEnabled()) {
setTimeout(showTriggerBar, 2000);
}
const fonts = [{
name: 'Inter',
type: 'sans-serif'
},
{
name: 'Merriweather',
type: 'serif'
},
{
name: 'Special Elite',
type: 'cursive'
}
];
const scrollKey = 'gm_scroll_' + btoa(window.location.href.split('#')[0]).substring(0, 50);
if (settings.isActive) {
const bg = settings.theme === 'dark' ? '#20282e' : '#f4ecd8';
const shield = document.createElement('style');
shield.id = 'gm-protection-shield';
shield.innerHTML = `html{background-color:${bg}!important;} body{opacity:0!important;overflow:hidden!important;} #gm-reader-overlay{opacity:1!important;display:block!important;}`;
document.documentElement.appendChild(shield);
}
const initReader = () => {
if (document.getElementById('gm-reader-overlay')) return;
const fontLink = document.createElement('link');
fontLink.rel = 'stylesheet';
fontLink.href = 'https://fonts.googleapis.com/css2?family=Special+Elite&family=Inter:wght@400;700&family=Merriweather:wght@300;400;700&display=swap';
document.head.appendChild(fontLink);
const style = document.createElement('style');
style.innerHTML = `
#gm-reader-overlay h1, #gm-reader-overlay .chapter-title, #gm-reader-overlay .post-title, #gm-reader-overlay .entry-title { display: none !important; }
#gm-reader-overlay.theme-sepia { --bg-color: #f4ecd8; --text-color: #2c2c2c; --icon-color: #333; --sidebar-bg: rgba(0,0,0,0.08); --sep-color: rgba(0,0,0,0.15); --accent: #d4a373; }
#gm-reader-overlay.theme-dark { --bg-color: #20282e; --text-color: #999; --icon-color: #fff; --sidebar-bg: rgba(255,255,255,0.12); --sep-color: rgba(255,255,255,0.25); --accent: #4a9eff; }
#gm-reader-overlay:focus { outline: none; }
#gm-reader-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 2147483646; overflow-y: auto; overflow-x: hidden !important; display: none; background-color: var(--bg-color) !important; scroll-behavior: smooth; }
.gm-reader-content-box { margin: 0 auto; padding: 80px 25px; min-height: 100vh; box-sizing: border-box; width: 100%; }
.gm-reader-content-box * { color: var(--text-color) !important; background-color: transparent !important; max-width: 100% !important; box-sizing: border-box !important; white-space: normal !important; overflow-wrap: anywhere !important; }
#gm-reader-sidebar {
position: fixed; right: 30px; top: 50%; transform: translateY(-50%); z-index: 2147483647;
width: 54px; padding: 12px 0; display: flex; flex-direction: column; align-items: center;
background: var(--sidebar-bg); backdrop-filter: blur(20px); border: 1px solid var(--sep-color);
border-radius: 27px; box-shadow: 0 4px 15px rgba(0,0,0,0.2);
transition: opacity 0.3s ease;
opacity: 1;
}
#gm-reader-sidebar.auto-hide-active { opacity: 0.15; }
#gm-reader-sidebar.auto-hide-active:hover { opacity: 1; }
.sidebar-btn { background: none; border: none; width: 40px; height: 40px; margin: 2px 0; cursor: pointer; display: flex; align-items: center; justify-content: center; border-radius: 50%; fill: var(--icon-color); transition: all 0.2s; }
.sidebar-btn:hover { background: rgba(128,128,128,0.2); }
.sidebar-btn.active-mode { fill: var(--accent); }
.sidebar-sep { width: 30px; height: 2px; background: var(--sep-color); margin: 8px 0; border-radius: 1px; }
`;
document.head.appendChild(style);
const overlay = document.createElement('div');
overlay.id = 'gm-reader-overlay';
overlay.tabIndex = 0;
const contentBox = document.createElement('div');
contentBox.className = 'gm-reader-content-box';
const sidebar = document.createElement('div');
sidebar.id = 'gm-reader-sidebar';
sidebar.innerHTML = `
<button class="sidebar-btn" id="btn-theme" title="Trocar Tema"><svg viewBox="0 0 24 24" width="20"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zm0-2V4a8 8 0 110 16z"/></svg></button>
<button class="sidebar-btn" id="btn-font" title="Trocar Fonte"><svg viewBox="0 0 24 24" width="20"><path d="M9.93 13.5h4.14L12 7.98zM20 2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-4.05 16.5l-1.14-3H9.17l-1.12 3H5.96l5.11-13h1.86l5.11 13h-2.09z"/></svg></button>
<div class="sidebar-sep"></div>
<button class="sidebar-btn" id="btn-f-plus" title="Aumentar Fonte"><svg viewBox="0 0 24 24" width="20"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg></button>
<button class="sidebar-btn" id="btn-f-minus" title="Diminuir Fonte"><svg viewBox="0 0 24 24" width="20"><path d="M19 13H5v-2h14v2z"/></svg></button>
<div class="sidebar-sep"></div>
<button class="sidebar-btn" id="btn-lh-plus" title="Aumentar Espaçamento"><svg viewBox="0 0 24 24" width="20"><path d="M7 21V3h2v18H7zm7-18l-4 4h3v10h-3l4 4 4-4h-3V7h3l-4-4z"/></svg></button>
<button class="sidebar-btn" id="btn-lh-minus" title="Diminuir Espaçamento"><svg viewBox="0 0 24 24" width="20"><path d="M7 21V3h2v18H7zm11-14l-4-4-4 4h3v10h-3l4 4 4-4h-3V7h3z"/></svg></button>
<div class="sidebar-sep"></div>
<button class="sidebar-btn" id="btn-w-plus" title="Aumentar Largura"><svg viewBox="0 0 24 24" width="20"><path d="M15 4h5v5h-2V6h-3V4zM4 15h2v3h3v2H4v-5zm14 3h-3v2h5v-5h-2v3zM6 6h3V4H4v5h2V6z"/></svg></button>
<button class="sidebar-btn" id="btn-w-minus" title="Diminuir Largura"><svg viewBox="0 0 24 24" width="20"><path d="M18 16h3v2h-5v-5h2v3zM8 8h-3V6h5v5H8V8zM16 8V5h2v5h-5V8h3zM8 16v3H6v-5h5v2H8z"/></svg></button>
<div class="sidebar-sep"></div>
<button class="sidebar-btn" id="btn-autohide" title="Ativar/Desativar Auto-Hide"><svg viewBox="0 0 24 24" width="20"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg></button>
`;
overlay.append(contentBox, sidebar);
document.body.appendChild(overlay);
const applyStyles = () => {
const f = fonts[settings.fontIndex];
contentBox.style.maxWidth = settings.maxWidth + 'px';
contentBox.style.setProperty('font-family', `'${f.name}', ${f.type}`, 'important');
contentBox.style.setProperty('font-size', settings.fontSize + 'px', 'important');
contentBox.style.setProperty('line-height', settings.lineHeight, 'important');
contentBox.querySelectorAll('*').forEach(el => {
el.style.setProperty('font-family', `'${f.name}', ${f.type}`, 'important');
el.style.setProperty('font-size', settings.fontSize + 'px', 'important');
el.style.setProperty('line-height', settings.lineHeight, 'important');
if (el.tagName === 'P') el.style.setProperty('margin-bottom', '1.6em', 'important');
});
const btnHide = document.getElementById('btn-autohide');
if (settings.autoHide) {
sidebar.classList.add('auto-hide-active');
btnHide.classList.add('active-mode');
} else {
sidebar.classList.remove('auto-hide-active');
btnHide.classList.remove('active-mode');
}
saveSettings();
};
const updateUI = () => {
overlay.className = 'theme-' + settings.theme;
applyStyles();
};
const tryCapture = () => {
let best = null,
max = 0;
document.querySelectorAll('article, main, .content, .post-body, #content, section, div:not(#gm-reader-overlay)').forEach(el => {
const txt = el.innerText.trim();
if (txt.length > max) {
max = txt.length;
best = el;
}
});
if (best && max > 200) {
const clone = best.cloneNode(true);
clone.querySelectorAll('button, input, nav, footer, header, aside, form, svg, ul, ol, script, style, img, figure, .comments, #comments').forEach(el => el.remove());
contentBox.innerHTML = clone.innerHTML;
overlay.style.display = 'block';
document.body.style.overflow = 'hidden';
updateUI();
setTimeout(() => {
overlay.focus();
const savedPos = localStorage.getItem(scrollKey);
if (savedPos) overlay.scrollTop = parseInt(savedPos);
else overlay.scrollTop = 0;
}, 100);
const shield = document.getElementById('gm-protection-shield');
if (shield) shield.remove();
document.body.style.opacity = '1';
return true;
}
return false;
};
overlay.addEventListener('scroll', () => {
if (overlay.scrollTop > 100) localStorage.setItem(scrollKey, overlay.scrollTop);
});
window.addEventListener('keydown', (e) => {
if (e.altKey && e.key.toLowerCase() === 'r') {
e.preventDefault();
settings.isActive = !settings.isActive;
if (settings.isActive) tryCapture();
else {
overlay.style.display = 'none';
document.body.style.overflow = '';
document.body.style.opacity = '1';
}
saveSettings();
}
});
const obs = new MutationObserver(() => {
if (settings.isActive && tryCapture()) obs.disconnect();
});
obs.observe(document.body, {
childList: true,
subtree: true
});
if (settings.isActive) tryCapture();
document.getElementById('btn-theme').onclick = () => {
settings.theme = settings.theme === 'sepia' ? 'dark' : 'sepia';
updateUI();
};
document.getElementById('btn-font').onclick = () => {
settings.fontIndex = (settings.fontIndex + 1) % fonts.length;
updateUI();
};
document.getElementById('btn-autohide').onclick = () => {
settings.autoHide = !settings.autoHide;
updateUI();
};
document.getElementById('btn-f-plus').onclick = () => {
settings.fontSize += 2;
updateUI();
};
document.getElementById('btn-f-minus').onclick = () => {
settings.fontSize = Math.max(12, settings.fontSize - 2);
updateUI();
};
document.getElementById('btn-lh-plus').onclick = () => {
settings.lineHeight = parseFloat((settings.lineHeight + 0.1).toFixed(1));
updateUI();
};
document.getElementById('btn-lh-minus').onclick = () => {
settings.lineHeight = Math.max(1.0, parseFloat((settings.lineHeight - 0.1).toFixed(1)));
updateUI();
};
document.getElementById('btn-w-plus').onclick = () => {
settings.maxWidth = Math.min(1900, settings.maxWidth + 50);
updateUI();
};
document.getElementById('btn-w-minus').onclick = () => {
settings.maxWidth = Math.max(400, settings.maxWidth - 50);
updateUI();
};
};
if (document.readyState === 'complete' || document.readyState === 'interactive') {
initReader();
} else {
window.addEventListener('DOMContentLoaded', initReader);
}
})();