PureReader (ZenMode)

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.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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