feat: Add 51cg website helper script with hot post filtering and read marking

- Add toggle to hide hot trending posts and ads on list pages
  - Implement automatic read post tracking with visual indicators (strikethrough, grayscale)
  - Auto-manage read history (max 1000 records with FIFO)
  - Add floating control panel with persistent settings
  - Support dark mode and dynamic content loading
  - Fix issue where article content pages were incorrectly hidden
  - Update README with script documentation
This commit is contained in:
Ching 2025-09-21 20:31:00 +08:00
commit e0b022d791
3 changed files with 586 additions and 0 deletions

418
51cg-helper.user.js Normal file
View File

@ -0,0 +1,418 @@
// ==UserScript==
// @name 51吃瓜网助手
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @description 隐藏热搜HOT帖子标记已读帖子
// @author Your Name
// @match https://51cg1.com/*
// @match https://www.51cg1.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// 添加样式
GM_addStyle(`
/* 控制按钮样式 */
.cg-helper-controls {
position: fixed;
top: 100px;
right: 20px;
z-index: 9999;
background: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
min-width: 150px;
}
.cg-helper-controls h4 {
margin: 0 0 10px 0;
font-size: 14px;
color: #333;
}
/* 开关样式 */
.switch-container {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
margin-right: 10px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #2196F3;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.switch-label {
font-size: 13px;
color: #666;
}
/* 隐藏热搜帖子和广告帖子的样式类 */
.hot-post-hidden,
.ad-post-hidden {
display: none !important;
margin: 0 !important;
padding: 0 !important;
height: 0 !important;
overflow: hidden !important;
visibility: hidden !important;
position: absolute !important;
width: 0 !important;
}
/* 确保隐藏article标签时不留空白但排除内容页的article.post */
article:not(.post).hot-post-hidden,
article:not(.post).ad-post-hidden {
display: none !important;
}
/* 修复第一个可见帖子的上边距,但不影响内容页 */
#index article:not(.hot-post-hidden):not(.ad-post-hidden):not(.post) {
margin-top: 0;
}
/* 如果article被隐藏确保下一个不会有额外间距 */
#index article.hot-post-hidden + article,
#index article.ad-post-hidden + article {
margin-top: 0 !important;
}
/* 已读帖子样式 - 标题删除线 + 图片黑白 */
.post-card.visited .post-card-title {
text-decoration: line-through;
opacity: 0.6;
}
/* 已读帖子的背景图片变黑白 */
.post-card.visited .blog-background {
filter: grayscale(100%);
opacity: 0.7;
}
/* 已读帖子整体透明度降低 */
.post-card.visited {
opacity: 0.65;
}
/* 深色模式适配 */
body.theme-dark .cg-helper-controls {
background: #2b2b2b;
color: #e0e0e0;
}
body.theme-dark .cg-helper-controls h4 {
color: #e0e0e0;
}
body.theme-dark .switch-label {
color: #b0b0b0;
}
`);
// 获取已访问的帖子列表
function getVisitedPosts() {
const visited = GM_getValue('visitedPosts', '[]');
try {
return JSON.parse(visited);
} catch (e) {
return [];
}
}
// 保存已访问的帖子
function saveVisitedPosts(posts) {
GM_setValue('visitedPosts', JSON.stringify(posts));
}
// 标记帖子为已访问
function markPostAsVisited(postId) {
let visited = getVisitedPosts();
// 如果已存在,先移除旧的位置
const existingIndex = visited.indexOf(postId);
if (existingIndex !== -1) {
visited.splice(existingIndex, 1);
}
// 添加到末尾(最新的)
visited.push(postId);
// 只保留最近的1000个记录避免数据过大
// 使用 while 循环确保数组长度不超过 1000
while (visited.length > 1000) {
visited.shift(); // 移除最旧的记录
}
saveVisitedPosts(visited);
// 输出调试信息
console.log(`已标记帖子 ${postId} 为已读,当前已读记录数:${visited.length}`);
}
// 应用已访问样式
function applyVisitedStyles() {
const visited = getVisitedPosts();
visited.forEach(postId => {
const postCard = document.getElementById(`post-card-${postId}`);
if (postCard) {
postCard.classList.add('visited');
}
});
}
// 监听点击事件
function setupClickListeners() {
// 监听所有帖子链接的点击
document.querySelectorAll('a[href*="/archives/"]').forEach(link => {
link.addEventListener('click', function() {
const match = this.href.match(/\/archives\/(\d+)\//);
if (match) {
const postId = match[1];
markPostAsVisited(postId);
// 立即应用样式
const postCard = document.getElementById(`post-card-${postId}`);
if (postCard) {
postCard.classList.add('visited');
}
}
});
});
}
// 处理热搜帖子的显示/隐藏
function toggleHotPosts(hide) {
// 判断是否在文章内容页面
const isArticlePage = window.location.pathname.includes('/archives/') ||
document.querySelector('article.post') !== null;
// 如果是文章内容页面,不执行隐藏操作
if (isArticlePage) {
console.log('文章内容页面,不执行隐藏操作');
return;
}
let hotCount = 0;
let adCount = 0;
// 1. 查找所有包含"热搜 HOT"标签的帖子(仅在列表页)
document.querySelectorAll('.wrap .wraps').forEach(element => {
if (element.textContent.includes('热搜 HOT')) {
// 优先找到最近的 article 元素,但排除内容页的 article.post
let articleElement = element.closest('article');
// 确保不是文章内容页的主article
if (articleElement && !articleElement.classList.contains('post')) {
if (hide) {
// 使用 display: none 和 !important 确保完全隐藏
articleElement.style.cssText = 'display: none !important;';
articleElement.classList.add('hot-post-hidden');
hotCount++;
} else {
// 恢复显示
articleElement.style.cssText = '';
articleElement.classList.remove('hot-post-hidden');
}
}
}
});
// 2. 同时隐藏广告帖子作者为AD1234或包含post-card-ads类
// 只处理列表页的 article不处理 article.post
document.querySelectorAll('article:not(.post)').forEach(article => {
// 检查是否是广告帖子
const authorMeta = article.querySelector('meta[itemprop="name"]');
const isAd = (authorMeta && authorMeta.content === 'AD1234') ||
article.querySelector('.post-card-ads') ||
article.querySelector('a[rel*="sponsored"]');
if (isAd) {
if (hide) {
article.style.cssText = 'display: none !important;';
article.classList.add('ad-post-hidden');
adCount++;
} else {
// 只恢复非广告的帖子
if (article.classList.contains('ad-post-hidden')) {
article.style.cssText = '';
article.classList.remove('ad-post-hidden');
}
}
}
});
// 调试信息
if (hide) {
console.log(`已隐藏 ${hotCount} 个热搜帖子,${adCount} 个广告帖子`);
}
}
// 创建控制面板
function createControlPanel() {
const panel = document.createElement('div');
panel.className = 'cg-helper-controls';
panel.innerHTML = `
<h4>51吃瓜助手</h4>
<div class="switch-container">
<label class="switch">
<input type="checkbox" id="hideHotSwitch">
<span class="slider"></span>
</label>
<span class="switch-label">隐藏热搜</span>
</div>
<div class="switch-container">
<label class="switch">
<input type="checkbox" id="markVisitedSwitch" checked>
<span class="slider"></span>
</label>
<span class="switch-label">标记已读</span>
</div>
`;
document.body.appendChild(panel);
// 热搜开关
const hideHotSwitch = document.getElementById('hideHotSwitch');
hideHotSwitch.checked = GM_getValue('hideHot', false);
if (hideHotSwitch.checked) {
toggleHotPosts(true);
}
hideHotSwitch.addEventListener('change', function() {
GM_setValue('hideHot', this.checked);
toggleHotPosts(this.checked);
});
// 标记已读开关
const markVisitedSwitch = document.getElementById('markVisitedSwitch');
markVisitedSwitch.checked = GM_getValue('markVisited', true);
markVisitedSwitch.addEventListener('change', function() {
GM_setValue('markVisited', this.checked);
if (this.checked) {
applyVisitedStyles();
} else {
// 移除所有已访问样式
document.querySelectorAll('.post-card.visited').forEach(card => {
card.classList.remove('visited');
});
}
});
}
// 处理动态加载的内容
function observeNewContent() {
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes.length) {
// 重新应用已访问样式
if (GM_getValue('markVisited', true)) {
applyVisitedStyles();
}
// 重新应用热搜隐藏
if (GM_getValue('hideHot', false)) {
toggleHotPosts(true);
}
// 重新设置点击监听
setupClickListeners();
}
});
});
// 观察主内容区域的变化
const contentArea = document.querySelector('.body-container-wrapper');
if (contentArea) {
observer.observe(contentArea, {
childList: true,
subtree: true
});
}
}
// 初始化
function init() {
// 清理超过限制的已读记录
cleanupVisitedRecords();
// 创建控制面板
createControlPanel();
// 应用保存的设置
if (GM_getValue('hideHot', false)) {
toggleHotPosts(true);
}
if (GM_getValue('markVisited', true)) {
applyVisitedStyles();
}
// 设置点击监听
setupClickListeners();
// 监听动态内容
observeNewContent();
}
// 清理超过限制的已读记录
function cleanupVisitedRecords() {
let visited = getVisitedPosts();
if (visited.length > 1000) {
// 只保留最新的 1000 条记录
visited = visited.slice(-1000);
saveVisitedPosts(visited);
console.log(`已清理已读记录,保留最新的 1000 条`);
}
}
// 等待DOM加载完成
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

