CSDN Ultimate Reader

极简阅读增强插件

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         CSDN Ultimate Reader
// @namespace    local.csdn.ultimate.reader
// @version      5.0
// @description  极简阅读增强插件
// @author       liulipei
// @license      MIT
//
// @match        *://blog.csdn.net/*/article/details/*
//
// @homepageURL  https://github.com/vae-debug/csdn-ultimate-reader
// @supportURL   https://github.com/vae-debug/csdn-ultimate-reader/issues
//
// @grant        GM_setClipboard
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function () {

    'use strict';

    //--------------------------------------------------
    // 全局状态
    //--------------------------------------------------

    let currentFontSize = parseInt(
        localStorage.getItem('csdn_font_size') || '18'
    );

    let darkMode = localStorage.getItem(
        'csdn_dark_mode'
    ) === '1';

    //--------------------------------------------------
    // 获取正文
    //--------------------------------------------------

    function getArticle() {

        return (
            document.querySelector('.htmledit_views')
            || document.querySelector('#content_views')
            || document.querySelector('.blog-content-box')
        );

    }

    //--------------------------------------------------
    // 获取标题
    //--------------------------------------------------

    function getTitle() {

        const h1 = document.querySelector('h1');

        return h1
            ? h1.innerText
            : document.title;

    }

    //--------------------------------------------------
    // 创建阅读模式
    //--------------------------------------------------

    function createReaderMode() {

        const article = getArticle();

        if (!article) {
            console.log('未找到正文');
            return;
        }

        const content = article.innerHTML;
        const title = getTitle();

        document.body.innerHTML = '';

        //--------------------------------------------------
        // Body
        //--------------------------------------------------

        document.body.style.margin = '0';
        document.body.style.transition = 'all .3s';
        document.body.style.fontFamily = `
            "PingFang SC",
            "Microsoft YaHei",
            sans-serif
        `;

        //--------------------------------------------------
        // 主容器
        //--------------------------------------------------

        const container = document.createElement('div');

        container.id = 'reader-container';

        container.style.maxWidth = '960px';
        container.style.margin = '40px auto';
        container.style.padding = '60px';
        container.style.borderRadius = '14px';
        container.style.boxSizing = 'border-box';
        container.style.transition = 'all .3s';

        //--------------------------------------------------
        // 标题
        //--------------------------------------------------

        const titleEl = document.createElement('h1');

        titleEl.innerText = title;

        titleEl.style.fontSize = '42px';
        titleEl.style.lineHeight = '1.4';
        titleEl.style.marginBottom = '50px';

        //--------------------------------------------------
        // 正文
        //--------------------------------------------------

        const articleEl = document.createElement('div');

        articleEl.id = 'reader-content';

        articleEl.innerHTML = content;

        articleEl.style.fontSize = currentFontSize + 'px';
        articleEl.style.lineHeight = '2';
        articleEl.style.wordBreak = 'break-word';

        //--------------------------------------------------
        // 页面样式
        //--------------------------------------------------

        const style = document.createElement('style');

        style.innerHTML = `

            #reader-content img {
                display:block;
                max-width:100% !important;
                height:auto !important;
                margin:30px auto;
                border-radius:10px;
            }

            #reader-content pre {
                overflow-x:auto;
                padding:18px;
                border-radius:10px;
                background:#1e1e1e;
                color:#fff;
                position:relative;
            }

            #reader-content code {
                font-family:Consolas, monospace;
            }

            #reader-content table {
                width:100%;
                border-collapse:collapse;
                overflow:auto;
                display:block;
            }

            #reader-content p {
                margin:1.2em 0;
            }

            #reader-content h1,
            #reader-content h2,
            #reader-content h3,
            #reader-content h4 {
                margin-top:1.8em;
                margin-bottom:.8em;
            }

            #reader-content blockquote {
                margin:20px 0;
                padding:10px 20px;
                border-left:4px solid #888;
            }

            .copy-btn {
                position:absolute;
                right:10px;
                top:10px;
                border:none;
                border-radius:6px;
                padding:4px 10px;
                cursor:pointer;
            }

            #progress-bar {
                position:fixed;
                top:0;
                left:0;
                height:4px;
                width:0;
                z-index:99999999;
                transition:width .1s;
            }

            #toc {
                position:fixed;
                left:20px;
                top:100px;
                width:260px;
                max-height:70vh;
                overflow:auto;
                padding:15px;
                border-radius:12px;
                font-size:14px;
                z-index:999999;
            }

            #toc ul {
                list-style:none;
                padding-left:10px;
            }

            #toc li {
                margin:8px 0;
                line-height:1.5;
            }

            #toc a {
                text-decoration:none;
            }

            .reader-toolbar {
                position:fixed;
                right:20px;
                top:100px;
                z-index:999999;
                display:flex;
                flex-direction:column;
                gap:10px;
                padding:14px;
                border-radius:12px;
            }

            .reader-toolbar button {
                border:none;
                border-radius:8px;
                padding:8px 12px;
                cursor:pointer;
                font-size:14px;
            }

        `;

        document.head.appendChild(style);

        //--------------------------------------------------
        // 插入页面
        //--------------------------------------------------

        container.appendChild(titleEl);
        container.appendChild(articleEl);

        document.body.appendChild(container);

        //--------------------------------------------------
        // 进度条
        //--------------------------------------------------

        createProgressBar();

        //--------------------------------------------------
        // 工具栏
        //--------------------------------------------------

        createToolbar();

        //--------------------------------------------------
        // 自动目录
        //--------------------------------------------------

        createTOC();

        //--------------------------------------------------
        // 代码复制按钮
        //--------------------------------------------------

        addCopyButtons();

        //--------------------------------------------------
        // 数学公式优化
        //--------------------------------------------------

        optimizeMath();

        //--------------------------------------------------
        // 夜间模式
        //--------------------------------------------------

        applyTheme();

        //--------------------------------------------------
        // Vim 快捷键
        //--------------------------------------------------

        setupVimKeys();

    }

    //--------------------------------------------------
    // 主题
    //--------------------------------------------------

    function applyTheme() {

        const container = document.querySelector('#reader-container');
        const toc = document.querySelector('#toc');
        const toolbar = document.querySelector('.reader-toolbar');
        const progress = document.querySelector('#progress-bar');

        if (darkMode) {

            document.body.style.background = '#111';

            container.style.background = '#1b1b1b';
            container.style.color = '#ddd';
            container.style.boxShadow = '0 0 20px rgba(0,0,0,.5)';

            if (toc) {
                toc.style.background = '#222';
                toc.style.color = '#ddd';
            }

            if (toolbar) {
                toolbar.style.background = '#222';
            }

            if (progress) {
                progress.style.background = '#4caf50';
            }

        } else {

            document.body.style.background = '#f5f5f5';

            container.style.background = '#fff';
            container.style.color = '#222';
            container.style.boxShadow = '0 2px 14px rgba(0,0,0,.08)';

            if (toc) {
                toc.style.background = '#fff';
                toc.style.color = '#222';
            }

            if (toolbar) {
                toolbar.style.background = '#fff';
            }

            if (progress) {
                progress.style.background = '#1976d2';
            }

        }

    }

    //--------------------------------------------------
    // 工具栏
    //--------------------------------------------------

    function createToolbar() {

        const toolbar = document.createElement('div');

        toolbar.className = 'reader-toolbar';

        toolbar.innerHTML = `

            <button id="font-dec">A-</button>
            <button id="font-inc">A+</button>
            <button id="toggle-dark">夜间模式</button>
            <button id="export-md">导出MD</button>
            <button id="export-pdf">导出PDF</button>
            <button id="ai-summary">AI总结</button>

        `;

        document.body.appendChild(toolbar);

        toolbar.querySelector('#font-inc').onclick = () => {
            currentFontSize++;
            updateFont();
        };

        toolbar.querySelector('#font-dec').onclick = () => {
            currentFontSize = Math.max(12, currentFontSize - 1);
            updateFont();
        };

        toolbar.querySelector('#toggle-dark').onclick = () => {
            darkMode = !darkMode;
            localStorage.setItem(
                'csdn_dark_mode',
                darkMode ? '1' : '0'
            );
            applyTheme();
        };

        toolbar.querySelector('#export-md').onclick = exportMarkdown;

        toolbar.querySelector('#export-pdf').onclick = () => {
            window.print();
        };

        toolbar.querySelector('#ai-summary').onclick = aiSummary;

    }

    //--------------------------------------------------
    // 更新字体
    //--------------------------------------------------

    function updateFont() {

        const content = document.querySelector('#reader-content');

        content.style.fontSize = currentFontSize + 'px';

        localStorage.setItem(
            'csdn_font_size',
            currentFontSize
        );

    }

    //--------------------------------------------------
    // 阅读进度条
    //--------------------------------------------------

    function createProgressBar() {

        const bar = document.createElement('div');

        bar.id = 'progress-bar';

        document.body.appendChild(bar);

        window.addEventListener('scroll', () => {

            const scrollTop = document.documentElement.scrollTop;

            const height =
                document.documentElement.scrollHeight
                - document.documentElement.clientHeight;

            const percent =
                (scrollTop / height) * 100;

            bar.style.width = percent + '%';

        });

    }

    //--------------------------------------------------
    // 自动目录
    //--------------------------------------------------

    function createTOC() {

        const headings = document.querySelectorAll(
            '#reader-content h1,#reader-content h2,#reader-content h3'
        );

        if (!headings.length) return;

        const toc = document.createElement('div');

        toc.id = 'toc';

        const ul = document.createElement('ul');

        headings.forEach((h, i) => {

            const id = 'toc-heading-' + i;

            h.id = id;

            const li = document.createElement('li');

            li.style.marginLeft =
                (parseInt(h.tagName[1]) - 1) * 12 + 'px';

            const a = document.createElement('a');

            a.href = '#' + id;
            a.innerText = h.innerText;

            li.appendChild(a);

            ul.appendChild(li);

        });

        toc.appendChild(ul);

        document.body.appendChild(toc);

    }

    //--------------------------------------------------
    // Markdown 导出
    //--------------------------------------------------

    function exportMarkdown() {

        const text = document.querySelector(
            '#reader-content'
        ).innerText;

        const blob = new Blob([text], {
            type: 'text/markdown'
        });

        const a = document.createElement('a');

        a.href = URL.createObjectURL(blob);

        a.download = getTitle() + '.md';

        a.click();

    }

    //--------------------------------------------------
    // 代码复制按钮
    //--------------------------------------------------

    function addCopyButtons() {

        const blocks = document.querySelectorAll('pre');

        blocks.forEach(pre => {

            const btn = document.createElement('button');

            btn.className = 'copy-btn';

            btn.innerText = '复制';

            btn.onclick = () => {

                navigator.clipboard.writeText(
                    pre.innerText
                );

                btn.innerText = '已复制';

                setTimeout(() => {
                    btn.innerText = '复制';
                }, 1500);

            };

            pre.appendChild(btn);

        });

    }

    //--------------------------------------------------
    // 数学公式优化
    //--------------------------------------------------

    function optimizeMath() {

        const math = document.querySelectorAll(
            '.MathJax, .katex'
        );

        math.forEach(el => {
            el.style.overflowX = 'auto';
            el.style.padding = '8px 0';
        });

    }

    //--------------------------------------------------
    // AI 总结(占位版本)
    //--------------------------------------------------

    function aiSummary() {

        const content = document.querySelector(
            '#reader-content'
        ).innerText;

        const shortText = content.slice(0, 1500);

        alert(
            'AI总结(演示版):\n\n'
            + shortText.slice(0, 300)
            + '...'
        );

        //--------------------------------------------------
        // 如果你有 OpenAI API Key
        // 可在这里接真实 AI 接口
        //--------------------------------------------------

    }

    //--------------------------------------------------
    // Vim 快捷键
    //--------------------------------------------------

    function setupVimKeys() {

        document.addEventListener('keydown', e => {

            if (
                e.target.tagName === 'INPUT'
                || e.target.tagName === 'TEXTAREA'
            ) {
                return;
            }

            switch (e.key) {

                case 'j':
                    window.scrollBy(0, 120);
                    break;

                case 'k':
                    window.scrollBy(0, -120);
                    break;

                case 'g':
                    window.scrollTo(0, 0);
                    break;

                case 'G':
                    window.scrollTo(
                        0,
                        document.body.scrollHeight
                    );
                    break;

                case 'd':
                    darkMode = !darkMode;
                    applyTheme();
                    break;

                case '+':
                    currentFontSize++;
                    updateFont();
                    break;

                case '-':
                    currentFontSize--;
                    updateFont();
                    break;

            }

        });

    }

    //--------------------------------------------------
    // 启动
    //--------------------------------------------------

    window.addEventListener('load', () => {

        setTimeout(
            createReaderMode,
            1200
        );

    });

})();