Reader Mode + Highlight with resilient floating button for tricky sites
Tính đến
// ==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);
})();