Reader view

Reader Mode + Highlight with resilient floating button for tricky sites

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Reader view
// @namespace    http://tampermonkey.net/
// @version      16.10.2025
// @description  Reader Mode + Highlight with resilient floating button for tricky sites
// @author       Copilot
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    let readerOpened = false; // flag to prevent reinjection after Reader Mode

    // ✅ Menu command added
    GM_registerMenuCommand("Open Reader Mode", () => {
        const btn = document.getElementById('tm-reader-btn');
        if (btn) {
            btn.click();
        } else {
            alert("Reader button not available yet.");
        }
    });

    function injectButton() {
        if (readerOpened) return; // stop reinjecting once Reader Mode is active
        if (document.getElementById('tm-reader-box')) return;

        const readerBox = document.createElement('div');
        readerBox.id = 'tm-reader-box';
        const readerBtn = document.createElement('button');
        readerBtn.id = 'tm-reader-btn';
        readerBtn.textContent = 'Reader';
        readerBox.appendChild(readerBtn);

        // Strong CSS
        const style = document.createElement('style');
        style.textContent = `
            #tm-reader-box {
                position: fixed !important;
                bottom: 10px !important;
                right: 60px !important;
                padding: 2px 6px !important;
                background: transparent !important;
                border: 1px solid transparent !important;
                border-radius: 6px !important;
                z-index: 2147483647 !important;
            }
            #tm-reader-btn {
                border: none !important;
                background: transparent !important;
                color: #696969 !important;
                font-size: 12px !important;
                cursor: pointer !important;
                font-family: system-ui, sans-serif !important;

                padding: 0 !important;
                margin: 0 !important;
            }
            #tm-reader-btn:hover { text-decoration: underline !important; }
        `;
        document.head.appendChild(style);

        // Append to both html and body
        if (document.body) document.body.appendChild(readerBox);
        document.documentElement.appendChild(readerBox);

        // Reader Mode logic
        readerBtn.addEventListener('click', async function() {
            const loadScript = url => new Promise((resolve, reject) => {
                const script = document.createElement('script');
                script.src = url;
                script.onload = resolve;
                script.onerror = reject;
                document.head.appendChild(script);
            });

            try {
                if (!window.Readability) {
                    await loadScript('https://cdn.jsdelivr.net/npm/@mozilla/[email protected]/Readability.js');
                }
                if (!window.DOMPurify) {
                    await loadScript('https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.min.js');
                }

                const clone = document.cloneNode(true);
                const article = new Readability(clone).parse();
                if (!article) {
                    alert('Reader Mode: No article found on this page.');
                    return;
                }

                // Remove the floating Reader button completely
                const readerBox = document.getElementById('tm-reader-box');
                if (readerBox) readerBox.remove();
                readerOpened = true; // prevent reinjection

                const css = `
                    html, body { margin:0; padding:0; background:#fff; }
                    body {
                        font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif !important;
                        max-width: 48rem;
                        margin: 0 auto;
                        padding: 2rem;
                        color: #111;
                        line-height: 1.6;
                    }
                    article *, article {
                        font-size: 28px !important;
                        font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif !important;
                    }
                    p, div, span, li { margin-bottom:1.5rem; word-break:normal; overflow-wrap:break-word; }
                    h1,h2,h3,h4,h5,h6 { line-height:1.3; margin:1.5rem 0 0.5rem; font-weight:700; }
                    a { color:#0b57d0; text-decoration:none; cursor:pointer; }
                    a:hover { text-decoration:underline; }
                    img, video { max-width:100%; height:auto; border-radius:8px; display:block; margin:2rem auto; }
                    pre, code { font-family: ui-monospace, monospace !important; font-size:22px !important; background:#f5f5f5; padding:0.2em 0.4em; border-radius:3px; }
                    @media (prefers-color-scheme: dark) {
                        html, body { background:#111; }
                        body { color:#eee; }
                        pre, code { background:#222; }
                        a { color:#4a90e2; }
                    }
                `;

                const html = `
                    <!DOCTYPE html>
                    <html>
                    <head>
                        <meta charset="utf-8">
                        <meta name="viewport" content="width=device-width, initial-scale=1.0">
                        <title>${article.title || 'Reader Mode'}</title>
                        <style>${css}</style>
                    </head>
                    <body>
                        <article>
                            <h1>${article.title || ''}</h1>
                            <div>${window.DOMPurify ? DOMPurify.sanitize(article.content) : article.content}</div>
                        </article>
                    </body>
                    </html>
                `;

                document.open();
                document.write(html);
                document.close();

                // Highlight button
                const btn = document.createElement('button');
                btn.innerText = '🖍️';
                btn.id = 'tm-highlight-btn';
                btn.style.cssText = `
                    position: fixed;
                    bottom: 20px;
                    right: 20px;
                    z-index: 10000;
                    padding: 10px;
                    border-radius: 50%;
                    background-color: #ffeb3b;
                    border: 2px solid #000;
                    font-size: 20px;
                    display: none;
                `;
                document.body.appendChild(btn);

                document.addEventListener('selectionchange', () => {
                    const selection = window.getSelection();
                    btn.style.display = (selection.toString().length > 0) ? 'block' : 'none';
                });

                btn.addEventListener('click', () => {
                    const selection = window.getSelection();
                    if (!selection.rangeCount) return;
                    const range = selection.getRangeAt(0);
                    const mark = document.createElement('mark');
                    mark.style.backgroundColor = 'yellow';
                    mark.style.color = 'black';
                    try {
                        range.surroundContents(mark);
                        selection.removeAllRanges();
                        btn.style.display = 'none';
                    } catch (e) {
                        alert("Can't highlight across multiple complex elements. Try a smaller selection!");
                    }
                });
            } catch (err) {
                alert('Reader Mode failed: ' + err.message);
            }
        });
    }

    // Inject after load
    window.addEventListener('load', injectButton);

    // Reinjection checks
    const observer = new MutationObserver(() => injectButton());
    observer.observe(document.documentElement, { childList: true, subtree: true });
    setInterval(injectButton, 2000);
})();