[分享] 强迫症的底层重构:Linux.do 全方位净化与智能导航

脚本简介 这是一个专为 Linux.do 论坛 设计的用户脚本,旨在提供干净、极简、功能增强的浏览体验。它通过隐藏干扰元素、折叠置顶话题以及添加便捷导航面板,让你专注于内容阅读,而不被广告、横幅和公告打扰。 主要功能 界面净化 自动隐藏论坛顶部的搜索横幅。 移除全局公告栏和欢迎横幅。 折叠置顶话题,...
[分享] 强迫症的底层重构:Linux.do 全方位净化与智能导航
[分享] 强迫症的底层重构:Linux.do 全方位净化与智能导航

脚本简介

这是一个专为 Linux.do 论坛 设计的用户脚本,旨在提供干净、极简、功能增强的浏览体验。它通过隐藏干扰元素、折叠置顶话题以及添加便捷导航面板,让你专注于内容阅读,而不被广告、横幅和公告打扰。

主要功能

  1. 界面净化
    • 自动隐藏论坛顶部的搜索横幅。
    • 移除全局公告栏和欢迎横幅。
    • 折叠置顶话题,减少列表占用空间,同时可一键展开查看。
  2. 智能置顶话题管理
    • 显示折叠/展开置顶话题的按钮,并记录用户偏好。
    • 支持动态刷新,自动适应新加载的置顶话题。
    • 可通过按钮快速切换显示状态。
  3. iOS 风格导航面板
    • 固定在页面右下角的毛玻璃浮动面板。
    • 面板包含:
      • 回到顶部:滚动到页面顶部并记录当前位置。
      • 返回原位 / 页尾:快速回到之前滚动的位置或页面底部。
    • 支持浅色/深色模式自适应,具备悬停、点击动画效果。
  4. 眼睛保护平滑滚动
    • 滚动过程中自动添加遮罩,减少闪烁和突变。
    • 提供平滑过渡动画,保护视觉体验。
  5. CloudFlare Challenge 自动跳转
    • 当访问受 CloudFlare 保护的页面时,检测错误提示并自动重定向至 Challenge 页面。
    • 提供菜单命令手动触发跳转,确保论坛访问不中断。
  6. 实时监控与自动刷新
    • 使用 MutationObserver 实时监控页面变化。
    • 当页面元素变动或新内容加载时,自动重新渲染导航面板和置顶话题。

使用方法

  1. 安装用户脚本管理器(如 Tampermonkey、Violentmonkey)。
  2. 新建脚本,将代码粘贴进去。
  3. 保存并访问 https://linux.do/ 即可生效。
  4. 可通过右键扩展菜单或脚本菜单手动触发 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: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>`,
    bottom: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>`
  };

  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();
  })();
})();

4 个帖子 - 4 位参与者

阅读完整话题

来源: LinuxDo 最新话题查看原文