72
README.md Normal file
View File

@ -0,0 +1,72 @@
# Tampermonkey Scripts Collection
一个 Tampermonkey 脚本集合,包含多个用于增强不同网站功能的脚本。
## 脚本列表
### 1. 51吃瓜网助手 (`51cg-helper.user.js`)
用于增强 51吃瓜网 (51cg1.com) 浏览体验的脚本。
#### 功能特性
##### 隐藏热搜帖子
- 一键隐藏所有带有"热搜 HOT"标签的帖子
- 同时自动隐藏广告帖子
- 开关状态会自动保存,刷新页面后保持设置
- 仅在列表页生效,不影响文章内容页的正常显示
##### 标记已读帖子
- 点击过的帖子会自动标记为已读状态
- 已读帖子视觉效果:
- 标题添加删除线
- 图片变为黑白色
- 整体透明度降低
- 自动管理已读记录:
- 最多保存 1000 条最近的已读记录
- 超过限制自动删除最旧的记录FIFO 策略)
- 无需手动清理
##### 控制面板
- 固定在页面右侧的控制面板
- 包含两个开关:
- 隐藏热搜:控制是否隐藏热搜和广告帖子
- 标记已读:控制是否启用已读标记功能
- 支持深色模式自动适配
#### 安装使用
1. 安装 Tampermonkey 扩展
2. 打开脚本文件 `51cg-helper.user.js`
3. 复制内容到 Tampermonkey 新建脚本
4. 保存并启用脚本
5. 访问 51cg1.com 即可看到右侧控制面板
---
### 2. Sort Jable Videos by Like Count (`sortbylikecount.js`)
按点赞数排序 Jable 视频的脚本。
---
## 通用安装方法
1. **安装 Tampermonkey 扩展**
- [Chrome](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo)
- [Firefox](https://addons.mozilla.org/firefox/addon/tampermonkey/)
- [Edge](https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd)
2. **安装脚本**
- 打开 Tampermonkey 管理面板
- 点击"添加新脚本"
- 复制对应脚本文件的内容
- 保存脚本Ctrl+S 或 Cmd+S
## 文件结构
```
tampermonkey-script/
├── README.md # 本文档
├── 51cg-helper.user.js # 51吃瓜网助手脚本
└── sortbylikecount.js # Jable 排序脚本
```

96
sortbylikecount.js Normal file
View File

@ -0,0 +1,96 @@
// ==UserScript==
// @name Sort Videos by Like Count
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Sort videos by like count on a specified website
// @author NoName
// @match https://jable.tv/*
// @match https://jable.tv
// @grant none
// ==/UserScript==
(function() {
'use strict';
// Function to extract the like count from the sub-title element
function getLikeCount(subTitleElement) {
if (subTitleElement) {
const textContent = subTitleElement.textContent.trim();
console.log('Full text content:', textContent);
// Extract all numbers from the text content
const numbers = textContent.match(/\d+/g);
if (numbers && numbers.length > 0) {
const likeCount = parseInt(numbers[numbers.length - 1].replace(/\s/g, ''), 10);
console.log('Parsed like count:', likeCount);
return isNaN(likeCount) ? 0 : likeCount;
}
}
return 0;
}
// Function to sort the videos within each parent div
function sortVideosInParent(parent) {
console.log('Sorting videos in parent:', parent);
const videoContainers = parent.querySelectorAll('.video-img-box');
const videos = Array.from(videoContainers).map(v => {
const subTitle = v.querySelector('.sub-title');
if (!subTitle) {
return null;
}
const vParent = v.parentElement; // 获取 v 的上一级 div
const likeCount = getLikeCount(subTitle);
console.log('Video element:', vParent, 'Like count:', likeCount);
return { element: vParent, likeCount: likeCount };
}).filter(video => video !== null);
videos.sort((a, b) => b.likeCount - a.likeCount);
videos.forEach(video => {
parent.appendChild(video.element);
});
}
// Function to find all parent divs and sort their child videos
function sortAllVideos() {
const videoContainers = document.querySelectorAll('.video-img-box');
const parents = new Set();
videoContainers.forEach(v => {
const parent = v.parentElement.parentElement;
console.log('Found parent:', parent);
parents.add(parent);
});
parents.forEach(parent => {
sortVideosInParent(parent);
});
}
// Add a sort button after the element with class "inactive-color fs-2 mb-0"
function addSortButton() {
const targetElement = document.querySelector('.inactive-color.fs-2.mb-0');
if (targetElement) {
const sortItem = document.createElement('li');
const sortLink = document.createElement('a');
sortLink.href = '#';
sortLink.textContent = '排序';
sortLink.style.marginLeft = '10px';
sortLink.addEventListener('click', function(event) {
event.preventDefault();
sortAllVideos();
});
sortItem.appendChild(sortLink);
targetElement.parentNode.insertBefore(sortItem, targetElement.nextSibling);
console.log('Sort button added after the target element');
}
}
// Run the sort function after the DOM content has loaded
window.addEventListener('load', function() {
addSortButton();
sortAllVideos();
})
})();