Distraction-free novel reader with translation compatibility, progress tracking, customizable typography, and much more.
// ==UserScript==
// @name PureReader
// @namespace https://gitlab.com/wandersons13/purereader
// @version 0.2
// @description Distraction-free novel reader with translation compatibility, progress tracking, customizable typography, and much more.
// @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 fonts = [{
name: 'Inter',
type: 'sans-serif'
},
{
name: 'Merriweather',
type: 'serif'
},
{
name: 'Special Elite',
type: 'cursive'
}
];
const saveSettings = () => localStorage.setItem('gm_reader_settings', JSON.stringify(settings));
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.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);
}
})();