如题,其实大多数人的贴子内容也就是一句话的事,但标题总讲不清楚,站长能不能给贴子在主界面开个预览,主贴前一定字数(比如30字就行)直接显示在标题下面
3 Likes
用插件啊
Linux do增强插件,你值得拥有
这插件我装了,它这个预览和默认逻辑区别不大吧,也要手动点击打开和关闭,我希望的逻辑是主界面直接显示就可以,完全省略用户自己的操作
像置顶公告那种?
1 Like
对的,简洁省事
以前电脑版是所有帖子都支持的 后来不支持了
像我手机版现在是没有任何帖子会提供预览功能
1 Like
其实点开瞄一眼就回退也影响不大,预览功能会不会比较吃机器性能呢?
对的对的,教练我就想要这个
能不能梦回一下
该帖子仅回复后可见
// ==UserScript==
// @name Linux.do 话题自动预览(v2)
// @namespace https://tampermonkey.net/
// @version 2.0
// @description 自动显示话题首帖,支持懒加载、缓存、展开动画和清除缓存按钮!
// @author 星缘
// @match https://linux.do/*
// @grant GM_xmlhttpRequest
// @connect linux.do
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
const CACHE_EXPIRY_HOURS = 24;
// 插入清除缓存按钮
function insertClearCacheButton() {
const btn = document.createElement('button');
btn.textContent = '🧹 清除预览缓存';
btn.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
padding: 8px 12px;
font-size: 14px;
background: #444;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
opacity: 0.8;
`;
btn.onclick = () => {
Object.keys(localStorage).forEach(k => {
if (k.startsWith('preview_')) localStorage.removeItem(k);
});
alert('已清除所有话题预览缓存!请刷新页面查看效果~');
};
document.body.appendChild(btn);
}
// 获取话题ID
function getTopicId(href) {
const match = href.match(/\/t\/[^/]+\/(\d+)/);
return match ? match[1] : null;
}
// 插入预览内容 + 折叠按钮
function insertPreview(row, html) {
if (row.querySelector('.topic-preview')) return;
const wrapper = document.createElement('div');
wrapper.className = 'topic-preview';
wrapper.style.cssText = `
overflow: hidden;
transition: max-height 0.4s ease, opacity 0.4s ease;
background: #f7f7f7;
border: 1px solid #ddd;
border-radius: 5px;
margin-top: 8px;
padding: 10px;
max-height: 1000px;
opacity: 1;
`;
const closeBtn = document.createElement('button');
closeBtn.textContent = '收起预览';
closeBtn.style.cssText = `
float: right;
font-size: 12px;
margin-bottom: 8px;
background: #eee;
border: 1px solid #aaa;
border-radius: 3px;
cursor: pointer;
`;
closeBtn.onclick = () => {
wrapper.style.maxHeight = '0';
wrapper.style.opacity = '0';
setTimeout(() => wrapper.remove(), 400);
};
wrapper.innerHTML = html;
wrapper.prepend(closeBtn);
row.appendChild(wrapper);
}
// 缓存读取与写入
function getCachedPreview(topicId) {
const cache = localStorage.getItem(`preview_${topicId}`);
if (!cache) return null;
const { html, timestamp } = JSON.parse(cache);
const age = (Date.now() - timestamp) / (1000 * 60 * 60);
return age < CACHE_EXPIRY_HOURS ? html : null;
}
function setCachedPreview(topicId, html) {
localStorage.setItem(`preview_${topicId}`, JSON.stringify({
html,
timestamp: Date.now()
}));
}
// 请求 JSON 数据并渲染
function fetchAndInsertPreview(row, topicId) {
const cached = getCachedPreview(topicId);
if (cached) {
insertPreview(row, cached);
return;
}
const url = `https://linux.do/t/${topicId}.json`;
GM_xmlhttpRequest({
method: 'GET',
url,
onload: res => {
try {
const json = JSON.parse(res.responseText);
const cooked = json.post_stream.posts[0].cooked;
insertPreview(row, cooked);
setCachedPreview(topicId, cooked);
} catch (err) {
console.error('预览加载失败', err);
}
}
});
}
// 懒加载处理器
const observer = new IntersectionObserver(entries => {
for (const entry of entries) {
if (entry.isIntersecting) {
const row = entry.target;
const link = row.querySelector('a.title');
if (link && link.href) {
const topicId = getTopicId(link.href);
if (topicId) {
observer.unobserve(row);
fetchAndInsertPreview(row, topicId);
}
}
}
}
}, {
rootMargin: '150px',
threshold: 0.1
});
function scanTopics() {
const rows = document.querySelectorAll('.topic-list tr:not(.preview-observed)');
rows.forEach(row => {
const link = row.querySelector('a.title');
if (link && link.href) {
row.classList.add('preview-observed');
observer.observe(row);
}
});
}
const mutationObserver = new MutationObserver(scanTopics);
mutationObserver.observe(document.body, { childList: true, subtree: true });
scanTopics();
insertClearCacheButton();
})();
闲的没事加了一个小动画 可以试毒一下
我这边手机测试没什么问题 目前的逻辑是直接把第一个帖子的内容给你展示出来 相当于直接在主界面刷所有帖子~
如果需要前30字预览的话可以改一下下~
预览的缓存是一整天 如果不想看了可以清除
动画好像没有成功展示 问题不大
3 Likes
性能不是问题吧,毕竟每页显示数量都会限制的;对于标题党的贴子点开总感觉有点亏
说的就是我
// ==UserScript==
// @name Linux do 在话题列表下方显示话题内容
// @namespace http://tampermonkey.net/
// @version 1.5
// @description 在话题/搜索列表下方显示话题内容,点击重新加载内容开始加载
// @match https://linux.do/*
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-end
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js
// ==/UserScript==
(function () {
'use strict';
require("discourse/routes/topic").default.disableReplaceState = true;
var theDate = getFormattedDate();
var today = new Date();
var yesterday = theDate[1];
//加载摘要的间隔事件, 单位毫秒
var waitTime = 2000;
//新增内容
var addDiv = '<div is_insert="1" style="background: #C6FFC6;width: 60vw;"></div>';
const linuxDo = "linux.do";
//加载的内容
var contentCache = {};
var yesterdayCache = {};
var totalCache = {};
//保存的数据
var linuxDoData = {};
function init() {
console.log('初始化数据')
linuxDoData = GM_getValue(linuxDo, {});
Object.keys(linuxDoData).forEach(dateKey => {
const keyDate = new Date(dateKey);
const diffDays = Math.floor((today - keyDate) / (1000 * 60 * 60 * 24));
if (diffDays > 3) {
delete linuxDoData[dateKey];
} else {
linuxDoData[dateKey] = linuxDoData[dateKey] || {};
Object.assign(totalCache, linuxDoData[dateKey]);
}
});
// console.log('totalCache', totalCache)
}
function finish() {
linuxDoData[theDate[0]] = linuxDoData[theDate[0]] || {};
Object.assign(linuxDoData[theDate[0]], contentCache);
GM_setValue(linuxDo, linuxDoData);
}
async function getPreviewContent(topicId) {
try {
// Check if content is already cached
if (totalCache[topicId]) {
//console.log("Content found in cache:", contentCache[linkHref]);
return totalCache[topicId];
}
let url = `https://linux.do/t/topic/${topicId}.json`;
//console.log("linkHref url: " + linkHref);
// Fetch content from the link URL
var response = await fetch(url);
if (!response.ok) {
throw new Error("Network response was not ok");
}
let jsonData = await response.json();
let cookedContent = jsonData.post_stream.posts[0].cooked;
if (cookedContent != null) {
contentCache[topicId] = cookedContent;
totalCache[topicId] = cookedContent;
await sleep(waitTime);
}
// Return the extracted cooked content
return cookedContent;
} catch (error) {
console.error("Error fetching link preview content:", error);
return null;
}
}
function ascPost() {
// 获取当前页面的URL, 加上升序排列
let currentUrl = window.location.href;
// 判断URL是否已经包含?ascending=true
if (!currentUrl.includes('?ascending=true')) {
// 如果URL已经有其他参数,使用&符号添加新的参数
if (currentUrl.includes('?')) {
currentUrl += '&ascending=true';
} else {
// 如果URL没有任何参数,使用?符号添加新的参数
currentUrl += '?ascending=true';
}
// 重新加载页面,并且包含新的URL
window.location.href = currentUrl;
}
}
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function work() {
// 查找所有的<tr>标签,并筛选出有data-topic-id属性的标签
const rows = $('tr[data-topic-id]');
if (rows && rows.length === 0) return;
init();
try {
for (const tr of rows) {
let $tr = $(tr);
// 判断是否是隐藏的
if ($tr.is(':hidden')) {
continue;
}
let topicId = $tr.attr('data-topic-id');
let cookedContent = await getPreviewContent(topicId);
//console.log("url: " + url, "cookedContent:" + cookedContent);
if (cookedContent) {
//数据是否已插入
var nextTr = $tr.next('tr')
if (nextTr && nextTr.find('div[is_insert="1"]').length > 0) {
continue;
}
// 在<tr>标签最后插入新的<div>
//$tr.appendChild(newCell1);
// 在新的 tr 中插入包含 cookedContent 的 div
let $newRow = $('<tr></tr>');
let $newDiv = $(addDiv); // 创建新的 div 元素
$newDiv.html(cookedContent); // 设置 div 的 HTML 内容
$newRow.append($newDiv);
$tr.after($newRow);
}
}
} catch (error) {
console.error('处理行时出错:', error);
}
finish()
}
async function workSearchData() {
// 查找 class="fps-result-entries" 的 div 标签
const fpsResultEntries = document.querySelector('.fps-result-entries');
if (!fpsResultEntries) return;
// 遍历下面 role="listitem" 的 div
const listItems = fpsResultEntries.querySelectorAll('div[role="listitem"]');
//判断是否有数据
if (listItems && listItems.length === 0) return;
init();
for (let divItem of listItems) {
// 判断是否是隐藏的
if ($(divItem).is(':hidden')) {
continue;
}
// 从 divItem 中获取 href="/t/topic/数字" 的 a 标签
const aTag = divItem.querySelector('a[href^="/t/topic/"]');
if (aTag) {
// 取 href 中的数字值为 topicId
const topicId = aTag.getAttribute('href').match(/\/t\/topic\/(\d+)/)[1];
//console.log('url', url)
const cookedContent = await getPreviewContent(topicId);
// 移除 divItem 中 class="blurb container" 的 div
const blurbContainer = divItem.querySelector('.blurb.container');
if (cookedContent && blurbContainer) {
var $listItem = $(divItem);
// 查找 is_insert="1" 的 div 元素
var targetDiv = $listItem.find('div[is_insert="1"]');
// 检查是否已插入
if (targetDiv.length > 0) {
continue
}
// 将 blurbContainer 的内容替换为 cookedContent
let $newDiv = $(addDiv); // 创建新的 div 元素
$newDiv.html(cookedContent); // 设置 div 的 HTML 内容
$(blurbContainer).after($newDiv);
}
}
}
finish()
}
// 添加 Tampermonkey 菜单按钮
GM_registerMenuCommand("滚动到最后未看贴", async function () {
let currentUrl = window.location.href;
if (currentUrl === 'https://linux.do/latest?order=created') {
if (Object.keys(totalCache).length === 0) {
init();
if (Object.keys(totalCache).length === 0) {
console.log('没有历史浏览数据');
return;
}
}
// 获取最新的日期键
// 获取最新的日期键
let dateKeys = Object.keys(totalCache).sort();
// 获取最后一个已读帖子的ID
let lastReadTopicId = dateKeys[dateKeys.length - 1];
console.log('最后一个已读帖子的ID', lastReadTopicId);
while(1){
let rows = $('tr[data-topic-id]');
if (rows.length == 0) {
console.log('获取帖子失败');
break;
}
let lastTr = rows.last()[0];
let currentTopicId = parseInt($(lastTr).attr('data-topic-id'));
if(currentTopicId > lastReadTopicId){
lastTr.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(2000);
} else {
console.log('滑动到最后一个没有看的贴');
console.log('隐藏已看贴');
toggleHidePosts(true)
await sleep(2000);
console.log('加载内容');
work();
break;
}
}
}
});
GM_registerMenuCommand("重新加载内容", function () {
let currentUrl = window.location.href;
if (currentUrl.includes('/search?')) {
workSearchData()
} else {
work();
}
});
// 添加 Tampermonkey 菜单按钮
GM_registerMenuCommand("升序排序", function () {
ascPost();
});
// 添加 Tampermonkey 菜单按钮
GM_registerMenuCommand("搜索当日帖子", function () {
var today = new Date();
var toDayDate = formatDate(today)
today.setDate(today.getDate() + 1);
var beforeDate = formatDate(today)
var url = 'https://linux.do/search?q=after:' + toDayDate + ' before:' + beforeDate + ' in:first order:oldest -tags:KFC,刺猬';
window.location.href = url;
});
GM_registerMenuCommand("隐藏已加载帖子", () => toggleHidePosts(true));
GM_registerMenuCommand("显示已加载帖子", () => toggleHidePosts(false));
GM_registerMenuCommand("只隐藏上次隐藏帖子", () => onlyHidePosts());
function toggleHidePosts(isHidden) {
if (Object.keys(totalCache).length == 0) {
init()
if (Object.keys(totalCache).length == 0) {
console.log('没数据')
return;
}
}
console.log('totalCache', Object.keys(totalCache).length)
const href = window.location.href;
const selector = href.includes('/search?') ?
$('.fps-result-entries').find('div[role="listitem"]') :
$('tr[data-topic-id]');
// 记录隐藏的帖子
var hidePosts = []
selector.each(function () {
let $this = $(this);
let topicId;
// 根据不同的容器获取topicId
if ($this.is('div')) {
topicId = $this.find('div[data-topic-id]').first().attr('data-topic-id');
} else if ($this.is('tr')) {
topicId = $this.attr('data-topic-id');
}
//console.log(topicId, totalCache[topicId])
if (topicId && totalCache[topicId]) {
if (isHidden) {
hidePosts.push(topicId)
}
$this[isHidden ? 'hide' : 'show']();
if ($this.is('tr')) {
$this.next('tr')[isHidden ? 'hide' : 'show']();
}
}
});
if (isHidden) {
GM_setValue('hide', hidePosts);
}
}
function onlyHidePosts() {
if (Object.keys(totalCache).length == 0) {
init()
if (Object.keys(totalCache).length == 0) {
console.log('没数据')
return;
}
}
//console.log('linuxDoData', linuxDoData)
const href = window.location.href;
const selector = href.includes('/search?') ?
$('.fps-result-entries').find('div[role="listitem"]') :
$('tr[data-topic-id]');
// 记录隐藏的帖子
var hidePosts = GM_getValue('hide', []);
// console.log('hide', hidePosts)
selector.each(function () {
let $this = $(this);
let topicId;
// 根据不同的容器获取topicId
if ($this.is('div')) {
topicId = $this.find('div[data-topic-id]').first().attr('data-topic-id');
} else if ($this.is('tr')) {
topicId = $this.attr('data-topic-id');
}
if (topicId && totalCache[topicId] && hidePosts.includes(topicId)) {
$this.hide();
if ($this.is('tr')) {
$this.next('tr').hide();
}
}
});
}
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/*
// 页面加载完成后执行
$(document).ready(function () {
if (window.location.href.includes('linux.do/new')) {
work();
}
});
*/
function getFormattedDate() {
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
// 获取昨天的日期
const yesterday = new Date(date);
yesterday.setDate(date.getDate() - 1);
const yYear = yesterday.getFullYear();
const yMonth = String(yesterday.getMonth() + 1).padStart(2, '0');
const yDay = String(yesterday.getDate()).padStart(2, '0');
const beforeDate = `${yYear}-${yMonth}-${yDay}`;
return [`${year}-${month}-${day}`, beforeDate];
}
})();
搞的油猴脚本, 配合下面的
// ==UserScript==
// @name linux.do多功能脚本
// @namespace
// @version 2024-10-01
// @description 1. 查看/不看回复 2. 展开/折叠回复 3. 显示楼层 4. 禁用视频自动播放 5. 预览界面
// @author Jason+马克思+神奇的哆啦z梦
// @match https://linux.do/*
// @match https://meta.appinn.net/t/topic/*
// @icon
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @license MIT
// ==/UserScript==
(function () {
'use strict';
//by Jason
// 简化对localStorage的访问
const storage = {
set: (key, value) => window.localStorage.setItem(key, value),
get: (key, defaultValue) => window.localStorage.getItem(key) || defaultValue,
};
GM_registerMenuCommand("查看回复", () => createToggleButton("off"));
GM_registerMenuCommand("不看回复", () => createToggleButton("on"));
// 创建并配置按钮
function createToggleButton(status) {
storage.set("hide_replies", status);
toggleRepliesVisibility(1); //立即应用显示设置
/**
if (document.getElementById("toggleRepliesVisibilityBtn")) {
// 如果按钮已存在,则无需重复创建
return;
}
const btn = document.createElement("button");
btn.id = "toggleRepliesVisibilityBtn";
btn.textContent = storage.get("hide_replies", "off") === "on" ? '查看回复' : '不看回复';
btn.onclick = function() {
const currentState = storage.get("hide_replies", "off");
const newState = currentState === "on" ? "off" : "on";
btn.textContent = newState === "on" ? '查看回复' : '不看回复';
storage.set("hide_replies", newState);
toggleRepliesVisibility(); //立即应用显示设置
};
// 设置按钮样式
// btn.style.backgroundColor = "#555";
// btn.style.color = "#FFF";
// btn.style.border = "none";
// btn.style.padding = "10px 20px";
// btn.style.margin = "10px";
// btn.style.borderRadius = "5px";
// btn.style.cursor = "pointer";
btn.className = "btn btn-icon-text btn-default";
// 添加按钮到页面特定位置
const controlsContainer = document.querySelector(".timeline-footer-controls");
if (controlsContainer) {
controlsContainer.appendChild(btn);
}
*/
}
var replylength = 0
// 根据设置隐藏或显示回复帖子
function toggleRepliesVisibility(isOprate = 0) {
const isHidden = storage.get("hide_replies", "off") === "on";
const posts = document.querySelectorAll(".topic-post");
if (replylength != posts.length || isOprate) {
posts.forEach(post => {
//当前楼是否是回复其他人
const hasReply = post.querySelector(".reply-to-tab") !== null;
//是否有回复
const hasReply2 = post.querySelector("nav.post-controls .show-replies") !== null;
post.style.display = isHidden && hasReply && !hasReply2 ? 'none' : '';
// console.log("隐藏或显示回复帖子:", posts.length);
});
//显示楼层
showFloor(posts)
}
replylength = posts.length;
}
//by linux.do增加插件
GM_registerMenuCommand("展开回复", () => toggleReplyVisibility("on"));
GM_registerMenuCommand("折叠回复", () => toggleReplyVisibility("off"));
function toggleReplyVisibility(status) {
storage.set("collapse_replies", status);
}
const replyButtonSelector = "nav.post-controls .show-replies";
var pollinglength2 = 0
function clickReplyButtons() {
const isCollapse = storage.get("collapse_replies", "off") === "on";
if (isCollapse) {
var le = document.querySelectorAll(".post-stream .topic-post");
if (pollinglength2 != le.length) {
pollinglength2 = le.length;
const replyButtons = document.querySelectorAll(replyButtonSelector);
replyButtons.forEach(button => {
button.click(); // 模拟点击事件
// console.log("点击了回复按钮:", replyButtons.length);
});
}
}
}
//禁用自动播放
function disableAutoPlay() {
// console.log("禁用自动播放搜索调用");
document.querySelectorAll('iframe, video').forEach((element) => {
// console.log("禁用自动播放搜索:", element.length);
let src = element.getAttribute('src');
// 检查 src 是否存在
if (src) {
// 检查是否已有 autoplay=false
if (!src.includes('autoplay=false')) {
// 检查是否已有 autoplay 参数
if (src.includes('autoplay=')) {
// 如果存在,替换为 autoplay=false
src = src.replace(/autoplay=[^&]*/, 'autoplay=false');
} else {
// 如果不存在,添加 autoplay=false
const separator = src.includes('?') ? '&' : '?';
src += `${separator}autoplay=false`;
}
// 更新 src 属性
element.setAttribute('src', src);
}
}
// 针对 video 标签设置 autoplay 属性为 false
if (element.tagName.toLowerCase() === 'video' && element.autoplay) {
element.autoplay = false;
}
});
}
//显示楼层
function showFloor(posts) {
// console.log("显示楼层搜索调用");
posts.forEach(post => {
const article = post.querySelector('article');
if (article) {
const num = article.id.replace(/^post_/, ""); // 获取文章的ID并去掉 "post_"
// 如果尚未添加楼层标识,添加到 post-infos 中
if (!post.querySelector('.linuxfloor')) {
const postInfos = post.querySelector('.post-infos');
if (postInfos) {
const floorSpan = document.createElement('span');
floorSpan.className = 'linuxfloor';
floorSpan.textContent = `#${num}`;
postInfos.appendChild(floorSpan);
}
}
}
});
}
//预览帖子
let previousTopicCount = 0;
// let previousPostCount = 0;
function previewTopic() {
// console.log("预览帖子搜索调用");
let trs = document.querySelectorAll(".topic-list-body tr")
const topicCount = trs.length;
// const postCount = document.querySelectorAll(".post-stream .topic-post").length;
if (topicCount !== previousTopicCount) {
previousTopicCount = topicCount;
//创建背景
previousTopic();
setClick();
//过滤帖子
filterTopic(trs);
}
// if (postCount !== previousPostCount) {
// previousPostCount = postCount;
// setClick();
// }
}
//过滤帖子
let filterTexts = GM_getValue('filterTitles', []);
let filterUsers = GM_getValue('filterUsers', []); // 新增屏蔽发帖人
function filterTopic(trs) {
trs.forEach((tr, index) => {
if (tr.style.display === 'none') return; // Skip if already hidden
let isFilter = false;
let aDiv = tr.querySelector(".title");
// Filter titles
if (aDiv) {
let title = aDiv.textContent.toLowerCase();
isFilter = filterTexts.some(text => title.includes(text.toLowerCase()));
}
// Filter users
if (!isFilter) {
let posterLink = tr.querySelector(".posters a");
let username = posterLink ? posterLink.getAttribute("href").split("/u/")[1] : "";
if (username) {
isFilter = filterUsers.some(user => username.toLowerCase() === user.toLowerCase());
}
}
// Hide or show based on filtering
tr.style.display = isFilter ? (index !== trs.length - 1 ? 'none' : '') : '';
});
}
// 添加设置屏蔽菜单
GM_registerMenuCommand("设置屏蔽", showFilterSettings);
// 添加设置弹框相关函数
function showFilterSettings() {
const dialog = document.createElement('div');
dialog.className = 'filter-settings-dialog';
dialog.innerHTML = `
<div class="filter-settings-content">
<h3>屏蔽设置</h3>
<div class="filter-section">
<label>屏蔽标题(每行一个):</label>
<textarea id="filterTitles">${filterTexts.join('\n')}</textarea>
</div>
<div class="filter-section">
<label>屏蔽用户(每行一个):</label>
<textarea id="filterUsers">${filterUsers.join('\n')}</textarea>
</div>
<div class="filter-buttons">
<button id="saveFilters">保存</button>
<button id="cancelFilters">取消</button>
</div>
</div>
`;
document.body.appendChild(dialog);
// 绑定按钮事件
document.getElementById('saveFilters').onclick = () => {
const newTitles = document.getElementById('filterTitles').value.split('\n').filter(x => x.trim());
const newUsers = document.getElementById('filterUsers').value.split('\n').filter(x => x.trim());
GM_setValue('filterTitles', newTitles);
GM_setValue('filterUsers', newUsers);
filterTexts = newTitles;
filterUsers = newUsers;
document.body.removeChild(dialog);
// 重新过滤当前页面
const trs = document.querySelectorAll(".topic-list-body tr");
filterTopic(trs);
};
document.getElementById('cancelFilters').onclick = () => {
document.body.removeChild(dialog);
};
}
// 修改过小的内容
function sizeMotified() {
document.querySelectorAll("span").forEach((e) => {
let style = window.getComputedStyle(e);
let fontSize = parseFloat(style.fontSize);
// 如果 font-size 低于75%, 则将其设置为75%
if (fontSize < 16 * 0.75) { // 假设默认字体大小为16px
e.style.fontSize = "75%";
e.style.color = "grey";
}
});
};
//添加上/下一贴按钮
function addPrevNextBtn() {
// 获取标题元素
const titleElement = document.querySelector('.title');
// 判断是否有上/下一贴按钮
if (document.querySelector('#myprev-topic-btn') === null) {
// 创建上一贴按钮
const prevButton = document.createElement('button');
prevButton.textContent = '上一贴';
prevButton.className = 'btn btn-prev';
prevButton.id = 'myprev-topic-btn';
prevButton.onclick = function () {
prevNextTopic(true);
};
// 将按钮插入到标题元素右边
titleElement.appendChild(prevButton);
}
if (document.querySelector('#mynext-topic-btn') === null) {
// 创建下一贴按钮
const nextButton = document.createElement('button');
nextButton.textContent = '下一贴';
nextButton.className = 'btn btn-next';
nextButton.id = 'mynext-topic-btn';
nextButton.onclick = function () {
prevNextTopic(false);
};
// 将按钮插入到标题元素右边
titleElement.appendChild(nextButton);
}
navigationBarHeight = getNavigationBarHeight();
}
//上一贴下一贴
function prevNextTopic(isUp) {
// 确定方向,isUp为true表示向上,-1代表上一个,1代表下一个
const direction = isUp ? -1 : 1;
// 获取所有topic行的列表
const trs = document.querySelectorAll(".topic-list-body tr");
const emberList = Array.from(trs).filter(tr => {
return tr.style.display !== 'none'; // 过滤掉 display: none; 的行
});
// 查找第一个出现在视口中的元素
const currentElement = emberList.find(tr => isElementPartiallyInViewport(tr));
if (!currentElement) {
return;
}
// 获取当前元素的索引位置
const currentIndex = emberList.indexOf(currentElement);
// 获取下一个或上一个元素的索引
let nextIndex = currentIndex + direction;
// 如果下一个元素存在且需要滚动,找到不是"data-topic-id"前缀的元素
while (nextIndex >= 0 && nextIndex < emberList.length && !emberList[nextIndex].hasAttribute("data-topic-id")) {
nextIndex += direction;
}
// 如果下一个合法元素存在,则进行滚动
if (nextIndex >= 0 && nextIndex < emberList.length) {
const nextElement = emberList[nextIndex];
window.scrollBy(0, nextElement.getBoundingClientRect().top - navigationBarHeight);
}
}
//获取导航栏高度
function getNavigationBarHeight() {
return document.querySelector("#ember3 > div.drop-down-mode.d-header-wrap > header > div > div").offsetHeight;
}
let navigationBarHeight = 0;
//判断元素是否在可视范围内
function isElementPartiallyInViewport(el) {
const rect = el.getBoundingClientRect();
const partiallyInViewport = (
rect.top < (window.innerHeight || document.documentElement.clientHeight) &&
rect.bottom > navigationBarHeight + 5
);
return partiallyInViewport;
}
// 监听页面变化来重新应用显示设置
function observePageChanges() {
const observer = new MutationObserver((mutations) => {
//判断当前页面是否是帖子页面
const url = window.location.href;
//帖子内
if (url.indexOf("linux.do/t/topic/") != -1 || url.indexOf("meta.appinn.net/t/topic/") != -1) {
//是否显示回复的楼
toggleRepliesVisibility();
// 是否展开回复
clickReplyButtons();
//禁用自动播放
disableAutoPlay();
//修改过小的内容
//sizeMotified()
} else if (url.indexOf("linux.do/search") != -1 || url.indexOf("meta.appinn.net/search") != -1) {
} else {
previewTopic()
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
// 初始化脚本
function init() {
if (document.readyState === 'complete') {
observePageChanges();
toggleRepliesVisibility(); // 初始应用显示设置
} else {
window.addEventListener('load', () => {
observePageChanges();
toggleRepliesVisibility();
});
}
}
init();
//by 马克思
//按帖子创建时间排序
function waitForLoad(callback) {
var observer = new MutationObserver(function (mutations) {
if (document.readyState === 'complete') {
observer.disconnect();
callback();
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
}
// 等待页面加载完成
waitForLoad(function () {
modifyNavigationBar();
observeDocumentChanges();
});
function modifyNavigationBar() {
var navigationBar = document.querySelector('ul#navigation-bar');
if (navigationBar) {
var navigationItems = navigationBar.querySelectorAll('li');
navigationItems.forEach(function (item) {
var anchor = item.querySelector('a');
if (anchor && anchor.textContent.trim() === "最新") {
anchor.textContent = "新回复";
var newestCreatedElement = document.createElement('li');
newestCreatedElement.title = "新发的帖子";
newestCreatedElement.id = "ember999";
newestCreatedElement.className = "active latest_created ember-view nav-item_latest_created";
newestCreatedElement.innerHTML = '<a href="' + anchor.getAttribute('href') + '?order=created" pcked="1">新创建</a>';
item.insertAdjacentElement('afterend', newestCreatedElement);
}
});
//添加上/下一贴按钮
addPrevNextBtn();
}
}
function observeDocumentChanges() {
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.type === 'childList' && mutation.target.id === 'navigation-bar') {
modifyNavigationBar();
}
});
});
observer.observe(document.documentElement, { childList: true, subtree: true });
}
// 初始化话题预览界面
function previousTopic() {
if (document.querySelector(".topicpreview") === null) {
const topicPreviewDiv = document.createElement("div");
topicPreviewDiv.className = "topicpreview";
topicPreviewDiv.style.display = 'none'; // 隐藏初始预览窗口
topicPreviewDiv.innerHTML = `
<div class="topicpreview-opacity"></div>
<div class="topicpreview-container">
<p style="text-align: center">正在加载中...</p>
</div>
`;
document.body.appendChild(topicPreviewDiv);
}
// 为每个话题添加预览按钮
document.querySelectorAll(".topic-list .main-link a.title").forEach((element) => {
const id = element.getAttribute("data-topic-id");
const parent = element.closest(".link-top-line");
if (parent && parent.querySelector(".topicpreview-btn") === null) {
const button = document.createElement("button");
button.className = "btn btn-icon-text btn-default topicpreview-btn";
button.setAttribute("data-id", id);
button.textContent = "预览";
parent.appendChild(button);
button.addEventListener("click", function () {
console.log("点击预览按钮:", this.getAttribute("data-id"));
const previewContainer = document.querySelector(".topicpreview-container");
const previewOverlay = document.querySelector(".topicpreview");
previewOverlay.style.display = "block"; // 显示预览弹窗;
const previewId = this.getAttribute("data-id");
fetch(`/t/${previewId}.json`)
.then((response) => response.json())
.then((data) => {
const previewData = data;
// 更新预览窗口内容
previewContainer.innerHTML = `
<div class="topicpreview-title">${previewData.title}</div>
<p class="topicpreview-date">发帖时间:${formatDate(previewData.created_at)}</p>
<div class="topicpreview-content"></div>
<p style="text-align: center;">仅显示前 20 条,<a href="/t/topic/${previewId}/">查看更多</a></p>
`;
// 显示每个帖子
previewData.post_stream.posts.forEach((post, index) => {
const postElement = document.createElement("div");
postElement.className = "item";
postElement.innerHTML = `
<span class="itemfloor">${index + 1}楼</span>
<div class="itempost">
<div class="itemname">
${post.display_username} <span>${post.username}</span>
<div class="itemdate">${formatDate(post.created_at)}</div>
</div>
${post.cooked}
</div>
`;
document.querySelector(".topicpreview .topicpreview-content").appendChild(postElement);
});
// 防止图片点击打开 lightbox
setInterval(() => {
document.querySelectorAll(".lightbox").forEach((el) => {
el.href = "javascript:void(0)";
});
}, 3000);
});
});
}
});
}
// 格式化时间函数
function formatDate(isoString) {
const date = new Date(isoString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
// 设置按钮点击事件,显示预览弹窗
function setClick() {
// 点击背景关闭预览
document.querySelector(".topicpreview-opacity").addEventListener("click", function () {
document.querySelector(".topicpreview").style.display = "none";
document.querySelector(".topicpreview-container").innerHTML = `<p style="text-align: center">正在加载中...</p>`;
});
}
GM_addStyle(`
.topicpreview-btn {
padding: 4px 12px !important;
font-size: 14px !important;
opacity: 0 !important
}
.topic-list-item:hover .topicpreview-btn {
opacity: 1 !important;
}
.topicpreview {
position: fixed;
top: 0;
left: 0;
z-index: 99999;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
display: none;
.topicpreview-container {
padding: 30px 0;
border-radius: 5px;
width: 100%;
max-width: 800px;
overflow-y: auto;
height: 80vh;
z-index: 10;
background: var(--header_background);
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
.topicpreview-title {
font-size: 22px;
font-weight: 600;
padding: 0 30px;
}
.topicpreview-date {
padding: 0 30px;
color: #666;
}
.topicpreview-content {
&>.item {
display: flex;
align-items: flex-start;
padding: 20px 30px;
.itemfloor {
width: 50px;
text-align: left;
font-size: 16px;
padding-top: 15px;
color: #25b4cf;
}
.itempost {
flex: 1;
background: var(--tertiary-low);
padding: 15px 15px;
border-radius: 10px;
font-size: 15px;
word-break: break-all;
// color: #666;
pre code {
max-width: 620px;
}
img {
max-width: 100%;
max-height: 100%;
height: auto;
}
.itemname {
font-size: 16px;
color: #8f3a3a;
display: flex;
justify-content: space-between;
align-items: center;
span {
color: #9e9e9e;
margin-left: 20px;
}
}
.itemdate {
color: #b9b9b9;
font-size: 16px;
margin-left: auto;
}
}
}
}
}
}
.topicpreview-opacity {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 1;
background: rgba(0, 0, 0, .6);
z-index: 9;
}
`);
// 添加样式
GM_addStyle(`
.filter-settings-dialog {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 10000;
display: flex;
justify-content: center;
align-items: center;
}
.filter-settings-content {
background: var(--header_background);
padding: 20px;
border-radius: 8px;
min-width: 400px;
}
.filter-section {
margin: 15px 0;
}
.filter-section label {
display: block;
margin-bottom: 5px;
}
.filter-section textarea {
width: 100%;
height: 100px;
margin-bottom: 10px;
}
.filter-buttons {
text-align: right;
}
.filter-buttons button {
margin-left: 10px;
padding: 5px 15px;
}
`);
})();
可以配合这个一起用
2 Likes
不知道为啥加载首贴不可用,清除预览缓存按钮倒是出来了
长贴太占地方了,页面布局也会变乱,我试试能不能调到更好点
1 Like
我也有了
本来是前30字预览的 感觉好像出了点问题 缓存现在 刷新就清除
动画修复了一下
我感觉可以改为弹窗
// ==UserScript==
// @name Linux.do 话题自动预览(v3)
// @namespace https://tampermonkey.net/
// @version 3.0
// @description 展示话题首段或前30字摘要,SVG加载动画,点击展开全文,无缓存策略!
// @author 星缘
// @match https://linux.do/*
// @grant GM_xmlhttpRequest
// @connect linux.do
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
// 提取首段或前30字
function getShortPreview(cookedHTML, maxLen = 30) {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = cookedHTML;
const firstP = tempDiv.querySelector('p');
const raw = firstP?.textContent?.trim() || tempDiv.textContent?.trim() || '';
return raw.slice(0, maxLen) + '...';
}
// SVG 加载动画
function createLoadingSpinner() {
const wrap = document.createElement('div');
wrap.innerHTML = `
<div style="text-align:center; padding:8px;">
<svg width="24" height="24" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="35" stroke="#999" stroke-width="10" fill="none" stroke-linecap="round">
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite"
dur="1s" values="0 50 50;360 50 50" keyTimes="0;1"/>
</circle>
</svg>
</div>
`;
return wrap;
}
// 插入预览(初始摘要 + 展开按钮)
function insertShortPreview(row, cookedHTML) {
if (row.querySelector('.topic-preview')) return;
const preview = document.createElement('div');
preview.className = 'topic-preview';
preview.style.cssText = `
background:#f7f7f7; border:1px solid #ddd; border-radius:5px;
margin-top:8px; padding:10px; font-size:14px; line-height:1.5;
`;
const summary = getShortPreview(cookedHTML);
const shortP = document.createElement('div');
shortP.textContent = summary;
const expandBtn = document.createElement('button');
expandBtn.textContent = '展开全文';
expandBtn.style.cssText = `
display:inline-block; margin-top:8px; font-size:12px;
background:#eee; border:1px solid #aaa; border-radius:3px; cursor:pointer;
`;
expandBtn.onclick = () => {
preview.innerHTML = cookedHTML;
};
preview.appendChild(shortP);
preview.appendChild(expandBtn);
row.appendChild(preview);
}
// 异步加载 JSON 并插入
function fetchAndInsert(row, topicId) {
const spinner = createLoadingSpinner();
row.appendChild(spinner);
const url = `https://linux.do/t/${topicId}.json`;
GM_xmlhttpRequest({
method: 'GET',
url,
onload: (res) => {
row.removeChild(spinner);
try {
const json = JSON.parse(res.responseText);
const cooked = json.post_stream.posts[0].cooked;
insertShortPreview(row, cooked);
} catch (e) {
console.error('解析失败', e);
}
},
onerror: () => {
row.removeChild(spinner);
console.error('加载失败');
}
});
}
// 获取话题ID
function getTopicId(href) {
const m = href.match(/\/t\/[^/]+\/(\d+)/);
return m ? m[1] : null;
}
// 懒加载
const observer = new IntersectionObserver(entries => {
for (const entry of entries) {
if (entry.isIntersecting) {
const row = entry.target;
const link = row.querySelector('a.title');
if (link && link.href) {
const id = getTopicId(link.href);
if (id) {
observer.unobserve(row);
fetchAndInsert(row, id);
}
}
}
}
}, {
rootMargin: '100px',
threshold: 0.1
});
// 扫描话题列表
function scan() {
const rows = document.querySelectorAll('.topic-list tr:not(.preview-bound)');
rows.forEach(row => {
const link = row.querySelector('a.title');
if (!link) return;
row.classList.add('preview-bound');
observer.observe(row);
});
}
const mo = new MutationObserver(scan);
mo.observe(document.body, { childList: true, subtree: true });
scan(); // 初始执行
})();
1 Like
我能看到那个加载的圈圈动画,不过还是不显示摘要内容,应该是我浏览器这里有问题(之前还有其他 bug 还没修复),我再琢磨下
弹窗式来了 应该算是全网首创(反正我没搜索 )
我的测试浏览器是x浏览器
但是还是强调我是手机w
// ==UserScript==
// @name Linux.do 话题预览卡片 v4.2
// @namespace https://tampermonkey.net/
// @version 4.2
// @description 精准50字摘要,保留HTML格式,支持暗色主题,视觉美化与动画优化!
// @author 星缘
// @match https://linux.do/*
// @grant GM_xmlhttpRequest
// @connect linux.do
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
const PREVIEW_LENGTH = 50;
const style = document.createElement('style');
style.textContent = `
.topic-preview-card {
position: relative;
margin-top: 8px;
padding: 12px;
border-radius: 8px;
border: 1px solid;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
font-size: 14px;
line-height: 1.6;
max-width: 600px;
width: 90%;
transition: all 0.3s ease;
opacity: 0;
transform: translateY(10px);
animation: fadeInUp 0.4s forwards;
z-index: 99;
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
.topic-preview-dark {
background: #1e1e1e;
color: #ddd;
border-color: #444;
}
.topic-preview-dark a { color: #80bfff; }
.topic-preview-light a { color: #0074d9; }
.topic-preview-light {
background: #fff;
color: #333;
border-color: #ccc;
}
.topic-preview-meta {
font-size: 12px;
margin-bottom: 6px;
opacity: 0.8;
}
.topic-preview-btn {
display: inline-block;
margin-top: 10px;
padding: 4px 8px;
font-size: 12px;
border: 1px solid #999;
background: transparent;
border-radius: 4px;
cursor: pointer;
}
.topic-loading-spinner {
text-align: center;
padding: 8px;
}
.topic-loading-spinner svg {
width: 24px;
height: 24px;
animation: rotate 1s linear infinite;
}
@keyframes rotate {
0% {transform: rotate(0deg);}
100% {transform: rotate(360deg);}
}
`;
document.head.appendChild(style);
function isDarkMode() {
return document.documentElement.getAttribute('data-theme') === 'dark'
|| document.body.className.includes('dark');
}
const darkClass = () => isDarkMode() ? 'topic-preview-dark' : 'topic-preview-light';
function createSpinner() {
const div = document.createElement('div');
div.className = 'topic-loading-spinner';
div.innerHTML = `
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="35" stroke="#888" stroke-width="10" fill="none" stroke-linecap="round"/>
</svg>`;
return div;
}
function truncateHtmlToTextLimit(html, maxLength) {
const div = document.createElement('div');
div.innerHTML = html;
let charCount = 0;
let truncated = '';
function walk(node) {
if (charCount >= maxLength) return;
if (node.nodeType === Node.TEXT_NODE) {
const remaining = maxLength - charCount;
const text = node.textContent.slice(0, remaining);
charCount += text.length;
truncated += text;
} else if (node.nodeType === Node.ELEMENT_NODE) {
const tag = node.tagName.toLowerCase();
truncated += `<${tag}${[...node.attributes].map(attr => ` ${attr.name}="${attr.value}"`).join('')}>`;
node.childNodes.forEach(walk);
truncated += `</${tag}>`;
}
}
div.childNodes.forEach(walk);
return truncated + (charCount >= maxLength ? '...' : '');
}
function insertPreviewCard(row, cooked, meta) {
if (row.querySelector('.topic-preview-card')) return;
const card = document.createElement('div');
card.className = `topic-preview-card ${darkClass()}`;
const metaLine = document.createElement('div');
metaLine.className = 'topic-preview-meta';
metaLine.textContent = `分类: ${meta.category}|标签: ${meta.tags.join(', ') || '无'}|回复: ${meta.replyCount}`;
card.appendChild(metaLine);
const summaryDiv = document.createElement('div');
summaryDiv.innerHTML = truncateHtmlToTextLimit(cooked, PREVIEW_LENGTH);
card.appendChild(summaryDiv);
const expandBtn = document.createElement('button');
expandBtn.className = 'topic-preview-btn';
expandBtn.textContent = '展开全文';
expandBtn.onclick = () => {
card.innerHTML = cooked;
};
card.appendChild(expandBtn);
row.appendChild(card);
}
function fetchTopic(row, topicId) {
const loading = createSpinner();
row.appendChild(loading);
GM_xmlhttpRequest({
method: 'GET',
url: `https://linux.do/t/${topicId}.json`,
onload: (res) => {
row.removeChild(loading);
try {
const data = JSON.parse(res.responseText);
const cooked = data.post_stream.posts[0].cooked;
const meta = {
category: data.category_slug || '未知',
tags: data.tags || [],
replyCount: data.posts_count || 0
};
insertPreviewCard(row, cooked, meta);
} catch (err) {
console.error('解析失败', err);
}
},
onerror: () => {
row.removeChild(loading);
console.error('加载失败');
}
});
}
const observer = new IntersectionObserver(entries => {
for (const entry of entries) {
if (entry.isIntersecting) {
const row = entry.target;
const link = row.querySelector('a.title');
if (link && link.href) {
const id = link.href.match(/\/t\/[^/]+\/(\d+)/)?.[1];
if (id) {
observer.unobserve(row);
fetchTopic(row, id);
}
}
}
}
}, { rootMargin: '100px', threshold: 0.1 });
function scan() {
const rows = document.querySelectorAll('.topic-list tr:not(.preview-bound)');
rows.forEach(row => {
const link = row.querySelector('a.title');
if (link) {
row.classList.add('preview-bound');
observer.observe(row);
}
});
}
const themeObs = new MutationObserver(() => {
document.querySelectorAll('.topic-preview-card').forEach(card => {
card.classList.remove('topic-preview-dark', 'topic-preview-light');
card.classList.add(darkClass());
});
});
themeObs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
themeObs.observe(document.body, { attributes: true });
const mo = new MutationObserver(scan);
mo.observe(document.body, { childList: true, subtree: true });
scan();
})();
1 Like