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