// ==UserScript==
// @name 专注阅读 - 移动端沉浸式阅读
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 消除页面噪音,提供沉浸式阅读体验
// @author FocusReader
// @match http://*/*
// @match https://*/*
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @downloadURL https://update.greasyfork.icu/scripts/538369/%E4%B8%93%E6%B3%A8%E9%98%85%E8%AF%BB%20-%20%E7%A7%BB%E5%8A%A8%E7%AB%AF%E6%B2%89%E6%B5%B8%E5%BC%8F%E9%98%85%E8%AF%BB.user.js
// @updateURL https://update.greasyfork.icu/scripts/538369/%E4%B8%93%E6%B3%A8%E9%98%85%E8%AF%BB%20-%20%E7%A7%BB%E5%8A%A8%E7%AB%AF%E6%B2%89%E6%B5%B8%E5%BC%8F%E9%98%85%E8%AF%BB.meta.js
// ==/UserScript==
(function() {
'use strict';
let isReaderMode = false;
let originalContent = null;
let readerContainer = null;
// 护眼配色方案
const colorSchemes = {
default: {
name: '默认白',
bg: '#ffffff',
text: '#333333',
border: '#e0e0e0'
},
warm: {
name: '暖白护眼',
bg: '#faf8f3',
text: '#4a4a4a',
border: '#e8e6e1'
},
sepia: {
name: '复古褐',
bg: '#f4f0e6',
text: '#5c4b37',
border: '#d4c8b8'
},
green: {
name: '护眼绿',
bg: '#f0f8f0',
text: '#2d4a2d',
border: '#c8e6c8'
},
dark: {
name: '暗夜模式',
bg: '#1a1a1a',
text: '#e0e0e0',
border: '#404040'
},
blue: {
name: '海洋蓝',
bg: '#f0f8ff',
text: '#1e3a5f',
border: '#c8d8e8'
}
};
// 字体选择
const fontFamilies = {
system: '系统默认',
songti: '"Songti SC", "SimSun", serif',
heiti: '"Heiti SC", "SimHei", sans-serif',
kaiti: '"Kaiti SC", "KaiTi", cursive',
fangsong: '"FangSong SC", "FangSong", serif',
pingfang: '"PingFang SC", "Hiragino Sans GB", sans-serif'
};
// 获取当前设置
function getCurrentSettings() {
return {
colorScheme: GM_getValue('colorScheme', 'warm'),
fontSize: GM_getValue('fontSize', '18'),
fontFamily: GM_getValue('fontFamily', 'system'),
lineHeight: GM_getValue('lineHeight', '1.8'),
maxWidth: GM_getValue('maxWidth', '90%')
};
}
// 保存设置
function saveSettings(settings) {
GM_setValue('colorScheme', settings.colorScheme);
GM_setValue('fontSize', settings.fontSize);
GM_setValue('fontFamily', settings.fontFamily);
GM_setValue('lineHeight', settings.lineHeight);
GM_setValue('maxWidth', settings.maxWidth);
}
// 智能提取文章内容
function extractContent() {
// 常见的文章容器选择器
const contentSelectors = [
'article',
'[role="main"]',
'.content',
'.article-content',
'.post-content',
'.entry-content',
'.main-content',
'#content',
'.article-body',
'.post-body'
];
let content = null;
// 首先尝试通过选择器查找
for (let selector of contentSelectors) {
content = document.querySelector(selector);
if (content && content.innerText.length > 200) {
break;
}
}
// 如果没找到合适的内容,使用启发式方法
if (!content || content.innerText.length < 200) {
const allElements = document.querySelectorAll('div, section, article, main');
let maxScore = 0;
for (let element of allElements) {
let score = 0;
const text = element.innerText || '';
const textLength = text.length;
if (textLength < 100) continue;
// 文本长度得分
score += Math.min(textLength / 100, 50);
// 段落数量得分
const paragraphs = element.querySelectorAll('p').length;
score += paragraphs * 3;
// 链接密度惩罚
const links = element.querySelectorAll('a').length;
const linkDensity = links / Math.max(textLength / 100, 1);
score -= linkDensity * 10;
// 类名和ID得分
const className = element.className.toLowerCase();
const id = element.id.toLowerCase();
if (className.includes('content') || className.includes('article') ||
className.includes('post') || id.includes('content') ||
id.includes('article') || id.includes('post')) {
score += 15;
}
if (score > maxScore) {
maxScore = score;
content = element;
}
}
}
return content;
}
// 清理内容
function cleanContent(element) {
if (!element) return null;
const clonedElement = element.cloneNode(true);
// 移除不需要的元素 - 加强版噪音过滤
const removeSelectors = [
// 脚本和样式
'script', 'style', 'noscript', 'link[rel="stylesheet"]',
// 媒体元素(移除所有图片和视频)
'img', 'picture', 'figure', 'video', 'audio', 'canvas', 'svg',
'iframe', 'embed', 'object',
// 广告和营销
'.advertisement', '.ads', '.ad', '[class*="ad-"]', '[id*="ad-"]',
'[class*="ads-"]', '[id*="ads-"]', '.promo', '.banner',
'.sponsor', '.promotion', '[class*="affiliate"]',
// 社交和分享
'.social', '.share', '.social-share', '.social-links',
'.share-buttons', '.social-media', '[class*="social"]',
'[class*="share"]', '.follow', '.subscribe',
// 导航和菜单
'nav', '.navigation', '.nav', '.menu', '.navbar',
'.breadcrumb', '.breadcrumbs', '.pagination',
'.next-prev', '.prev-next',
// 页眉页脚
'header', '.header', 'footer', '.footer',
'.site-header', '.site-footer', '.page-header',
'.page-footer',
// 侧边栏和小工具
'aside', '.sidebar', '.side-bar', '.widget',
'.widgets', '.secondary', '.complementary',
// 评论系统
'.comments', '.comment', '.comment-section',
'.comment-form', '.comment-list', '.discussion',
'.disqus', '.facebook-comments',
// 相关内容和推荐
'.related', '.related-posts', '.related-articles',
'.recommended', '.suggestions', '.more-posts',
'.similar', '.also-read', '.you-may-like',
// 表单和输入
'form', 'input', 'textarea', 'select', 'button[type="submit"]',
'.form', '.search-form', '.contact-form',
'.newsletter', '.signup', '.login',
// 弹窗和模态框
'.popup', '.modal', '.overlay', '.lightbox',
'.dialog', '.tooltip', '.dropdown',
// 标签和分类
'.tags', '.tag', '.categories', '.category',
'.meta', '.post-meta', '.entry-meta',
'.author', '.author-bio', '.author-info',
'.date', '.timestamp', '.published',
// 通用垃圾内容
'.noise', '.clutter', '.extra', '.misc',
'[role="complementary"]', '[role="banner"]', '[role="navigation"]',
'[role="contentinfo"]', '.skip-link', '.screen-reader-text',
// 特定网站常见噪音
'.wp-caption', '.wp-caption-text', '.caption',
'.gallery', '.slideshow', '.slider',
'.code-toolbar', '.toolbar', '.copy-code',
'.highlight-toolbar'
];
// 先移除明确的噪音元素
removeSelectors.forEach(selector => {
const elements = clonedElement.querySelectorAll(selector);
elements.forEach(el => el.remove());
});
// 移除所有包含特定关键词的元素
const noiseKeywords = [
'advertisement', 'ads', 'promo', 'banner', 'sponsor',
'social', 'share', 'follow', 'subscribe', 'newsletter',
'comment', 'related', 'recommended', 'similar', 'tags',
'category', 'author', 'meta', 'breadcrumb', 'navigation'
];
const allElements = Array.from(clonedElement.querySelectorAll('*'));
allElements.forEach(el => {
const className = (el.className || '').toLowerCase();
const id = (el.id || '').toLowerCase();
const tagName = el.tagName.toLowerCase();
// 检查类名和ID是否包含噪音关键词
const hasNoiseKeyword = noiseKeywords.some(keyword =>
className.includes(keyword) || id.includes(keyword)
);
if (hasNoiseKeyword) {
el.remove();
return;
}
// 移除空元素或只包含空白的元素
const text = el.textContent?.trim() || '';
if (text.length === 0 &&
!['p', 'div', 'section', 'article', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
el.remove();
return;
}
// 移除链接密度过高的元素(可能是导航或广告)
if (tagName === 'div' || tagName === 'section') {
const links = el.querySelectorAll('a').length;
const textLength = text.length;
const linkDensity = textLength > 0 ? links / (textLength / 100) : 0;
if (linkDensity > 0.5 && links > 3) {
el.remove();
return;
}
}
// 清理所有属性,只保留基本结构
Array.from(el.attributes).forEach(attr => {
el.removeAttribute(attr.name);
});
});
// 最后清理:只保留有意义的文本内容
const finalElements = Array.from(clonedElement.querySelectorAll('*'));
finalElements.forEach(el => {
const text = el.textContent?.trim() || '';
const tagName = el.tagName.toLowerCase();
// 保留标题和段落,但要求有足够的文本内容
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
if (text.length === 0 || text.length > 200) {
el.remove();
}
} else if (tagName === 'p') {
if (text.length < 10) {
el.remove();
}
} else if (tagName === 'div' || tagName === 'section') {
// 如果div或section没有有效的子元素,移除它
const hasValidChildren = el.querySelector('p, h1, h2, h3, h4, h5, h6');
if (!hasValidChildren && text.length < 50) {
el.remove();
}
}
});
return clonedElement;
}
// 创建设置面板
function createSettingsPanel() {
const settings = getCurrentSettings();
const currentScheme = colorSchemes[settings.colorScheme];
const panel = document.createElement('div');
panel.innerHTML = `
阅读设置
${Object.entries(colorSchemes).map(([key, scheme]) => `
${scheme.name}
`).join('')}
`;
document.body.appendChild(panel);
// 事件监听
panel.querySelector('#closeSettings').onclick = () => panel.remove();
// 配色方案选择
panel.querySelectorAll('[data-scheme]').forEach(item => {
item.onclick = () => {
panel.querySelectorAll('[data-scheme]').forEach(el =>
el.style.border = '2px solid #ddd'
);
item.style.border = '2px solid #007AFF';
};
});
// 字体大小实时预览
const fontSizeSlider = panel.querySelector('#fontSize');
const fontSizeDisplay = fontSizeSlider.nextElementSibling;
fontSizeSlider.oninput = () => {
fontSizeDisplay.textContent = fontSizeSlider.value + 'px';
};
// 行间距实时预览
const lineHeightSlider = panel.querySelector('#lineHeight');
const lineHeightDisplay = lineHeightSlider.nextElementSibling;
lineHeightSlider.oninput = () => {
lineHeightDisplay.textContent = lineHeightSlider.value;
};
// 应用设置
panel.querySelector('#applySettings').onclick = () => {
const newSettings = {
colorScheme: panel.querySelector('[data-scheme][style*="rgb(0, 122, 255)"]')?.dataset.scheme || settings.colorScheme,
fontSize: fontSizeSlider.value,
fontFamily: panel.querySelector('#fontFamily').value,
lineHeight: lineHeightSlider.value,
maxWidth: settings.maxWidth
};
saveSettings(newSettings);
applyReaderStyles();
panel.remove();
};
// 重置设置
panel.querySelector('#resetSettings').onclick = () => {
const defaultSettings = {
colorScheme: 'warm',
fontSize: '18',
fontFamily: 'system',
lineHeight: '1.8',
maxWidth: '90%'
};
saveSettings(defaultSettings);
applyReaderStyles();
panel.remove();
};
}
// 应用阅读器样式
function applyReaderStyles() {
if (!readerContainer) return;
const settings = getCurrentSettings();
const scheme = colorSchemes[settings.colorScheme];
const fontFamily = settings.fontFamily === 'system' ?
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' :
fontFamilies[settings.fontFamily];
readerContainer.style.cssText = `
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
background: ${scheme.bg} !important;
color: ${scheme.text} !important;
z-index: 10000 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
padding: 0 !important;
margin: 0 !important;
font-family: ${fontFamily} !important;
font-size: ${settings.fontSize}px !important;
line-height: ${settings.lineHeight} !important;
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale !important;
`;
const contentArea = readerContainer.querySelector('.reader-content');
if (contentArea) {
contentArea.style.cssText = `
max-width: ${settings.maxWidth} !important;
margin: 0 auto !important;
padding: 40px 20px 60px 20px !important;
word-wrap: break-word !important;
hyphens: auto !important;
`;
}
// 应用内容样式
const allElements = readerContainer.querySelectorAll('*');
allElements.forEach(el => {
if (el.tagName === 'H1' || el.tagName === 'H2' || el.tagName === 'H3' ||
el.tagName === 'H4' || el.tagName === 'H5' || el.tagName === 'H6') {
el.style.cssText = `
color: ${scheme.text} !important;
margin: 1.5em 0 1em 0 !important;
font-weight: bold !important;
line-height: 1.4 !important;
`;
} else if (el.tagName === 'P') {
el.style.cssText = `
color: ${scheme.text} !important;
margin: 1em 0 !important;
text-align: justify !important;
text-indent: 2em !important;
`;
} else if (el.tagName === 'BLOCKQUOTE') {
el.style.cssText = `
border-left: 4px solid ${scheme.border} !important;
padding-left: 1em !important;
margin: 1em 0 !important;
font-style: italic !important;
color: ${scheme.text} !important;
background: transparent !important;
`;
}
// 移除了图片相关的样式设置,因为图片已经被过滤掉了
});
}
// 创建工具栏
function createToolbar() {
const toolbar = document.createElement('div');
toolbar.className = 'reader-toolbar';
toolbar.innerHTML = `
`;
readerContainer.appendChild(toolbar);
// 获取工具栏内容区域
const toolbarContent = toolbar.querySelector('.toolbar-content');
// 工具栏事件
toolbar.querySelector('#readerSettings').onclick = createSettingsPanel;
toolbar.querySelector('#readerScrollTop').onclick = () => {
readerContainer.scrollTo({ top: 0, behavior: 'smooth' });
};
toolbar.querySelector('#exitReader').onclick = exitReaderMode;
// --- Corrected Toolbar Scroll Logic ---
let scrollTimeout;
let lastScrollTop = readerContainer.scrollTop;
function showToolbar() {
toolbarContent.style.opacity = '1';
toolbarContent.style.transform = 'translateX(-50%) translateY(0)';
}
function hideToolbar() {
toolbarContent.style.opacity = '0';
toolbarContent.style.transform = 'translateX(-50%) translateY(20px)';
}
readerContainer.addEventListener('scroll', () => {
const currentScrollTop = readerContainer.scrollTop;
// Clear any existing timeout
clearTimeout(scrollTimeout);
if (currentScrollTop > lastScrollTop) { // Scrolling down
// Temporarily hide immediately when scrolling down
hideToolbar();
// If scrolling stops while going down, keep it hidden
scrollTimeout = setTimeout(() => {
// Do nothing, keep hidden if still scrolling down or stopped after scrolling down
}, 300); // Small delay to check if still scrolling
} else if (currentScrollTop < lastScrollTop) { // Scrolling up
// Show immediately when scrolling up
showToolbar();
// If scrolling stops after scrolling up, keep it visible
scrollTimeout = setTimeout(() => {
showToolbar();
}, 300);
} else { // Scroll stopped (no change in scrollTop)
if (currentScrollTop === 0) { // If at the very top, always show
showToolbar();
} else if (toolbarContent.style.opacity === '0') {
// If previously hidden (scrolled down), keep hidden when stopped
// This is the core fix for "down-scroll-then-stop-and-hide"
} else {
// If previously visible (scrolled up or at top), keep visible when stopped
showToolbar();
}
}
lastScrollTop = currentScrollTop;
});
// Ensure toolbar is visible when entering reader mode initially
showToolbar();
}
// 进入阅读模式
function enterReaderMode() {
if (isReaderMode) return;
// 保存原始内容
originalContent = {
html: document.documentElement.innerHTML,
title: document.title
};
// 提取文章内容
const content = extractContent();
if (!content) {
alert('未能识别到文章内容,请手动选择文本后重试');
return;
}
const cleanedContent = cleanContent(content);
if (!cleanedContent) {
alert('内容处理失败,请重试');
return;
}
// 创建阅读器容器
readerContainer = document.createElement('div');
readerContainer.className = 'focus-reader-container';
// 获取标题
let title = document.title;
const h1 = document.querySelector('h1');
if (h1 && h1.innerText.length < 100) {
title = h1.innerText;
}
// 检查cleanedContent中是否已经有标题
const existingH1 = cleanedContent.querySelector('h1');
let contentHTML = cleanedContent.innerHTML;
// 如果内容中已有h1标题,就不额外添加标题
if (existingH1) {
readerContainer.innerHTML = `
`;
} else {
// 如果内容中没有h1标题,才添加页面标题
readerContainer.innerHTML = `
`;
}
document.body.appendChild(readerContainer);
// 隐藏原始内容
document.body.style.overflow = 'hidden';
Array.from(document.body.children).forEach(child => {
if (child !== readerContainer) {
child.style.display = 'none';
}
});
// 应用样式和创建工具栏
applyReaderStyles();
createToolbar();
isReaderMode = true;
// 阻止页面滚动
document.addEventListener('touchmove', preventDefaultTouch, { passive: false });
}
// 阻止默认触摸事件(除了阅读器内部)
function preventDefaultTouch(e) {
if (!readerContainer.contains(e.target)) {
e.preventDefault();
}
}
// 退出阅读模式
function exitReaderMode() {
if (!isReaderMode) return;
// 移除阅读器容器
if (readerContainer) {
readerContainer.remove();
readerContainer = null;
}
// 恢复原始内容显示
document.body.style.overflow = '';
Array.from(document.body.children).forEach(child => {
child.style.display = '';
});
// 移除事件监听
document.removeEventListener('touchmove', preventDefaultTouch);
isReaderMode = false;
}
// 注册油猴菜单
GM_registerMenuCommand('🔍 进入专注阅读', enterReaderMode);
GM_registerMenuCommand('⚙️ 阅读设置', createSettingsPanel);
// 快捷键支持(可选)
document.addEventListener('keydown', function(e) {
// Ctrl/Cmd + Shift + R 进入/退出阅读模式
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'R') {
e.preventDefault();
if (isReaderMode) {
exitReaderMode();
} else {
enterReaderMode();
}
}
});
// 移动端优化:添加触摸手势支持
let touchStartY = 0;
let touchStartTime = 0;
document.addEventListener('touchstart', function(e) {
if (e.touches.length === 2) {
touchStartY = e.touches[0].clientY + e.touches[1].clientY;
touchStartTime = Date.now();
}
});
document.addEventListener('touchend', function(e) {
if (e.changedTouches.length === 2 && touchStartTime > 0) {
const touchEndTime = Date.now();
if (touchEndTime - touchStartTime < 500) { // 500ms内的双指触摸
if (isReaderMode) {
exitReaderMode();
} else {
enterReaderMode();
}
}
}
touchStartTime = 0;
});
console.log('专注阅读脚本已加载 - 使用油猴菜单或双指轻触进入阅读模式');
})();