[油猴脚本] 本论坛帖子筛选工具
本帖最后由 wonder2018 于 2024-11-21 19:49 编辑# 帖子筛选工具
在帖子列表头部添加了几个按钮支持以下功能:
1. 过滤被标记为已解决的帖子
2. 过滤回复数较高的帖子
3. 过滤悬赏 CB 较少的帖子
4. 还原被过滤的帖子
5. 使用正则表达式过滤帖子
6. 支持设置按钮添加位置
7. 支持设置各功能开启状态
8. 支持对帖子按照创建时间重新排序
## 安装
1. 通过 (https://greasyfork.org/zh-CN/scripts/518205) 安装
2. 通过 (https://raw.githubusercontent.com/Wonder2018/web-user-script/refs/heads/main/src/pj-filter-tools.user.js) 安装
3. 通过 本贴提供的源代码 安装
## 鸣谢
- [夜泉](https://www.52pojie.cn/home.php?mod=space&uid=580490) 报告脚本使用问题
- [三滑稽甲苯](https://www.52pojie.cn/home.php?mod=space&uid=1330976) 提供正则表达式过滤思路
- [木头人丶123](https://www.52pojie.cn/home.php?mod=space&uid=2121966) 提供按钮固定在屏幕右侧的相关代码
- (https://www.52pojie.cn/home.php?mod=space&uid=2339151) 提供将帖子按照创建时间排序的思路
> 排名不分先后
## 更新记录
### 2024-11-21
新增以下功能:
1. 将帖子根据创建时间重新排序
2. 将帖子恢复到默认排序
### 2024-11-20
新增以下功能:
1. 使用正则表达式过滤帖子
2. 支持设置按钮添加位置
3. 支持设置各功能开启状态
### 2024-11-19
1. 新增功能:过滤悬赏 CB 较少的帖子
2. 修复问题:导读页筛选失效(mod=guide&view=newthread)
### 2024-11-18
发布以下功能:
1. 过滤被标记为已解决的帖子
2. 过滤回复数较高的帖子
3. 还原被过滤的帖子
## 源代码
```javascript
// ==UserScript==
// @name 52pojie主题筛选工具
// @namespace http://tampermonkey.net/
// @version 0.0.1-b20241121
// @description52pojie主题页面增强,可以对主题列表进行筛选和排序
// @author wonder2018
// @license CC-BY-4.0 license
// @match https://www.52pojie.cn/forum*
// @icon http://52pojie.cn/favicon.ico
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// ==/UserScript==
;(function () {
'use strict'
// #region 初始化
const tableSelector = `div#threadlist table`
const appendSelector = `ul#thread_types`
const dftOrderAttrName = 'pj-dft-order'
const sideBtnWrapId = `filterBox`
const sideBtnWrapSelector = `ul#${sideBtnWrapId}`
const allHideTypes = new Set()
const filterBtnColor = '#66ccff'
const showBtnColor = 'goldenrod'
/** @type {HTMLLIElement[]} */
const btnList = []
const availableTools = [
{ switchName: '移除已解决', switchKey: 'useFilterBySolved', init: filterBySolved, menuId: null },
{ switchName: '移除回复多于x', switchKey: 'useFilterByReplyGt', init: filterByReplyGt, menuId: null },
{ switchName: '移除CB少于x', switchKey: 'useFilterByCBLt', init: filterByCBLt, menuId: null },
{ switchName: '正则过滤', switchKey: 'useFilterByRegExp', init: filterByRegExp, menuId: null },
{ switchName: '按创建时间排序', switchKey: 'useSortByCreateTime', init: sortByCreateTime, menuId: null },
]
const dftPJOpts = availableTools.reduce((p, i) => ((p = true), p), { showPos: 2 })
dftPJOpts.useFilterByRegExp = false
let togglePosMenuId = null
// #endregion 初始化
// #region 样式
// #region 样式 > 基础样式
GM_addStyle(`
.pj-btn {
border-radius: 5px;
color: #fff;
cursor: pointer;
}
.pj-btn>input.pj-ctl{
width:2em;
height:1em;
pointer-events:all;
text-align:center;
}
`)
// #endregion 样式 > 基础样式
// #region 样式 > fixed mode box
GM_addStyle(`
${sideBtnWrapSelector} {
position: fixed;
top: 50%;
right: 0;
padding: 40px 20px;
flex-direction: row;
z-index: 99999;
display: flex;
flex-wrap: wrap;
width: 295px;
transform: translate(95%, -50%);
transition: .3s;
transition-delay: .3s;
}
${sideBtnWrapSelector}:hover{
transform: translate(0, -50%);
transition-delay: 0s;
}
${sideBtnWrapSelector}::after{
content: '<';
position: absolute;
display: block;
width: 30px;
height: 50px;
line-height: 50px;
text-align: center;
background-color: #66ccff;
top: 0;
bottom: 0;
margin: auto;
left: -40px;
margin-left: 20px;
font-weight: bold;
color: rgb(0, 102, 255);
border-radius: 5px;
}
${sideBtnWrapSelector}>li {
margin: 5px 0;
flex-grow: 1;
flex-basis: 40%;
}
${sideBtnWrapSelector}>li>a {
padding: 10px;
border-color: rgb(0,102,255);
border-radius: 5px;
background-color: white;
color: rgb(0,102,255);
margin-right: 10px;
box-shadow: rgb(207,207,207) 1px 1px 9px 3px;
text-decoration: none;
display: block;
transition: .3s;
}
${sideBtnWrapSelector}>li>a:hover {
box-shadow: #888 1px 1px 9px 3px;
}
${sideBtnWrapSelector}>li>a>input {
float:right;
}
`)
// #endregion > fixed mode box
// #endregion 样式
// #region 菜单
const OPTION_KEY = 'PJ_FILTER_OPT'
const showPosList = ['移除按钮', '列表头部', '窗口右侧']
/** @type {typeof dftPJOpts} */
const options = JSON.parse(GM_getValue(OPTION_KEY, JSON.stringify(dftPJOpts)))
function saveOpt() {
regMenu()
GM_setValue(OPTION_KEY, JSON.stringify(options))
}
function toggleBtnPos(type = options.showPos) {
options.showPos = (type || 0) % 3
saveOpt()
if (options.showPos === 0) return removeAllBtn()
if (options.showPos === 1) return showBtnTableHead()
if (options.showPos === 2) return showBtnFixed()
}
/** @param {(typeof availableTools)} type */
function togglePJToolUse(type) {
if (type.switchKey === 'useFilterByRegExp') return changeRegExpFilter(type)
options = !options
saveOpt()
initPJTools()
}
/** @param {(typeof availableTools)} type */
function changeRegExpFilter(type) {
if (type.switchKey !== 'useFilterByRegExp') return
let reg = prompt('输入正则表达式(留空点击确定可关闭此功能)', options || '')
// 点击取消时不做任何操作
if (typeof reg !== 'string') return
reg = reg.trim()
const regObj = strToRegExp(reg.trim())
if (!(regObj instanceof RegExp) && regObj != null) return alert(`输入的正则表达式有误请检查:${reg}`)
options = !!regObj && `/${regObj.source}/${regObj.flags}`
saveOpt()
initPJTools()
}
function initPJTools() {
loadBtnByOpts()
toggleBtnPos(options.showPos)
}
function regToggleBtnPosMenu() {
if (togglePosMenuId != null) GM_unregisterMenuCommand(togglePosMenuId)
togglePosMenuId = GM_registerMenuCommand(`切换按钮位置:[${showPosList}]`, () => toggleBtnPos(options.showPos + 1))
}
/** @param {(typeof availableTools)} type */
function regToolsMenu(type) {
if (type.switchKey === 'useFilterByRegExp') return regRegExpFilterMenu(type)
if (type.menuId != null) GM_unregisterMenuCommand(type.menuId)
type.menuId = GM_registerMenuCommand(`${type.switchName}[${options ? '开' : '关'}]`, () => togglePJToolUse(type))
}
/** @param {(typeof availableTools)} type */
function regRegExpFilterMenu(type) {
if (type.switchKey !== 'useFilterByRegExp') return null
if (type.menuId != null) GM_unregisterMenuCommand(type.menuId)
type.menuId = GM_registerMenuCommand(`${type.switchName}[${options || '关'}]`, () => togglePJToolUse(type))
}
function regMenu() {
regToggleBtnPosMenu()
availableTools.forEach(i => regToolsMenu(i))
}
// #endregion 菜单
// #region utils
function createBtn(text, color) {
const btn = document.createElement('li')
btn.classList.add('pj-btn-wrap')
btn.innerHTML = `<a class="pj-btn" style="background-color:${color};">${text}</a>`
btnList.push(btn)
return btn
}
/** 处理还原隐藏帖子事件 */
function recoverByType(type) {
/** @type {HTMLTableSectionElement[]} */
const lines = [...document.querySelectorAll(`${tableSelector}>tbody[${type}]`)]
lines.forEach(i => {
i.setAttribute(type, false)
if ([...allHideTypes].every(t => !i.getAttribute(t) || i.getAttribute(t) == 'false')) {
i.style.display = ''
}
})
}
/** 根据配置项初始化所有按钮 */
function loadBtnByOpts() {
// 移除已添加的按钮,和附带元素(比如fixed模式下的按钮容器)
removeAllBtn()
// 确保所有按钮都被移除再清空数组
btnList.forEach(i => i.remove())
btnList.length = 0
availableTools.filter(i => options).forEach(i => i.init())
}
/** 按钮显示到列表头部 */
function removeAllBtn() {
btnList.forEach(i => i.remove())
const sideBtnWrap = document.querySelector(sideBtnWrapSelector)
sideBtnWrap && sideBtnWrap.remove()
}
/** 按钮显示到列表头部 */
function showBtnTableHead() {
const appendTarget = document.querySelector(appendSelector)
if (!appendTarget) return console.warn('不是论坛列表页,不添加按钮。')
btnList.forEach(i => appendTarget.appendChild(i))
const sideBtnWrap = document.querySelector(sideBtnWrapSelector)
sideBtnWrap && sideBtnWrap.remove()
}
/** 按钮固定在窗口右侧 */
function showBtnFixed() {
if (!btnList.length) return
const filterButtonBox = document.createElement('ul')
filterButtonBox.id = sideBtnWrapId
btnList.forEach(i => filterButtonBox.appendChild(i))
document.body.appendChild(filterButtonBox)
}
/** 字符串转正则表达式 */
function strToRegExp(str) {
if (!str || typeof str !== 'string') return null
try {
str = str.trim()
const part = str.match(/^\/(.+?)\/(\w*)$/)
/** @type {RegExp} */
let regObj = null
if (part) {
regObj = new RegExp(part, part)
} else if (str) {
regObj = new RegExp(str, 'i')
}
return regObj
} catch (error) {
return new Error('错误的正则表达式!')
}
}
/** 论坛目前默认排序方式是按照最新回复排序,为了兼容后续可能的改动,这里还是先保存默认排序,之后再按此顺序恢复 */
/** 保存原始顺序 @param {HTMLTableSectionElement[]} lines */
function setDftOrder(lines) {
let maxOrder = Math.max(-Infinity, 1, ...lines.map(i => i.getAttribute(dftOrderAttrName) || 0))
// 保存原始顺序
lines.forEach(i => {
// 已经记录了默认顺序的帖子跳过
if (i.hasAttribute(dftOrderAttrName)) return
i.setAttribute(dftOrderAttrName, maxOrder++)
})
}
/** 还原帖子排序 */
function recoverOrder() {
/** @type {HTMLTableSectionElement[]} */
const lines = [...document.querySelectorAll(`${tableSelector}>tbody`)]
// 为新增的没有参与排序的行添加顺序以便排序
setDftOrder(lines)
const sorted = lines.sort((a, b) => parseInt(a.getAttribute(dftOrderAttrName)) - parseInt(b.getAttribute(dftOrderAttrName)))
sortLines(sorted)
}
/** 按照顺序排列帖子 @param {HTMLTableSectionElement[]} lines */
function sortLines(lines) {
if (!Array.isArray(lines) || !lines.length) return
const container = lines.parentNode
if (!container) return console.warn('列表没有祖先节点,不进行操作!')
container.append(...lines)
}
/** 获取创建时间 @param {HTMLTableSectionElement} line */
function readCreateTime(line) {
let text = line.querySelector('.by')
if (!text || typeof text.innerText != 'string' || !text.innerText.trim()) return 0
let part = text.innerText
.replaceAll('\n', '')
.trim()
.match(/^.*?(\d{4})-(\d+)-(\d+)\s*(\d+):(\d+)$/)
// 获取时间失败,排在最后面
if (!part) return 0
// 将 part 转为标准数组,并且把获取到的值转为数字
part = new Array(5).fill(0).map((_, i) => parseInt(part))
// 获取date实例时月份要减1
part = part - 1
// 对 Invalid Date 执行 getTime 会得到 NaN
return new Date(...part).getTime() || 0
}
// #endregion utils
// #region 脚本功能
/** 移除已解决 */
function filterBySolved() {
const hideType = 'hide-by-solved'
allHideTypes.add(hideType)
const filterNonSolvedBtn = createBtn('移除已解决', filterBtnColor)
filterNonSolvedBtn.addEventListener('click', () => {
/** @type {HTMLTableSectionElement[]} */
const lines = [...document.querySelectorAll(`${tableSelector}>tbody`)]
lines.forEach(i => {
if (!(i.innerText || '').includes('已解决')) return
i.setAttribute(hideType, true)
i.style.display = 'none'
})
})
createBtn('还原已解决', showBtnColor).addEventListener('click', () => recoverByType(hideType))
}
/** 移除回复多于x */
function filterByReplyGt() {
const hideType = 'hide-by-reply-gt'
allHideTypes.add(hideType)
const filterByReply = createBtn(`移除回复多于 <input class="pj-ctl"/>`, filterBtnColor)
const replyCount = filterByReply.querySelector('input')
replyCount.addEventListener('click', e => e.stopPropagation())
replyCount.value = 0
filterByReply.addEventListener('click', () => {
const maxCount = replyCount.value
/** @type {HTMLTableSectionElement[]} */
const lines = [...document.querySelectorAll(`${tableSelector}>tbody`)]
lines.forEach(i => {
const countEl = i.querySelector('tr>td.num>.xi2')
if (parseInt(countEl.innerText) <= maxCount) return
i.setAttribute(hideType, true)
i.style.display = 'none'
})
})
createBtn('还原回复多于x', showBtnColor).addEventListener('click', () => recoverByType(hideType))
}
/** 移除CB少于x */
function filterByCBLt() {
const hideType = 'hide-by-cb-lt'
allHideTypes.add(hideType)
const filterByCB = createBtn(`移除CB少于 <input class="pj-ctl"/>`, filterBtnColor)
const CBCount = filterByCB.querySelector('input')
CBCount.addEventListener('click', e => e.stopPropagation())
CBCount.value = 0
filterByCB.addEventListener('click', () => {
const minCount = CBCount.value
/** @type {HTMLTableSectionElement[]} */
const lines = [...document.querySelectorAll(`${tableSelector}>tbody`)]
lines.forEach(i => {
const cb = parseInt(i.innerText.replaceAll('\n', '').replace(/.*悬赏\s*(\d+)\s*CB\s*吾爱币.*/, '$1'))
if (cb >= minCount) return
i.setAttribute(hideType, true)
i.style.display = 'none'
})
})
createBtn('还原CB少于x', showBtnColor).addEventListener('click', () => recoverByType(hideType))
}
/** 正则过滤 */
function filterByRegExp() {
const hideType = 'hide-by-reg-exp'
allHideTypes.add(hideType)
const filterByRegExp = createBtn(`正则过滤`, filterBtnColor)
filterByRegExp.addEventListener('click', () => {
/** @type {HTMLTableSectionElement[]} */
const lines = [...document.querySelectorAll(`${tableSelector}>tbody`)]
const regObj = strToRegExp(options.useFilterByRegExp)
if (!(regObj instanceof RegExp)) return
lines.forEach(i => {
if (!regObj.test(i.innerText.replaceAll('\n', ''))) return
i.setAttribute(hideType, true)
i.style.display = 'none'
})
})
createBtn('还原正则过滤', showBtnColor).addEventListener('click', () => recoverByType(hideType))
}
/** 按创建时间排序 */
function sortByCreateTime() {
const sortByCreateTime = createBtn(`按创建时间排序`, filterBtnColor)
sortByCreateTime.addEventListener('click', () => {
/** @type {HTMLTableSectionElement[]} */
const lines = [...document.querySelectorAll(`${tableSelector}>tbody`)]
setDftOrder(lines)
const sorted = lines.sort((a, b) => readCreateTime(b) - readCreateTime(a))
sortLines(sorted)
})
createBtn('还原排序', showBtnColor).addEventListener('click', recoverOrder)
}
// #endregion 脚本功能
window.addEventListener('load', () => {
regMenu()
const appendTarget = document.querySelector(appendSelector)
if (!appendTarget) return console.warn('不是论坛列表页,不添加按钮。')
initPJTools()
})
})()
``` 这里面失效
https://www.52pojie.cn/forum.php?mod=guide&view=newthread 哈哈,之前写了个正则表达式过滤帖子的小工具,感觉可以和这个一起用 本帖最后由 木头人丶123 于 2024-11-20 00:49 编辑
{:1_893:}感谢楼主提供的工具
在楼主的基础上优化了一点样式,增加了按钮浮动显示方式
使用方式:修改filterBtnType=2,默认为1原有显示
// ==UserScript==
// @name 52pojie主题筛选
// @namespace http://tampermonkey.net/
// @version 2024-11-18
// @descriptiontry to take over the world!
// @AuThor You
// @match https://www.52pojie.cn/forum*
// @Icon https://www.google.com/s2/favicons?sz=64&domain=52pojie.cn
// @grant none
// ==/UserScript==
(function () {
"use strict";
const tableSelector = `div#threadlist table`;
const appendSelector = `ul#thread_types`;
const allHideTypes = ["hide-by-solved", "hide-by-reply", "hide-by-lt-cb"];
const btnList = [];
//filterBtnType 1 按钮显示到顶部,2 按钮浮动显示
const filterBtnType = 1;
function createBtn(text, color) {
const btn = document.createElement("li");
btn.innerHTML = `<a style="background-color:${color};border-radius:5px;color:#fff;cursor:pointer;">${text}</a>`;
btnList.push(btn);
return btn;
}
function recoverByType(type) {
/** @type {HTMLTableSectionElement[]} */
const lines = [...document.querySelectorAll(`${tableSelector}>tbody[${type}]`)];
lines.forEach((i) => {
i.setAttribute(type, false);
if (allHideTypes.every((t) => !i.getAttribute(t) || i.getAttribute(t) == "false")) {
i.style.display = "";
}
});
}
//按钮显示到顶部
function showBtnTop(btnList) {
btnList.forEach((i) => document.querySelector(appendSelector).appendChild(i));
}
//按钮浮动显示
function showBtnFloat(btnList) {
const domHead = document.getElementsByTagName('head');
const domStyle = document.createElement('style');
domStyle.type = 'text/css';
domStyle.rel = 'stylesheet';
const filterStyle = `
#filterBox {
position: fixed;
top: 50%;
transform: translateY(-50%);
right: 20px;
gap: 20px;
flex-direction: column;
z-index: 99999;
display: flex;
}
#filterBox>li {
margin-top: 5px;
}
#filterBox>li>a {
padding: 10px;
border-color: rgb(0,102,255);
border-radius: 5px;
background-color: white;
color: rgb(0,102,255);
margin-right: 10px;
box-shadow: rgb(207,207,207) 1px 1px 9px 3px;
text-decoration: none;
}
`;
domStyle.appendChild(document.createTextNode(filterStyle));
domHead.appendChild(domStyle);
const filterButtonBox = document.createElement("ul");
filterButtonBox.id = "filterBox";
btnList.forEach((i) => {
filterButtonBox.appendChild(i);
});
document.body.appendChild(filterButtonBox);
}
window.addEventListener("load", () => {
// #region 移除已解决
{
const filterNonSolvedBtn = createBtn("移除已解决", "#66ccff");
filterNonSolvedBtn.addEventListener("click", () => {
/** @type {HTMLTableSectionElement[]} */
const lines = [...document.querySelectorAll(`${tableSelector}>tbody`)];
lines.forEach((i) => {
if (!(i.innerText || "").includes("已解决")) return;
i.setAttribute("hide-by-solved", true);
i.style.display = "none";
});
});
}
// #endregion 移除已解决
// #region 还原已解决
{
const showNonSolvedBtn = createBtn("还原已解决", "Green");
showNonSolvedBtn.addEventListener("click", () => recoverByType("hide-by-solved"));
}
// #endregion 还原已解决
// #region 移除回复多余x
{
const filterByReply = createBtn(`移除回复多于 <input style="width:2em;height:1em;pointer-events:all;text-align:center"/>`, "#66ccff");
const replyCount = filterByReply.querySelector("input");
replyCount.addEventListener("click", (e) => e.stopPropagation());
replyCount.value = 0;
filterByReply.addEventListener("click", () => {
const maxCount = replyCount.value;
/** @type {HTMLTableSectionElement[]} */
const lines = [...document.querySelectorAll(`${tableSelector}>tbody`)];
lines.forEach((i) => {
const countEl = i.querySelector("tr>td.num>.xi2");
if (parseInt(countEl.innerText) <= maxCount) return;
i.setAttribute("hide-by-reply", true);
i.style.display = "none";
});
});
}
// #endregion 移除回复多余
// #region 还原回复多余x
{
const showByReply = createBtn("还原回复多于x", "Green");
showByReply.addEventListener("click", () => recoverByType("hide-by-reply"));
}
// #endregion 还原已解决
// #region 移除CB少于x
{
const filterByCB = createBtn(`移除CB少于 <input style="width:2em;height:1em;pointer-events:all;text-align:center"/>`, "#66ccff");
const CBCount = filterByCB.querySelector("input");
CBCount.addEventListener("click", (e) => e.stopPropagation());
CBCount.value = 0;
filterByCB.addEventListener("click", () => {
const minCount = CBCount.value;
/** @type {HTMLTableSectionElement[]} */
const lines = [...document.querySelectorAll(`${tableSelector}>tbody`)];
lines.forEach((i) => {
const cb = parseInt(i.innerText.replaceAll("\n", "").replace(/.*悬赏\s*(\d+)\s*CB\s*吾爱币.*/, "$1"));
if (cb >= minCount) return;
i.setAttribute("hide-by-lt-cb", true);
i.style.display = "none";
});
});
}
// #endregion 移除CB少于x
// #region 还原CB少于x
{
const showByReply = createBtn("还原CB少于x", "Green");
showByReply.addEventListener("click", () => recoverByType("hide-by-lt-cb"));
}
// #endregion 还原CB少于x
if (filterBtnType === 1) {
showBtnTop(btnList);
} else if (filterBtnType === 2) {
showBtnFloat(btnList);
}
});
})();
木头人丶123 发表于 2024-11-20 00:46
感谢楼主提供的工具
在楼主的基础上优化了一点样式,增加了按钮浮动显示方式
使用方式:修改fil ...
赞!你的改动已经合并了!
{:301_1003:} c293943 发表于 2024-11-20 14:04
用脚本会不会被关小黑屋呀?
### 应该不会,我是基于以下几点确定的:
1. 脚本只是本地运行,隐藏帖子是通过在对应帖子行上面添加隐藏属性实现。
2. 运行时不会向服务器发送任何请求,也不会增加页面发送的请求。
3. 不会破坏论坛原有功能,不会因此对论坛服务器造成更大的压力。
> PS:如果你因为筛选过后剩下的帖子很少而疯狂点击**下一页**按钮,这种情况确实可能会对服务器造成更高的压力,但这不是因为运行此脚本造成的,而是你自己点击的。 感谢楼主,请问有没有默认按发帖时间排序的脚本? 可以。。 感谢分享,筛选很方便 厉害厉害 方便了很多,感谢分享 Ios的userscripts能用吗 学习学习,很厉害{:1_921:} 感谢分享 学到了