// ==UserScript== // @name Linuxdo Sieve | Linux.do筛选工具 // @namespace http://tampermonkey.net/ // @version 3.0 // @description 更优雅、更强力、更智能的 Linux.do 浏览体验增强脚本 // @author chadyi // @match https://linux.do/ // @icon https://www.google.com/s2/favicons?sz=64&domain=linux.do // @grant GM_setValue // @grant GM_getValue // @run-at document-end // @license MIT // @updateURL https://raw.githubusercontent.com/chadyi/LinuxdoSieve/main/LinuxdoSieve.user.js // @downloadURL https://raw.githubusercontent.com/chadyi/LinuxdoSieve/main/LinuxdoSieve.user.js // ==/UserScript== (function() { 'use strict'; // ================= 配置区域 ================= // 允许自动滚动的路径白名单 (首页及核心列表) const AUTO_SCROLL_PATHS = ['/', '/latest', '/top', '/new']; // 图标定义 const SVG_CHECK = ``; const SVG_BAN = ``; const LEVEL_CONFIG = [ { key: 'public', label: '公开(Lv0)', check: (cls) => !/lv\d+/i.test(cls) }, { key: 'lv1', label: 'Lv1', check: (cls) => /lv1/i.test(cls) }, { key: 'lv2', label: 'Lv2', check: (cls) => /lv2/i.test(cls) }, { key: 'lv3', label: 'Lv3', check: (cls) => /lv3/i.test(cls) }, ]; const CATEGORY_CONFIG = [ { id: '4', name: '开发调优' }, { id: '98', name: '国产替代' }, { id: '14', name: '资源荟萃' }, { id: '42', name: '文档共建' }, { id: '10', name: '跳蚤市场' }, { id: '27', name: '非我莫属' }, { id: '32', name: '读书成诗' }, { id: '46', name: '扬帆起航' }, { id: '34', name: '前沿快讯' }, { id: '92', name: '网络记忆' }, { id: '36', name: '福利羊毛' }, { id: '11', name: '搞七捻三' }, { id: '102', name: '社区孵化' }, { id: '2', name: '运营反馈' }, { id: '45', name: '深海幽域' } ]; const TAG_LIST = [ "无标签", "纯水", "快问快答", "人工智能", "软件开发", "夸克网盘", "病友", "ChatGPT", "树洞", "AFF", "OpenAI", "影视", "百度网盘", "VPS", "职场", "网络安全", "订阅节点", "抽奖", "Cursor", "游戏", "动漫", "作品集", "晒年味", "Gemini", "PT", "拼车", "求资源", "配置优化", "Claude", "NSFW", "圆圆满满" ]; const STATE_NEUTRAL = 0; const STATE_INCLUDE = 1; const STATE_EXCLUDE = 2; const TARGET_COUNT = 20; // ================= 初始化 ================= let activeLevelFilters = GM_getValue('filter_levels_v14', LEVEL_CONFIG.map(l => l.key)); let activeCategoryFilters = GM_getValue('filter_cats_v14', CATEGORY_CONFIG.map(c => c.id)); let tagStates = GM_getValue('filter_tags_state_v14', {}); let statusDiv = null; let checkInterval = null; // ================= 核心筛选逻辑 ================= function filterTopics() { const rows = document.querySelectorAll('.topic-list-body tr.topic-list-item'); if (!rows.length) return 0; let visibleCount = 0; const isAllLevels = activeLevelFilters.length === LEVEL_CONFIG.length; const isAllCats = activeCategoryFilters.length === CATEGORY_CONFIG.length; const includeTags = []; const excludeTags = []; TAG_LIST.forEach(tag => { const s = tagStates[tag] || STATE_NEUTRAL; if (s === STATE_INCLUDE) includeTags.push(tag); if (s === STATE_EXCLUDE) excludeTags.push(tag); }); rows.forEach(row => { const classListRaw = row.className; const classListArray = Array.from(row.classList); // 1. 等级 let levelMatch = isAllLevels; if (!levelMatch) { for (let filter of LEVEL_CONFIG) { if (activeLevelFilters.includes(filter.key) && filter.check(classListRaw)) { levelMatch = true; break; } } } // 2. 分类 let categoryMatch = isAllCats; if (levelMatch && !categoryMatch) { const categoryBadge = row.querySelector('.badge-category__wrapper span[data-category-id]'); if (categoryBadge) { const cid = categoryBadge.getAttribute('data-category-id'); const pid = categoryBadge.getAttribute('data-parent-category-id'); if (activeCategoryFilters.includes(cid) || (pid && activeCategoryFilters.includes(pid))) { categoryMatch = true; } } else { categoryMatch = true; } } // 3. 标签 let tagMatch = true; if (levelMatch && categoryMatch) { const rowTags = classListArray .filter(cls => cls.startsWith('tag-')) .map(cls => { let rawTag = cls.substring(4); try { return decodeURIComponent(rawTag); } catch (e) { return rawTag; } }); const hasNoTags = rowTags.length === 0; if (excludeTags.length > 0) { if (hasNoTags) { if (excludeTags.includes("无标签")) tagMatch = false; } else { if (rowTags.some(t => excludeTags.includes(t))) tagMatch = false; } } if (tagMatch && includeTags.length > 0) { let hit = false; if (hasNoTags) { if (includeTags.includes("无标签")) hit = true; } else { if (rowTags.some(t => includeTags.includes(t))) hit = true; } if (!hit) tagMatch = false; } } if (levelMatch && categoryMatch && tagMatch) { row.style.display = ''; visibleCount++; } else { row.style.display = 'none'; } }); return visibleCount; } function isFooterReached() { const footerMessage = document.querySelector('.footer-message'); if (footerMessage && footerMessage.offsetParent !== null) return true; const bottom = document.getElementById('topic-list-bottom'); if (bottom && bottom.innerText.includes('没有更多')) return true; return false; } function isLoading() { const spinner = document.querySelector('.spinner'); return spinner && spinner.offsetParent !== null; } function forceLoadLoop() { // === 1. 安全检查:只在首页/核心列表页自动滚动 === const currentPath = window.location.pathname; const isHomePage = AUTO_SCROLL_PATHS.includes(currentPath); // 如果不在首页,停止一切自动滚动行为,隐藏状态栏 if (!isHomePage) { updateStatus(''); return; } // === 2. 正常的加载逻辑 === const currentCount = filterTopics(); const isAllLevels = activeLevelFilters.length === LEVEL_CONFIG.length; const isAllCats = activeCategoryFilters.length === CATEGORY_CONFIG.length; const isAllTagsNeutral = TAG_LIST.every(t => !tagStates[t]); if (isAllLevels && isAllCats && isAllTagsNeutral) { updateStatus(''); return; } if (currentCount < TARGET_COUNT) { if (isFooterReached()) { updateStatus(`🚫 已到底部,共找到 ${currentCount} 条`); return; } if (isLoading()) { updateStatus(`⏳ 数据读取中... (当前 ${currentCount} 条)`); } else { updateStatus(`🚀 帖子不足 (${currentCount}/${TARGET_COUNT}),正在请求历史记录...`); window.scrollTo(0, document.body.scrollHeight - 150); setTimeout(() => { window.scrollTo(0, document.body.scrollHeight); }, 100); } } else { updateStatus(`✅ 筛选完毕 (当前显示 ${currentCount} 条)`); } } function updateStatus(text) { if (!statusDiv) return; statusDiv.innerText = text; statusDiv.style.opacity = text ? '1' : '0'; if (text.includes('🚀') || text.includes('⏳')) statusDiv.style.color = '#e67e22'; else if (text.includes('🚫')) statusDiv.style.color = '#e74c3c'; else statusDiv.style.color = '#2ecc71'; } // ================= 创建 UI ================= function createUI() { if (document.getElementById('topic-filter-panel-v14')) return; const targetContainer = document.querySelector('.list-controls') || document.querySelector('.topic-list'); if (!targetContainer) return; const container = document.createElement('div'); container.id = 'topic-filter-panel-v14'; container.style.cssText = ` margin-bottom: 15px; padding: 10px; background: var(--secondary); border: 1px solid var(--primary-low); border-radius: 8px; display: flex; flex-direction: column; gap: 6px; position: relative; `; function setButtonStyle(btn, isActive, labelText, isExclude = false) { btn.style.backgroundColor = 'transparent'; if (isExclude) { btn.style.color = '#dc3545'; btn.style.borderColor = '#dc3545'; btn.style.fontWeight = 'bold'; btn.innerHTML = SVG_BAN + labelText; } else if (isActive) { btn.style.color = '#28a745'; btn.style.borderColor = '#28a745'; btn.style.fontWeight = 'bold'; btn.innerHTML = SVG_CHECK + labelText; } else { btn.style.color = 'var(--primary)'; btn.style.borderColor = 'var(--primary-low)'; btn.style.fontWeight = 'normal'; btn.innerHTML = labelText; } } function createBinaryGroup(titleText, items, activeList, storageKey, valueGetter, labelGetter) { const row = document.createElement('div'); row.style.cssText = 'display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font-size: 13px; border-bottom: 1px dashed var(--primary-low); padding-bottom: 4px; padding-right: 120px;'; const title = document.createElement('strong'); title.innerText = titleText; title.style.marginRight = '5px'; title.style.minWidth = '60px'; row.appendChild(title); const toggleBtn = document.createElement('button'); toggleBtn.className = 'btn btn-small'; toggleBtn.innerText = '全选/重置'; toggleBtn.style.marginRight = '8px'; toggleBtn.onclick = () => { const allValues = items.map(valueGetter); if (activeList.length === allValues.length) activeList.length = 0; else { activeList.length = 0; activeList.push(...allValues); } GM_setValue(storageKey, activeList); row.querySelectorAll('.binary-btn').forEach(btn => { const val = btn.dataset.value; const lbl = btn.dataset.label; const isActive = activeList.includes(val); setButtonStyle(btn, isActive, lbl); }); filterTopics(); }; row.appendChild(toggleBtn); items.forEach(item => { const val = valueGetter(item); const lbl = labelGetter(item); const btn = document.createElement('div'); btn.className = 'binary-btn'; btn.dataset.value = val; btn.dataset.label = lbl; btn.style.cssText = 'cursor: pointer; padding: 2px 8px; border: 1px solid; border-radius: 4px; user-select: none; font-size: 12px; transition: all 0.2s; display: flex; align-items: center;'; setButtonStyle(btn, activeList.includes(val), lbl); btn.onclick = () => { if (activeList.includes(val)) { activeList.splice(activeList.indexOf(val), 1); setButtonStyle(btn, false, lbl); } else { activeList.push(val); setButtonStyle(btn, true, lbl); } GM_setValue(storageKey, activeList); filterTopics(); }; row.appendChild(btn); }); return row; } function createTriStateGroup(titleText, tagItems) { const row = document.createElement('div'); row.style.cssText = 'display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font-size: 13px; padding-top: 4px;'; const title = document.createElement('strong'); title.innerText = titleText; title.style.marginRight = '5px'; title.style.minWidth = '60px'; row.appendChild(title); const resetBtn = document.createElement('button'); resetBtn.className = 'btn btn-small'; resetBtn.innerText = '重置所有'; resetBtn.style.marginRight = '8px'; resetBtn.onclick = () => { tagStates = {}; GM_setValue('filter_tags_state_v14', tagStates); row.querySelectorAll('.tri-state-btn').forEach(btn => { const lbl = btn.dataset.tag; btn.dataset.state = STATE_NEUTRAL; setButtonStyle(btn, false, lbl, false); }); filterTopics(); }; row.appendChild(resetBtn); tagItems.forEach(tag => { const btn = document.createElement('div'); btn.className = 'tri-state-btn'; btn.dataset.tag = tag; btn.style.cssText = 'cursor: pointer; padding: 2px 8px; border: 1px solid; border-radius: 4px; user-select: none; font-size: 12px; transition: all 0.2s; display: flex; align-items: center;'; const currentState = tagStates[tag] || STATE_NEUTRAL; btn.dataset.state = currentState; const isExclude = currentState === STATE_EXCLUDE; const isInclude = currentState === STATE_INCLUDE; setButtonStyle(btn, isInclude, tag, isExclude); btn.onclick = () => { let s = parseInt(btn.dataset.state); s = (s + 1) % 3; btn.dataset.state = s; if (s === STATE_NEUTRAL) delete tagStates[tag]; else tagStates[tag] = s; GM_setValue('filter_tags_state_v14', tagStates); setButtonStyle(btn, s === STATE_INCLUDE, tag, s === STATE_EXCLUDE); filterTopics(); }; row.appendChild(btn); }); return row; } container.appendChild(createBinaryGroup('🛡️ 等级:', LEVEL_CONFIG, activeLevelFilters, 'filter_levels_v14', i => i.key, i => i.label)); container.appendChild(createBinaryGroup('📂 分类:', CATEGORY_CONFIG, activeCategoryFilters, 'filter_cats_v14', i => i.id, i => i.name)); container.appendChild(createTriStateGroup('🏷️ 标签:', TAG_LIST)); // 状态栏 statusDiv = document.createElement('div'); statusDiv.style.cssText = ` position: absolute; top: 13px; right: 15px; font-size: 13px; font-weight: bold; color: #888; text-align: right; transition: opacity 0.3s; opacity: 0; pointer-events: none; `; container.appendChild(statusDiv); targetContainer.parentNode.insertBefore(container, targetContainer); } // ================= 启动逻辑 ================= function start() { createUI(); if (checkInterval) clearInterval(checkInterval); checkInterval = setInterval(forceLoadLoop, 1500); } let lastUrl = location.href; new MutationObserver(() => { const url = location.href; if (url !== lastUrl) { lastUrl = url; setTimeout(start, 1000); } if (!document.getElementById('topic-filter-panel-v14')) { start(); } }).observe(document, {subtree: true, childList: true}); setTimeout(start, 1000); })();