脚本简介
这是一个专为 Linux.do 设计的用户脚本,旨在提供干净、极简、功能增强的浏览体验。它通过隐藏干扰元素、折叠置顶话题以及添加便捷导航面板,让你专注于内容阅读,而不被广告、横幅和公告打扰。
主要功能
-
界面净化
-
自动隐藏论坛顶部的搜索横幅。
-
移除全局公告栏和欢迎横幅。
-
折叠置顶话题,减少列表占用空间,同时可一键展开查看。
-
-
智能置顶话题管理
-
显示折叠/展开置顶话题的按钮,并记录用户偏好。
-
支持动态刷新,自动适应新加载的置顶话题。
-
可通过按钮快速切换显示状态。
-
-
iOS 风格导航面板
-
固定在页面右下角的毛玻璃浮动面板。
-
面板包含:
-
回到顶部:滚动到页面顶部并记录当前位置。
-
返回原位 / 页尾:快速回到之前滚动的位置或页面底部。
-
-
支持浅色/深色模式自适应,具备悬停、点击动画效果。
-
-
眼睛保护平滑滚动
-
滚动过程中自动添加遮罩,减少闪烁和突变。
-
提供平滑过渡动画,保护视觉体验。
-
-
CloudFlare Challenge 自动跳转
-
当访问受 CloudFlare 保护的页面时,检测错误提示并自动重定向至 Challenge 页面。
-
提供菜单命令手动触发跳转,确保论坛访问不中断。
-
-
实时监控与自动刷新
-
使用 MutationObserver 实时监控页面变化。
-
当页面元素变动或新内容加载时,自动重新渲染导航面板和置顶话题。
-
使用方法
-
安装用户脚本管理器(如 Tampermonkey、Violentmonkey)。
-
新建脚本,将代码粘贴进去。
-
保存并访问 https://linux.do/ 即可生效。
-
可通过右键扩展菜单或脚本菜单手动触发 CloudFlare Challenge 跳转。
// ==UserScript==
// @name Linux.do - 论坛全方位净化导航
// @namespace https://linux.do/
// @version 1.0
// @description 隐藏搜索横幅 + 移除全局公告栏 + 折叠置顶话题
// @author
// @match https://linux.do/*
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @run-at document-start
// @license MIT
// ==/UserScript==
GM_addStyle(`
:root {
–ios-panel-width: 44px;
–ios-panel-radius: 24px;
–ios-btn-width: 36px;
–ios-btn-height: 36px;
–ios-blur: blur(35px) saturate(220%);
–ios-border: 1px solid rgba(255, 255, 255, 0.45);
}
#main-outlet > div[class*=“welcome-banner”],
#main-outlet > div.–location-above-topic-content,
div.custom-search-banner-wrap.welcome-banner__wrap,
div[data-component=“welcome-banner”],
div.global-notice {
display: none !important;
}
.topic-list-item.pinned, tr.pinned, .latest-topic-list-item.pinned {
background-color: var(–secondary, #ffffff) !important;
}
.dark-mode .topic-list-item.pinned, .dark-mode tr.pinned, .dark-mode .latest-topic-list-item.pinned {
background-color: var(–secondary, #111111) !important;
}
.topic-list-body tr.pinned, .topic-list tbody tr.pinned, table.topic-list tbody tr.pinned {
display: none !important;
}
html.ld-pinned-expanded .topic-list-body tr.pinned,
html.ld-pinned-expanded .topic-list tbody tr.pinned,
html.ld-pinned-expanded table.topic-list tbody tr.pinned {
display: table-row !important;
}
tr.linux-do-pinned-toggle-row td {
padding: 12px; border-bottom: 1px solid var(–primary-low, #e9ecef); background: var(–secondary, #fff);
}
.linux-do-pinned-toggle-button {
padding: 0; border: 0; background: transparent; color: var(–tertiary, #0ea5e9); font-size: 14px; font-weight: 600; cursor: pointer;
}
.linux-do-pinned-toggle-button:hover { text-decoration: underline; }
#ld-nav-panel {
position: fixed; right: 20px; bottom: 140px; z-index: 99999; width: var(–ios-panel-width); padding: 8px 0;
display: flex; flex-direction: column; align-items: center; gap: 8px;
border: var(–ios-border); border-radius: var(–ios-panel-radius); background: rgba(255, 255, 255, 0.42);
-webkit-backdrop-filter: var(–ios-blur); backdrop-filter: var(–ios-blur);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.08);
}
#ld-nav-panel.ld-nav-hidden {
display: none !important;
}
.ld-nav-btn {
width: var(–ios-btn-width); height: var(–ios-btn-height); border: none; border-radius: 50%; background: transparent; cursor: pointer;
display: flex; align-items: center; justify-content: center; color: rgba(0, 0, 0, 0.7);
transition: background 0.1s ease, color 0.1s ease;
}
.ld-nav-btn:hover { color: #000000; background: rgba(0, 0, 0, 0.06); }
.ld-nav-btn:active { background: rgba(0, 0, 0, 0.12); }
.dark-mode #ld-nav-panel { background: rgba(28, 28, 30, 0.45); border: 1px solid rgba(255, 255, 255, 0.15); box-shadow: 0 12px 36px rgba(0, 0, 0, 0.4); }
.dark-mode .ld-nav-btn { color: rgba(255, 255, 255, 0.8); }
.dark-mode .ld-nav-btn:hover { color: #ffffff; background: rgba(255, 255, 255, 0.12); }
.dark-mode .ld-nav-btn:active { background: rgba(255, 255, 255, 0.18); }
html.ld-scroll-locked { overflow: hidden !important; }
#ld-eye-protection-mask {
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 999999; opacity: 0; pointer-events: none;
transform: translate3d(0, 0, 0); background: radial-gradient(circle at center, rgba(255, 255, 255, 0.35) 0%, rgba(232, 241, 255, 0.65) 100%);
-webkit-backdrop-filter: blur(0px) saturate(100%); backdrop-filter: blur(0px) saturate(100%);
will-change: opacity, backdrop-filter, -webkit-backdrop-filter;
transition: opacity 0.2s ease, backdrop-filter 0.2s ease, -webkit-backdrop-filter 0.2s ease;
}
.dark-mode #ld-eye-protection-mask { background: radial-gradient(circle at center, rgba(14, 14, 17, 0.6) 0%, rgba(24, 25, 33, 0.8) 100%); }
#ld-eye-protection-mask.mask-entering { opacity: 1; pointer-events: auto; -webkit-backdrop-filter: blur(55px) saturate(240%); backdrop-filter: blur(55px) saturate(240%); }
#ld-eye-protection-mask.mask-leaving { opacity: 0; pointer-events: none; -webkit-backdrop-filter: blur(0px) saturate(100%); backdrop-filter: blur(0px) saturate(100%); transition: opacity 0.22s ease, backdrop-filter 0.22s ease, -webkit-backdrop-filter 0.22s ease; }
html.ld-scroll-locked .d-header, html.ld-scroll-locked .d-header * { transition: none !important; animation: none !important; opacity: 0 !important; visibility: hidden !important; pointer-events: none !important; }
`);
(() => {
‘use strict’;
var CF_CONFIG = {
ERROR_TEXTS: [‘403 error’, ‘该回应是很久以前创建的’, ‘reaction was created too long ago’, ‘我们无法加载该话题’, ‘You are not allowed to react’],
DIALOG_SELECTOR: ‘.dialog-body’,
CHALLENGE_PATH: ‘/challenge’,
MENU_TEXT: ‘手动触发 Challenge 跳转’,
NOT_FOUND_REDIRECT_GUARD_KEY: ‘linux_do_auto_challenge_nf_guard’
};
if (globalThis.location.pathname.startsWith(CF_CONFIG.CHALLENGE_PATH)) {
const isNotFoundPage = () => Boolean(document.querySelector(‘.page-not-found’));
const getRedirectParamUrl = () => {
try {
const sp = new URLSearchParams(globalThis.location.search);
const raw = sp.get(‘redirect’);
if (!raw) return void 0;
const url = new URL(raw, globalThis.location.origin);
return url.origin === globalThis.location.origin ? url.href : void 0;
} catch (e) { return void 0; }
};
const redirectFromNotFoundPage = () => {
const fallback = ‘’.concat(globalThis.location.origin, ‘/’);
const target = getRedirectParamUrl() || fallback;
const now = Date.now();
let guardTs = 0;
try { const raw = sessionStorage.getItem(CF_CONFIG.NOT_FOUND_REDIRECT_GUARD_KEY); guardTs = raw ? Number(raw) : 0; } catch (e) {}
if (guardTs && now - guardTs < 5e3) return;
try { sessionStorage.setItem(CF_CONFIG.NOT_FOUND_REDIRECT_GUARD_KEY, String(now)); } catch (e) {}
if (target === globalThis.location.href) { globalThis.location.replace(fallback); } else { globalThis.location.replace(target); }
};
const runChallengeGuard = () => { if (isNotFoundPage()) redirectFromNotFoundPage(); };
if (document.readyState === ‘loading’) { document.addEventListener(‘DOMContentLoaded’, runChallengeGuard); } else { runChallengeGuard(); }
return;
}
const SVG_ICONS = {
top: ``,
bottom: ``
};
const STORAGE_KEY = ‘linux-do-collapse-pinned-topics’;
const TOGGLE_ROW_CLASS = ‘linux-do-pinned-toggle-row’;
const TOGGLE_BUTTON_CLASS = ‘linux-do-pinned-toggle-button’;
let engineTimer = null, globalObserver = null;
let lastRecordedY = null;
function syncExpandedClass() {
document.documentElement.classList.toggle(‘ld-pinned-expanded’, window.localStorage.getItem(STORAGE_KEY) === ‘expanded’);
}
syncExpandedClass();
function redirectToChallenge() {
try { globalThis.location.href = ‘’.concat(CF_CONFIG.CHALLENGE_PATH, ‘?redirect=’).concat(encodeURIComponent(globalThis.location.href)); } catch (error) {}
}
function checkAndRedirect() {
try {
const dialogElement = document.querySelector(CF_CONFIG.DIALOG_SELECTOR);
if (!dialogElement) return false;
const text = dialogElement.textContent || ‘’;
if (CF_CONFIG.ERROR_TEXTS.some((errorText) => text.includes(errorText))) {
if (globalObserver) globalObserver.disconnect();
redirectToChallenge();
return true;
}
} catch (error) {}
return false;
}
function smoothScrollWithMask(targetY) {
if (!document.body || targetY === null || targetY === undefined) return;
document.documentElement.classList.add(‘ld-scroll-locked’);
let mask = document.getElementById(‘ld-eye-protection-mask’);
if (!mask) {
mask = document.createElement(‘div’);
mask.id = ‘ld-eye-protection-mask’;
document.body.append(mask);
}
mask.classList.remove(‘mask-leaving’);
mask.classList.add(‘mask-entering’);
if (mask.safetyTimer) clearTimeout(mask.safetyTimer);
const forceCleanUp = () => {
document.documentElement.classList.remove('ld-scroll-locked');
if (mask && mask.parentNode) mask.remove();
};
setTimeout(() => {
window.requestAnimationFrame(() => {
window.scrollTo({ top: targetY, behavior: 'auto' });
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
mask.classList.remove('mask-entering');
mask.classList.add('mask-leaving');
document.documentElement.classList.remove('ld-scroll-locked');
const onTransitionEnd = (e) => {
if (e.propertyName === 'opacity' && mask.classList.contains('mask-leaving')) {
forceCleanUp();
mask.removeEventListener('transitionend', onTransitionEnd);
}
};
mask.addEventListener('transitionend', onTransitionEnd);
mask.safetyTimer = setTimeout(forceCleanUp, 350);
});
});
});
}, 240);
}
function renderPinnedTopics() {
const tableBody = document.querySelector(‘#list-area tbody.topic-list-body, #list-area .topic-list tbody, table.topic-list tbody, tbody.topic-list-body’);
if (!tableBody) {
const activeRow = document.querySelector(`tr.${TOGGLE_ROW_CLASS}`);
if (activeRow) activeRow.remove();
return;
}
const pinnedRows = [];
for (const row of Array.from(tableBody.children)) {
if (row.tagName !== ‘TR’ || row.classList.contains(TOGGLE_ROW_CLASS)) continue;
if (!row.classList.contains(‘pinned’)) break;
pinnedRows.push(row);
}
if (pinnedRows.length === 0) {
const activeRow = document.querySelector(`tr.${TOGGLE_ROW_CLASS}`);
if (activeRow) activeRow.remove();
return;
}
const firstRow = tableBody.querySelector(‘tr:not(.linux-do-pinned-toggle-row)’);
const colCount = firstRow ? firstRow.cells.length : 5;
let toggleRow = tableBody.querySelector(\`:scope > tr.${TOGGLE_ROW_CLASS}\`);
if (!toggleRow) {
toggleRow = document.createElement('tr');
toggleRow.className = TOGGLE_ROW_CLASS;
toggleRow.innerHTML = \`<td colspan="${colCount}"><button type="button" class="${TOGGLE_BUTTON_CLASS}"></button></td>\`;
tableBody.insertBefore(toggleRow, pinnedRows\[0\]);
} else {
const td = toggleRow.querySelector('td');
if (td && td.getAttribute('colspan') !== String(colCount)) td.setAttribute('colspan', colCount);
}
const button = toggleRow.querySelector(\`button.${TOGGLE_BUTTON_CLASS}\`);
if (button) {
button.textContent = \`${window.localStorage.getItem(STORAGE_KEY) !== 'expanded' ? '显示' : '折叠'}置顶话题 (${pinnedRows.length})\`;
}
}
function processNavPanel() {
if (!document.body) return;
let panel = document.getElementById(‘ld-nav-panel’);
if (!panel) {
panel = document.createElement(‘div’);
panel.id = ‘ld-nav-panel’;
panel.className = ‘ld-nav-hidden’;
const buttons = \[
{ action: 'top', title: '回到顶部(记录原坐标)', svg: SVG_ICONS.top },
{ action: 'bottom', title: '返回原位 / 未读分割线 / 去页尾', svg: SVG_ICONS.bottom }
\];
for (const b of buttons) {
const btn = document.createElement('button');
btn.className = 'ld-nav-btn';
btn.setAttribute('data-action', b.action);
btn.title = b.title;
btn.setAttribute('aria-label', b.title);
btn.innerHTML = b.svg;
panel.append(btn);
}
document.body.append(panel);
}
panel.classList.toggle('ld-nav-hidden', !document.querySelector('#topic-title, .topic-navigation, .posts-section'));
}
function handleBottomClick() {
if (lastRecordedY !== null) {
smoothScrollWithMask(lastRecordedY);
lastRecordedY = null;
} else {
const unreadBar = document.querySelector(‘.unread-highlighter, .topic-avatar.unread’);
if (unreadBar) {
smoothScrollWithMask(unreadBar.getBoundingClientRect().top + window.scrollY - 100);
} else {
smoothScrollWithMask(document.documentElement.scrollHeight);
}
}
}
function executeEngine() {
const notice = document.querySelector(‘div.global-notice’);
if (notice) notice.remove();
processNavPanel();
renderPinnedTopics();
}
document.addEventListener(‘click’, (e) => {
const btn = e.target.closest(‘.ld-nav-btn’);
if (btn) {
const action = btn.getAttribute(‘data-action’);
if (action === ‘top’) {
lastRecordedY = window.scrollY;
smoothScrollWithMask(0);
} else if (action === ‘bottom’) {
handleBottomClick();
}
return;
}
if (e.target.classList.contains(TOGGLE_BUTTON_CLASS)) {
window.localStorage.setItem(STORAGE_KEY, window.localStorage.getItem(STORAGE_KEY) !== 'expanded' ? 'expanded' : 'collapsed');
syncExpandedClass();
executeEngine();
}
});
(() => {
try {
globalObserver = new MutationObserver((mutations) => {
let hasValidChange = false;
let hasNewNodes = false;
for (const m of mutations) {
if (m.target.id === 'ld-eye-protection-mask' || m.target.id === 'ld-nav-panel' || m.target.className === TOGGLE_ROW_CLASS) continue;
hasValidChange = true;
if (m.addedNodes.length > 0) hasNewNodes = true;
}
if (!hasValidChange) return;
if (hasNewNodes && checkAndRedirect()) return;
if (engineTimer) clearTimeout(engineTimer);
engineTimer = setTimeout(executeEngine, 40);
});
globalObserver.observe(document.documentElement, { childList: true, subtree: true, characterData: true });
} catch (error) {}
try { GM_registerMenuCommand(CF_CONFIG.MENU_TEXT, redirectToChallenge); } catch (error) {}
executeEngine();
checkAndRedirect();
})();
})();
1 个帖子 - 1 位参与者