// ==UserScript==
// @name 2048-预览
// @namespace https://sleazyfork.org/zh-CN/users/1461640-%E6%98%9F%E5%AE%BF%E8%80%81%E9%AD%94
// @version 1.0.6
// @description 为2048核基地论坛帖子添加图片、链接预览
// @author 星宿老魔
// @match https://hjd2048.com/2048/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=hjd2048.com
// @license MIT
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function() {
'use strict';
// ========== 配置管理 ==========
const CONFIG = {
defaults: {
MAX_PREVIEW_IMAGES: 3,
CONCURRENT_LIMIT: 9,
SMALL_IMAGE_WIDTH_THRESHOLD: 250,
SMALL_IMAGE_HEIGHT_THRESHOLD: 150,
SMALL_IMAGE_AMPLIFY_FACTOR: 3,
},
get(key) {
return GM_getValue(key, this.defaults[key]);
},
set(key, value) {
GM_setValue(key, value);
},
getAll() {
const allSettings = {};
for (const key in this.defaults) {
allSettings[key] = this.get(key);
}
return allSettings;
}
};
// ========== UI & 设置面板 ==========
function createSettingsPanel() {
const panelHtml = `
<div id="preview-settings-panel" style="display:none; position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); background:white; padding:25px; border-radius:8px; box-shadow:0 4px 20px rgba(0,0,0,0.2); z-index:10001;">
<div id="settings-form">
<h4 class="settings-subtitle">预览图设置</h4>
<div class="settings-item">
<label for="MAX_PREVIEW_IMAGES">最大预览图数</label>
<input type="number" name="MAX_PREVIEW_IMAGES" id="MAX_PREVIEW_IMAGES">
<p class="setting-desc">帖子列表中,每个帖子下方最多显示的预览图数量。</p>
</div>
<div class="settings-item">
<label for="CONCURRENT_LIMIT">并发加载上限</label>
<input type="number" name="CONCURRENT_LIMIT" id="CONCURRENT_LIMIT">
<p class="setting-desc">同时加载预览内容的最大帖子数量,可缓解网络压力。</p>
</div>
<hr class="settings-separator">
<h4 class="settings-subtitle">小图放大设置</h4>
<div class="settings-item">
<label for="SMALL_IMAGE_WIDTH_THRESHOLD">小图宽度阈值 (px)</label>
<input type="number" name="SMALL_IMAGE_WIDTH_THRESHOLD" id="SMALL_IMAGE_WIDTH_THRESHOLD">
<p class="setting-desc">当图片宽度和高度都小于阈值时,查看大图时会自动放大。</p>
</div>
<div class="settings-item">
<label for="SMALL_IMAGE_HEIGHT_THRESHOLD">小图高度阈值 (px)</label>
<input type="number" name="SMALL_IMAGE_HEIGHT_THRESHOLD" id="SMALL_IMAGE_HEIGHT_THRESHOLD">
<p class="setting-desc">与宽度阈值一同判断是否为需要放大的小图。</p>
</div>
<div class="settings-item">
<label for="SMALL_IMAGE_AMPLIFY_FACTOR">小图放大倍数</label>
<input type="number" name="SMALL_IMAGE_AMPLIFY_FACTOR" id="SMALL_IMAGE_AMPLIFY_FACTOR">
<p class="setting-desc">符合上述条件的小图,在查看大图时宽度被放大的倍数。</p>
</div>
</div>
<div class="settings-buttons">
<button id="save-settings-btn">保存并刷新</button>
<button id="close-settings-btn" style="margin-left:10px;">关闭</button>
</div>
</div>
<div id="preview-settings-overlay" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); z-index:10000;"></div>
`;
document.body.insertAdjacentHTML('beforeend', panelHtml);
const panel = document.getElementById('preview-settings-panel');
const overlay = document.getElementById('preview-settings-overlay');
const form = document.getElementById('settings-form');
// 加载当前配置到表单
const currentConfig = CONFIG.getAll();
for (const key in currentConfig) {
const input = form.querySelector(`[name="${key}"]`);
if (input) {
input.value = currentConfig[key];
}
}
// 事件监听
document.getElementById('save-settings-btn').addEventListener('click', () => {
const inputs = form.querySelectorAll('input');
inputs.forEach(input => {
const value = input.type === 'number' ? parseInt(input.value, 10) : input.value;
CONFIG.set(input.name, value);
});
alert('设置已保存,页面将刷新以应用更改。');
window.location.reload();
});
const closePanel = () => {
panel.style.display = 'none';
overlay.style.display = 'none';
};
document.getElementById('close-settings-btn').addEventListener('click', closePanel);
overlay.addEventListener('click', closePanel);
return {
show: () => {
panel.style.display = 'block';
overlay.style.display = 'block';
}
};
}
function addSettingsButton() {
// 尝试将按钮添加到主导航栏的“搜索”项前
const searchLink = document.querySelector('#nav-pc a[href="/search.php"]');
const searchLi = searchLink ? searchLink.parentElement : null;
if (searchLi) {
const settingsLi = document.createElement('li');
const settingsBtn = document.createElement('a');
settingsBtn.href = 'javascript:;';
settingsBtn.textContent = '脚本配置';
settingsLi.appendChild(settingsBtn);
// 将新 li 插入到 “搜索” li 之后
searchLi.parentElement.insertBefore(settingsLi, searchLi.nextSibling);
const settingsPanel = createSettingsPanel();
settingsBtn.addEventListener('click', (e) => {
e.preventDefault();
settingsPanel.show();
});
} else {
// 如果找不到主导航栏,则回退到旧方法(可能布局不佳)
const searchForm = document.getElementById('nav-s');
if (searchForm) {
const settingsBtn = document.createElement('a');
settingsBtn.href = 'javascript:;';
settingsBtn.textContent = '脚本配置';
settingsBtn.style.color = '#fff';
settingsBtn.style.marginRight = '15px';
settingsBtn.className = 'fr'; // 借用现有样式
searchForm.insertBefore(settingsBtn, searchForm.firstChild);
const settingsPanel = createSettingsPanel();
settingsBtn.addEventListener('click', (e) => {
e.preventDefault();
settingsPanel.show();
});
}
}
}
// --- 冒泡提示函数 (移至全局作用域) ---
function showClickTip(text, e) {
let tip = document.querySelector('.click-tip');
if (tip) {
tip.remove();
}
tip = document.createElement('div');
tip.className = 'click-tip';
tip.textContent = text;
document.body.appendChild(tip);
tip.style.left = `${e.clientX}px`;
tip.style.top = `${e.clientY}px`;
setTimeout(() => { tip.style.opacity = '1'; }, 10);
setTimeout(() => {
tip.style.opacity = '0';
setTimeout(() => { if (tip.parentElement) tip.remove(); }, 200);
}, 1000);
}
// 等待页面加载完成
window.addEventListener('load', main);
async function main() {
// 在所有页面都添加设置按钮
addSettingsButton();
if (window.location.href.includes('read.php?tid=')) {
// 内容页:禁止网站的复制弹窗
suppressCopyPopupOnContentPage();
// 内容页:处理磁力链接
await processContentPageLinks();
} else {
// 列表页:移除广告
removeAds();
// 列表页:执行预览功能
await displayThreadImages();
}
}
// ========== 移除置顶帖 ==========
function removeAds() {
// 查找所有置顶帖并直接移除
const threadRows = document.querySelectorAll('tr.tr3.t_one');
threadRows.forEach(tr => {
const tdContent = tr.querySelector('td.tal');
if (tdContent && tdContent.innerHTML.includes('headtopic_3.gif')) {
tr.remove();
}
});
}
function suppressCopyPopupOnContentPage() {
const textareas = document.querySelectorAll('textarea[readonly]');
textareas.forEach(textarea => {
const textToCopy = textarea.value;
if (!textToCopy.startsWith('magnet:?xt=urn:btih:')) return;
let searchScope = textarea.parentElement;
if (!searchScope) return;
const copyButton = Array.from(searchScope.querySelectorAll('a, button, input, span')).find(el => {
return (el.textContent.trim() === '复制' || el.value === '复制');
});
if (copyButton) {
const newButton = copyButton.cloneNode(true);
copyButton.parentNode.replaceChild(newButton, copyButton);
newButton.addEventListener('click', e => {
e.stopImmediatePropagation();
e.preventDefault();
navigator.clipboard.writeText(textToCopy).then(() => {
showClickTip('已复制', e);
});
}, true);
}
});
}
async function processContentPageLinks() {
const contentSelectors = ['#read_tpc', '.tpc_content', '.f14.cc', 'div[id="read_tpc"]', '.t_f'];
let contentDiv;
for (const selector of contentSelectors) {
contentDiv = document.querySelector(selector);
if (contentDiv) break;
}
if (!contentDiv) return;
const contentText = contentDiv.innerHTML;
if (/magnet:\?xt=urn:btih:/.test(contentText)) {
console.log('脚本提示:页面已存在磁力链接,跳过自动提取。');
return;
}
let magnet = '';
const btLinkEl = contentDiv.querySelector('a[href*="bt.ivcbt.com/list.php?name="], a[href*="bt.bxmho.cn/list.php?name="]');
if (btLinkEl) {
const fetchedMagnet = await fetchMagnetFromBtPage(btLinkEl.href);
if (fetchedMagnet) magnet = fetchedMagnet;
}
if (!magnet) {
const hashMatch = contentText.match(/([A-F0-9]{40})/i);
if (hashMatch && hashMatch[1]) {
magnet = `magnet:?xt=urn:btih:${hashMatch[1]}`;
}
}
if (magnet) {
const magnetDiv = document.createElement('div');
magnetDiv.className = 'preview-magnet';
magnetDiv.textContent = magnet;
magnetDiv.title = '点击链接可复制';
magnetDiv.onclick = function(e) {
navigator.clipboard.writeText(magnet).then(() => {
showClickTip('已复制', e);
});
};
const infoContainer = document.createElement('div');
infoContainer.className = 'preview-section';
infoContainer.style.marginTop = '20px';
const infoSectionTitle = document.createElement('div');
infoSectionTitle.className = 'preview-section-title';
infoSectionTitle.textContent = '脚本提取-磁力链接';
infoContainer.appendChild(infoSectionTitle);
infoContainer.appendChild(magnetDiv);
contentDiv.appendChild(infoContainer);
}
}
// ========== 样式管理模块 ==========
const StyleManager = {
styleInjected: false,
injectStyles() {
if (this.styleInjected || document.getElementById('preview-styles')) {
return;
}
const style = document.createElement('style');
style.id = 'preview-styles';
style.textContent = this.getStylesCSS();
document.head.appendChild(style);
this.styleInjected = true;
},
getStylesCSS() {
return `
/* 标题行样式 */
.thread-title-highlighted {
background-color: #f0f8ff !important;
position: relative;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
transition: all 0.3s ease;
}
.thread-title-highlighted:hover {
background-color: #e3f2fd !important;
}
/* 点击复制后的冒泡提示 */
.click-tip {
position: fixed;
background: #333;
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
z-index: 10000;
pointer-events: none;
transform: translate(15px, -15px);
transition: opacity 0.2s;
opacity: 0;
}
/* 预览容器样式 */
.preview-container {
margin: 0 0 10px 0;
border: 1px solid #c8e1ff;
border-top: none;
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
padding: 15px;
background-color: #f5faff;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.preview-section {
margin-bottom: 12px;
animation: fadeIn 0.4s ease-in-out;
}
.preview-section-title {
font-size: 13px;
color: #666;
margin-bottom: 5px;
font-weight: bold;
padding-left: 5px;
border-left: 3px solid #3498db;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.preview-images {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
margin-bottom: 15px;
}
.preview-image {
width: 100%;
height: 220px;
object-fit: contain;
background-color: #f0f0f0;
border: 1px solid #ddd;
border-radius: 4px;
transition: transform 0.2s;
cursor: pointer;
}
.preview-image:hover {
transform: scale(1.03);
}
.preview-filesize {
color: #e74c3c;
font-weight: bold;
font-size: 14px;
}
.preview-magnet, .preview-bt-link {
font-size: 14px;
word-break: break-all;
cursor: pointer;
padding: 8px;
background-color: #f5f5f5;
border-radius: 4px;
margin-bottom: 8px;
transition: all 0.3s ease;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
position: relative;
}
.preview-magnet:hover, .preview-bt-link:hover {
background-color: #e8f4ff;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
transform: translateY(-1px);
}
/* 大图查看样式 */
.lightbox {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.lightbox.active {
opacity: 1;
visibility: visible;
}
.lightbox-content {
position: relative;
display: flex;
justify-content: center;
align-items: center;
max-width: 95%;
max-height: 95%;
transition: max-width 0.3s ease, max-height 0.3s ease;
}
.lightbox-content.landscape {
max-width: 80vw;
max-height: 90vh;
}
.lightbox-content.portrait {
max-width: 60vw;
max-height: 90vh;
}
.lightbox-image {
display: block;
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
}
.lightbox-prev, .lightbox-next {
position: absolute;
top: 50%;
transform: translateY(-50%);
font-size: 50px;
color: #fff;
cursor: pointer;
z-index: 10000;
padding: 0 20px;
user-select: none;
transition: color .2s ease;
}
.lightbox-prev:hover, .lightbox-next:hover {
color: #ccc;
}
.lightbox-prev { left: 15px; }
.lightbox-next { right: 15px; }
.lightbox-close {
position: absolute;
top: 20px;
right: 20px;
color: #fff;
font-size: 30px;
cursor: pointer;
}
.lightbox-loading {
color: white;
font-size: 16px;
}
/* 设置面板样式 */
#preview-settings-panel {
width: 480px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
#preview-settings-panel h3 {
margin: 0 0 20px 0;
padding-bottom: 15px;
border-bottom: 1px solid #e0e0e0;
font-size: 18px;
color: #333;
}
.settings-subtitle {
font-size: 16px;
color: #444;
margin-top: 25px;
margin-bottom: 15px;
padding-bottom: 8px;
}
.settings-subtitle:first-of-type {
margin-top: 0;
}
.settings-separator {
border: 0;
border-top: 1px solid #eaeaea;
margin: 25px 0;
}
.settings-item {
display: grid;
grid-template-columns: 1fr 120px;
gap: 10px 15px;
align-items: center;
margin-bottom: 18px;
}
.settings-item label {
font-size: 14px;
color: #555;
}
.settings-item input[type="number"] {
padding: 6px 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
width: 100%;
box-sizing: border-box;
}
.settings-item .setting-desc {
grid-column: 1 / -1;
font-size: 12px;
color: #888;
margin: -10px 0 5px 0;
}
.settings-buttons {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #e0e0e0;
text-align: right;
}
.settings-buttons button {
padding: 8px 16px;
font-size: 14px;
border: none;
border-radius: 5px;
cursor: pointer;
transition: opacity 0.2s;
}
.settings-buttons button:hover {
opacity: 0.85;
}
#save-settings-btn {
background-color: #28a745;
color: white;
}
#close-settings-btn {
background-color: #6c757d;
color: white;
}
`;
}
};
// ========== 工具函数模块 ==========
const Utils = {
// 复制到剪贴板
copyToClipboard(text, event) {
navigator.clipboard.writeText(text).then(() => {
showClickTip('已复制', event);
});
},
// 移除版规
removeRules() {
try {
const collapseHeader = document.querySelector('.collapse-header');
if (collapseHeader) {
const ruleContainer = collapseHeader.closest('div, section, .rule-container, .collapse-container');
if (ruleContainer) {
ruleContainer.remove();
}
}
} catch (e) {
console.error('移除版规时出错:', e);
}
},
// 检查是否为内容页
isContentPage() {
return window.location.href.includes('read.php?tid=');
},
// 并发控制
async asyncPool(poolLimit, array, iteratorFn) {
const ret = [];
const executing = [];
for (const item of array) {
const p = Promise.resolve().then(() => iteratorFn(item, array));
ret.push(p);
if (poolLimit <= array.length) {
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= poolLimit) {
await Promise.race(executing);
}
}
}
return Promise.all(ret);
}
};
// ========== Lightbox管理模块 ==========
const LightboxManager = {
lightbox: null,
lightboxImg: null,
lightboxContent: null,
loadingText: null,
currentImages: [],
currentIndex: 0,
config: {},
init(widthThreshold, heightThreshold, amplifyFactor) {
this.config = { widthThreshold, heightThreshold, amplifyFactor };
this.createLightbox();
this.setupEventListeners();
},
createLightbox() {
if (this.lightbox) return; // 避免重复创建
this.lightbox = document.createElement('div');
this.lightbox.className = 'lightbox';
this.lightboxContent = document.createElement('div');
this.lightboxContent.className = 'lightbox-content';
this.lightboxImg = document.createElement('img');
this.lightboxImg.className = 'lightbox-image';
const closeBtn = document.createElement('div');
closeBtn.className = 'lightbox-close';
closeBtn.innerHTML = '×';
const prevBtn = document.createElement('div');
prevBtn.className = 'lightbox-prev';
prevBtn.innerHTML = '‹';
const nextBtn = document.createElement('div');
nextBtn.className = 'lightbox-next';
nextBtn.innerHTML = '›';
this.loadingText = document.createElement('div');
this.loadingText.className = 'lightbox-loading';
this.loadingText.textContent = '加载中...';
this.lightboxContent.appendChild(this.loadingText);
this.lightboxContent.appendChild(this.lightboxImg);
this.lightbox.appendChild(this.lightboxContent);
this.lightbox.appendChild(closeBtn);
this.lightbox.appendChild(prevBtn);
this.lightbox.appendChild(nextBtn);
document.body.appendChild(this.lightbox);
// 保存按钮引用以便事件监听
this.closeBtn = closeBtn;
this.prevBtn = prevBtn;
this.nextBtn = nextBtn;
},
setupEventListeners() {
this.closeBtn.addEventListener('click', () => this.closeLightbox());
this.prevBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.currentIndex = (this.currentIndex - 1 + this.currentImages.length) % this.currentImages.length;
this.updateLightboxImage();
});
this.nextBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.currentIndex = (this.currentIndex + 1) % this.currentImages.length;
this.updateLightboxImage();
});
this.lightbox.addEventListener('click', (e) => {
if (e.target === this.lightbox) this.closeLightbox();
});
document.addEventListener('keydown', (e) => {
if (!this.lightbox.classList.contains('active')) return;
if (e.key === 'Escape') this.closeLightbox();
else if (e.key === 'ArrowLeft') this.prevBtn.click();
else if (e.key === 'ArrowRight') this.nextBtn.click();
});
},
updateLightboxImage() {
const imgSrc = this.currentImages[this.currentIndex];
this.loadingText.textContent = '加载中...';
this.loadingText.style.display = 'block';
this.lightboxImg.style.display = 'none';
// 重置样式
this.lightboxImg.style.width = '';
this.lightboxImg.style.height = '';
this.lightboxContent.classList.remove('landscape', 'portrait');
this.lightboxImg.src = imgSrc;
this.lightboxImg.onload = () => {
this.loadingText.style.display = 'none';
this.lightboxImg.style.display = 'block';
// 检查是否为需要放大的小图
if (this.lightboxImg.naturalWidth < this.config.widthThreshold &&
this.lightboxImg.naturalHeight < this.config.heightThreshold) {
this.lightboxImg.style.width = (this.lightboxImg.naturalWidth * this.config.amplifyFactor) + 'px';
} else {
// 对于大图,根据图片宽高比调整容器尺寸
if (this.lightboxImg.naturalWidth > this.lightboxImg.naturalHeight) {
this.lightboxContent.classList.add('landscape');
} else {
this.lightboxContent.classList.add('portrait');
}
}
};
this.lightboxImg.onerror = () => {
this.loadingText.textContent = '图片加载失败';
};
},
showLightbox(images, index) {
this.currentImages = images;
this.currentIndex = index;
this.updateLightboxImage();
this.lightbox.classList.add('active');
},
closeLightbox() {
this.lightbox.classList.remove('active');
}
};
// ========== 事件管理模块 ==========
const EventManager = {
initEd2kInterception() {
// 拦截页面上原有的ed2k链接点击事件,防止跳转,改为复制
document.addEventListener('click', function(e) {
if (e.target.tagName === 'A' && e.target.href && e.target.href.startsWith('ed2k://')) {
e.preventDefault();
Utils.copyToClipboard(e.target.href, e);
}
});
}
};
// ========== 数据提取模块 ==========
const DataExtractor = {
async extractFromPage(doc) {
const data = {
imgSrcs: await this.extractImages(doc),
fileSize: this.extractFileSize(doc),
magnet: this.extractMagnet(doc),
thunderLink: this.extractThunderLink(doc),
ed2kLink: this.extractEd2kLink(doc),
btIvcbtLink: this.extractBtLink(doc)
};
// 如果没有直接的磁力链接,但有bt链接,则尝试获取
if (!data.magnet && data.btIvcbtLink) {
const fetchedMagnet = await fetchMagnetFromBtPage(data.btIvcbtLink);
if (fetchedMagnet) {
data.magnet = fetchedMagnet;
}
}
return data;
},
extractImages(doc) {
const MAX_PREVIEW_IMAGES = CONFIG.get('MAX_PREVIEW_IMAGES');
// 查找图片元素
let imgElements = [];
const possibleSelectors = ['#read_tpc img', '.tpc_content img', '.f14.cc img', 'div[id="read_tpc"] img'];
for (const selector of possibleSelectors) {
imgElements = Array.from(doc.querySelectorAll(selector));
if (imgElements.length > 0) break;
}
// 过滤掉隐藏的图片
const visibleImgElements = imgElements.filter(img => {
const imgStyle = img.getAttribute('style') || '';
if (imgStyle.includes('display: none') || imgStyle.includes('display:none')) {
return false;
}
// 检查父元素是否隐藏
let parent = img.parentElement;
let levels = 0;
while (parent && levels < 5) {
const parentStyle = parent.getAttribute('style') || '';
if (parentStyle.includes('display: none') || parentStyle.includes('display:none')) {
return false;
}
const parentClass = parent.getAttribute('class') || '';
if (parentClass.includes('hidden') || parentClass.includes('hide')) {
return false;
}
parent = parent.parentElement;
levels++;
}
return true;
});
// 智能过滤无用图片
let imgSrcsWithPriority = visibleImgElements
.map(img => ({
src: img.getAttribute('data-original') || img.getAttribute('src') || '',
img
}))
.filter(item => {
if (!item.src || !item.src.startsWith('http')) return false;
// 基础过滤
if (item.src.includes('loading.') || item.src.includes('placeholder.')) return false;
// 过滤常见的无用图片模式
const src = item.src.toLowerCase();
const filename = src.split('/').pop() || '';
const badPatterns = [
/^(none|empty|blank|default)\./, // 占位图
/^(icon|logo|banner|ad)_/, // 图标、logo、广告
/\.(ico|cur)$/, // 图标文件
/^(loading|wait|spinner)/, // 加载图标
];
if (badPatterns.some(pattern => pattern.test(filename))) {
return false;
}
// 过滤过小的图片
if (item.img.width && item.img.height) {
if (item.img.width < 100 || item.img.height < 100) {
return false;
}
}
// 过滤特定class的图片
const imgClass = item.img.getAttribute('class') || '';
if (imgClass.includes('icon') || imgClass.includes('emoji') || imgClass.includes('smiley')) {
return false;
}
return true;
});
// 优先显示jpg/jpeg/png图片
imgSrcsWithPriority.sort((a, b) => {
const aIsMain = /\.(jpg|jpeg|png)$/i.test(a.src);
const bIsMain = /\.(jpg|jpeg|png)$/i.test(b.src);
if (aIsMain && !bIsMain) return -1;
if (!aIsMain && bIsMain) return 1;
return 0;
});
const imgSrcs = imgSrcsWithPriority.map(item => item.src).slice(0, MAX_PREVIEW_IMAGES);
// 调试日志
if (imgElements.length > 0) {
console.log(`图片处理: 总共找到 ${imgElements.length} 张图片,可见 ${visibleImgElements.length} 张,智能过滤后 ${imgSrcsWithPriority.length} 张,最终选择 ${imgSrcs.length}/${MAX_PREVIEW_IMAGES} 张预览`);
const filteredOut = visibleImgElements.length - imgSrcsWithPriority.length;
if (filteredOut > 0) {
console.log(`过滤掉 ${filteredOut} 张无用图片(占位图、图标等)`);
}
}
return imgSrcs;
},
extractFileSize(doc) {
let fileSize = '';
const contentSelectors = ['#read_tpc', '.tpc_content', '.f14.cc', 'div[id="read_tpc"]', '.t_f'];
for (const selector of contentSelectors) {
const contentDiv = doc.querySelector(selector);
if (contentDiv) {
const contentText = contentDiv.innerHTML;
let fileSizeMatch = contentText.match(/【影片容量】:([^<]+)<br/i) || contentText.match(/【影片大小】:([^<]+)</i);
if (fileSizeMatch && fileSizeMatch[1]) {
fileSize = fileSizeMatch[1].trim();
break;
}
}
}
return fileSize;
},
extractMagnet(doc) {
let magnet = '';
let magnetText = doc.querySelector('textarea[readonly], textarea#copytext');
if (magnetText) {
magnet = magnetText.value.trim();
} else {
let magnetA = doc.querySelector('a[href^="magnet:?xt=urn:btih:"]');
if (magnetA) {
magnet = magnetA.getAttribute('href');
} else {
const contentText = doc.body.innerHTML;
const hashMatch = contentText.match(/([A-F0-9]{40})/i);
if (hashMatch && hashMatch[1]) {
magnet = `magnet:?xt=urn:btih:${hashMatch[1]}`;
}
}
}
return magnet;
},
extractThunderLink(doc) {
const contentText = doc.body.innerHTML;
const thunderMatch = contentText.match(/thunder:\/\/[A-Za-z0-9+\/=]+/i);
return thunderMatch ? thunderMatch[0] : '';
},
extractEd2kLink(doc) {
// 先尝试从<a>标签的href属性中获取ed2k链接
let ed2kA = doc.querySelector('a[href^="ed2k://"]');
if (ed2kA) {
return ed2kA.getAttribute('href');
} else {
// 如果没有找到,再从纯文本中匹配
const contentText = doc.body.innerHTML;
const ed2kMatch = contentText.match(/ed2k:\/\/\|file\|[^|]+\|\d+\|[A-F0-9]{32}\|\//i);
return ed2kMatch ? ed2kMatch[0] : '';
}
},
extractBtLink(doc) {
// 检查bt.ivcbt.com链接
let btLinkEl = doc.querySelector('a[href*="bt.ivcbt.com/list.php?name="]');
if (btLinkEl) {
return btLinkEl.getAttribute('href');
} else {
// 检查bt.bxmho.cn链接
btLinkEl = doc.querySelector('a[href*="bt.bxmho.cn/list.php?name="]');
return btLinkEl ? btLinkEl.getAttribute('href') : '';
}
}
};
// 主函数:为每条帖子添加图片预览
async function displayThreadImages() {
const MAX_PREVIEW_IMAGES = CONFIG.get('MAX_PREVIEW_IMAGES');
const CONCURRENT_LIMIT = CONFIG.get('CONCURRENT_LIMIT');
const SMALL_IMAGE_WIDTH_THRESHOLD = CONFIG.get('SMALL_IMAGE_WIDTH_THRESHOLD');
const SMALL_IMAGE_HEIGHT_THRESHOLD = CONFIG.get('SMALL_IMAGE_HEIGHT_THRESHOLD');
const SMALL_IMAGE_AMPLIFY_FACTOR = CONFIG.get('SMALL_IMAGE_AMPLIFY_FACTOR');
// 检查当前页面是否为内容页
if (Utils.isContentPage()) {
console.log('当前页面为内容页,跳过预览功能');
return;
}
// 初始化样式和移除版规
StyleManager.injectStyles();
Utils.removeRules();
// 初始化Lightbox和事件监听
LightboxManager.init(SMALL_IMAGE_WIDTH_THRESHOLD, SMALL_IMAGE_HEIGHT_THRESHOLD, SMALL_IMAGE_AMPLIFY_FACTOR);
EventManager.initEd2kInterception();
// 获取所有需要处理的帖子链接并批量处理
const postLinks = document.querySelectorAll('a[target="_self"], a[target="_blank"]');
if (postLinks.length) {
await Utils.asyncPool(CONCURRENT_LIMIT, Array.from(postLinks), PreviewProcessor.processThreadLink);
}
}
// ========== UI组件模块 ==========
const UIComponents = {
buildPreviewUI(tr, previewData) {
const { imgSrcs, fileSize, magnet, btIvcbtLink, thunderLink, ed2kLink } = previewData;
if (tr.nextElementSibling && tr.nextElementSibling.classList.contains('imagePreviewTr')) return;
tr.classList.add('thread-title-highlighted');
const newTr = document.createElement('tr');
newTr.className = 'imagePreviewTr';
const newTd = document.createElement('td');
newTd.colSpan = tr.children.length;
const previewContainer = document.createElement('div');
previewContainer.className = 'preview-container';
previewContainer.style.borderTop = 'none';
// 添加图片预览
if (imgSrcs.length) {
previewContainer.appendChild(this.createImageSection(imgSrcs));
}
// 添加资源信息
if (fileSize || magnet || (btIvcbtLink && !magnet) || thunderLink || ed2kLink) {
previewContainer.appendChild(this.createInfoSection({
fileSize, magnet, btIvcbtLink, thunderLink, ed2kLink
}));
}
newTd.appendChild(previewContainer);
newTr.appendChild(newTd);
tr.parentNode.insertBefore(newTr, tr.nextSibling);
},
createImageSection(imgSrcs) {
const imgSection = document.createElement('div');
imgSection.className = 'preview-section';
const imgSectionTitle = document.createElement('div');
imgSectionTitle.className = 'preview-section-title';
imgSectionTitle.textContent = '预览图片';
imgSection.appendChild(imgSectionTitle);
const imgContainer = document.createElement('div');
imgContainer.className = 'preview-images';
imgSrcs.forEach((imgSrc, index) => {
if (imgSrc && imgSrc.startsWith('http')) {
const img = document.createElement('img');
img.src = imgSrc;
img.className = 'preview-image';
img.onerror = () => { img.style.display = 'none'; };
img.addEventListener('click', () => LightboxManager.showLightbox(imgSrcs, index));
imgContainer.appendChild(img);
}
});
imgSection.appendChild(imgContainer);
return imgSection;
},
createInfoSection({ fileSize, magnet, btIvcbtLink, thunderLink, ed2kLink }) {
const infoContainer = document.createElement('div');
infoContainer.className = 'preview-section';
const infoSectionTitle = document.createElement('div');
infoSectionTitle.className = 'preview-section-title';
infoSectionTitle.textContent = '资源信息';
infoContainer.appendChild(infoSectionTitle);
if (fileSize) {
infoContainer.appendChild(this.createFileSizeElement(fileSize));
}
if (magnet) {
infoContainer.appendChild(this.createLinkElement(magnet, 'magnet'));
}
if (thunderLink) {
infoContainer.appendChild(this.createLinkElement(thunderLink, 'thunder'));
}
if (ed2kLink) {
infoContainer.appendChild(this.createLinkElement(ed2kLink, 'ed2k'));
}
if (btIvcbtLink && !magnet) {
infoContainer.appendChild(this.createLinkElement(btIvcbtLink, 'bt'));
}
return infoContainer;
},
createFileSizeElement(fileSize) {
const fileSizeDiv = document.createElement('div');
fileSizeDiv.className = 'preview-filesize';
const sizeLabel = fileSize.includes('MB') || fileSize.includes('mb') ? '【影片大小】' : '【影片容量】';
fileSizeDiv.innerHTML = `${sizeLabel}:${fileSize}`;
fileSizeDiv.style.marginBottom = '10px';
return fileSizeDiv;
},
createLinkElement(linkText, linkType) {
const linkDiv = document.createElement('div');
linkDiv.className = 'preview-magnet';
linkDiv.textContent = linkText;
linkDiv.title = '点击链接可复制';
linkDiv.onclick = function(e) {
Utils.copyToClipboard(linkText, e);
};
return linkDiv;
}
};
// ========== 预览处理器模块 ==========
const PreviewProcessor = {
async processThreadLink(link) {
const threadURL = link.href;
if (!threadURL || !threadURL.includes('read.php?tid=')) return;
const tr = link.closest('tr');
if (!tr || tr.querySelector('img[src*="headtopic"]')) return; // 跳过置顶帖
try {
const response = await fetch(threadURL);
const pageContent = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(pageContent, 'text/html');
// 提取数据
const previewData = await DataExtractor.extractFromPage(doc);
// 检查是否有内容需要预览
if (!previewData.imgSrcs.length && !previewData.fileSize &&
!previewData.magnet && !previewData.btIvcbtLink &&
!previewData.thunderLink && !previewData.ed2kLink) {
return;
}
// 构建UI
UIComponents.buildPreviewUI(tr, previewData);
} catch (e) {
console.error('预览加载失败:', e);
}
}
};
// 从bt页面获取磁力链接的辅助函数
function fetchMagnetFromBtPage(url) {
console.log('尝试从以下URL获取磁力链接:', url);
return new Promise((resolve) => {
if (typeof GM_xmlhttpRequest === 'undefined') {
console.error("GM_xmlhttpRequest is not available. Please add '// @grant GM_xmlhttpRequest' to the script header.");
resolve(null);
return;
}
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: function(response) {
console.log('GM_xmlhttpRequest 响应状态:', response.status);
if (response.status >= 200 && response.status < 300) {
const html = response.responseText;
// console.log('获取到的HTML内容:', html); // 暂时注释掉,因为可能很长
// 使用统一的磁力链接匹配规则(适用于所有BT域名)
const magnetRegex = /magnet:\?xt=urn:btih:[a-zA-Z0-9]+/;
const match = html.match(magnetRegex);
if (match) {
console.log('成功匹配到磁力链接:', match[0]);
resolve(match[0]);
} else {
console.log('页面中未匹配到磁力链接');
console.log('获取到的HTML内容(前500字符):', html.substring(0, 500)); // 打印前500个字符看下
resolve(null);
}
} else {
console.error('GM_xmlhttpRequest 请求失败,状态码:', response.status);
resolve(null);
}
},
onerror: function(error) {
console.error('GM_xmlhttpRequest 请求出错:', error);
resolve(null);
}
});
});
}
})();