PureReader

Distraction-free novel reader with translation compatibility, progress tracking, customizable typography, and much more.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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