// ==UserScript==
// @name MTean 苹果壳 - 图片预览瀑布流(延迟加载)
// @namespace http://tampermonkey.net/
// @version 2025-01-29 3.0
// @description MT 增加开关选项和图片预览瀑布流布局,延迟加载修复初始化问题(2025-01-29)
// @author Yo
// @match http*://xp.m-team.io/*/*
// @match http*://xp.m-team.io/*
// @match http*://kp.m-team.cc/*/*
// @match http*://kp.m-team.cc/*
// @match http*://zp.m-team.io/*/*
// @match http*://zp.m-team.io/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=m-team.io
// @grant GM_addStyle
// @connect *
// @require http://code.jquery.com/jquery-latest.js
// @grant unsafeWindow
// @run-at document-end
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 定义 localStorage 的键名
const STORAGE_KEY_HOVER = 'isHoverEnabled';
const STORAGE_KEY_SEARCH_API = 'isSearchApiEnabled';
const STORAGE_KEY_MASONRY = 'isMasonryEnabled';
// 从 localStorage 中读取用户的选择,如果没有则使用默认值
let isHoverEnabled = localStorage.getItem(STORAGE_KEY_HOVER) !== 'false'; // 默认值为 true
let isSearchApiEnabled = localStorage.getItem(STORAGE_KEY_SEARCH_API) !== 'false'; // 默认值为 true
let isMasonryEnabled = localStorage.getItem(STORAGE_KEY_MASONRY) !== 'false'; // 默认值为 true
// 处理接口数据的方法
const _XMLHttpRequest = unsafeWindow.XMLHttpRequest;
function newXHR() {
const xhr = new _XMLHttpRequest();
// 保存原始的 send 方法
const originalSend = xhr.send;
// 保存原始的 onreadystatechange
const originalOnReadyStateChange = xhr.onreadystatechange;
xhr.onreadystatechange = function () {
if (this.readyState === 4) {
if (this.status === 200) {
let response = this.response;
let url = this.responseURL; // 使用 responseURL 替代 url
if (url.indexOf('/api/torrent/search') !== -1 && response !== '') {
try {
const dataParse = JSON.parse(response);
if (isSearchApiEnabled && Array.isArray(dataParse.data.data) && dataParse.data.data.length > 0) {
let now = new Date(); // 当前时间
let previous24Hours = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 当前时间往前推24小时
// 分离过去24小时内和非过去24小时的数据,并为每一项添加 sum 值
let { within24HoursData, otherData } = dataParse.data.data.reduce((acc, item) => {
let sum = 0;
if (item.status) {
sum = parseInt(item.status.leechers, 10) + parseInt(item.status.seeders, 10);
}
// 判断是否在当前时间和过去24小时之间
let createdDate = new Date(item.createdDate);
if (createdDate >= previous24Hours && createdDate <= now) {
acc.within24HoursData.push({ ...item, sum });
} else {
acc.otherData.push({ ...item, sum });
}
return acc;
}, { within24HoursData: [], otherData: [] });
// 过去24小时内的数据按 createdDate 时间升序排序
within24HoursData.sort((a, b) => new Date(b.createdDate) - new Date(a.createdDate));
// 非过去24小时的数据按 sum 值降序排序
otherData.sort((a, b) => b.sum - a.sum);
// 合并 24小时内的数据和其他数据
let tempData = [...within24HoursData, ...otherData];
const modifiedResponse = JSON.stringify({
...dataParse,
data: {
...dataParse.data,
data: tempData,
}
});
Object.defineProperty(this, 'response', {
get: function () {
return modifiedResponse;
}
});
Object.defineProperty(this, 'responseText', {
get: function () {
return modifiedResponse;
}
});
}
} catch (error) {
console.error('Error parsing or modifying response:', error);
}
}
}
if (originalOnReadyStateChange) {
originalOnReadyStateChange.apply(this, arguments);
}
}
};
// 重写 send 方法
xhr.send = function (data) {
return originalSend.call(this, data);
};
// 添加辅助方法来获取请求头
xhr.getRequestHeader = function (name) {
return this.requestHeaders ? this.requestHeaders[name] : null;
};
// 重写 setRequestHeader 方法
const originalSetRequestHeader = xhr.setRequestHeader;
xhr.requestHeaders = {};
xhr.setRequestHeader = function (name, value) {
this.requestHeaders[name] = value;
return originalSetRequestHeader.apply(this, arguments);
};
return xhr;
}
// 替换为新的 XMLHttpRequest
unsafeWindow.XMLHttpRequest = newXHR;
// 创建控制面板
function createControlPanel() {
const panel = document.createElement('div');
panel.style.position = 'fixed';
panel.style.top = '10px';
panel.style.right = '10px';
panel.style.zIndex = '10000';
panel.style.backgroundColor = 'rgba(255, 255, 255, 0.8)';
panel.style.padding = '10px';
panel.style.borderRadius = '5px';
panel.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.1)';
panel.style.display = 'flex';
panel.style.flexDirection = 'column';
panel.style.gap = '10px';
// 悬停显示图片开关
const hoverLabel = document.createElement('label');
hoverLabel.style.display = 'flex';
hoverLabel.style.alignItems = 'center';
hoverLabel.style.gap = '5px';
const hoverCheckbox = document.createElement('input');
hoverCheckbox.type = 'checkbox';
hoverCheckbox.checked = isHoverEnabled;
hoverCheckbox.onchange = (e) => {
isHoverEnabled = e.target.checked;
localStorage.setItem(STORAGE_KEY_HOVER, isHoverEnabled); // 存储用户选择
if (!isHoverEnabled) {
clearDom(); // 如果关闭功能,清除所有显示的图片
}
};
hoverLabel.appendChild(hoverCheckbox);
hoverLabel.appendChild(document.createTextNode('开启悬停显示图片'));
// 处理 /api/torrent/search 开关
const searchApiLabel = document.createElement('label');
searchApiLabel.style.display = 'flex';
searchApiLabel.style.alignItems = 'center';
searchApiLabel.style.gap = '5px';
const searchApiCheckbox = document.createElement('input');
searchApiCheckbox.type = 'checkbox';
searchApiCheckbox.checked = isSearchApiEnabled;
searchApiCheckbox.onchange = (e) => {
isSearchApiEnabled = e.target.checked;
localStorage.setItem(STORAGE_KEY_SEARCH_API, isSearchApiEnabled); // 存储用户选择
};
searchApiLabel.appendChild(searchApiCheckbox);
searchApiLabel.appendChild(document.createTextNode('开启搜索数据处理'));
// 瀑布流布局开关
const masonryLabel = document.createElement('label');
masonryLabel.style.display = 'flex';
masonryLabel.style.alignItems = 'center';
masonryLabel.style.gap = '5px';
const masonryCheckbox = document.createElement('input');
masonryCheckbox.type = 'checkbox';
masonryCheckbox.checked = isMasonryEnabled;
masonryCheckbox.onchange = (e) => {
isMasonryEnabled = e.target.checked;
localStorage.setItem(STORAGE_KEY_MASONRY, isMasonryEnabled); // 存储用户选择
applyMasonryLayout(); // 重新应用布局
};
masonryLabel.appendChild(masonryCheckbox);
masonryLabel.appendChild(document.createTextNode('开启瀑布流布局'));
// 将三个开关添加到面板
panel.appendChild(hoverLabel);
panel.appendChild(searchApiLabel);
panel.appendChild(masonryLabel);
document.body.appendChild(panel);
}
// 清除显示的图片
function clearDom() {
var elements = document.getElementsByClassName('imgdom');
var elementsArray = Array.from(elements);
elementsArray.forEach(function (element) {
element.parentNode.removeChild(element);
});
}
// 加载悬停显示图片功能
function loadScript() {
const hoverableImages = document.querySelectorAll('.ant-image');
hoverableImages.forEach((image) => {
image.addEventListener('mouseover', (event) => {
if (isHoverEnabled && event && event.target && event.target.previousElementSibling && event.target.previousElementSibling.src && event.target.previousElementSibling.src !== '') {
clearDom();
const div = document.createElement('div');
div.classList.add('imgdom');
let img = document.createElement('img');
img.src = event.target.previousElementSibling.src;
img.alt = '18x';
img.style.cssText = 'width: 70%;object-position: center;object-fit: contain';
img.id = 'imgProId';
div.style.cssText = 'position: fixed;top: 50%;left: 50%;transform: translate(-50%, -50%);max-width: 100%;';
div.appendChild(img);
document.body.appendChild(div);
}
});
});
document.getElementById('app-content').onscroll = () => clearDom();
}
// 应用瀑布流布局
GM_addStyle(`
/* 瀑布流容器 */
.masonry-container {
column-count: 6; /* 默认6列 */
column-gap: 20px;
padding: 10px;
width: calc(100% - 10px); /* 考虑左右padding */
box-sizing: border-box;
margin: 10px auto; /* 上下和左右各20px外边距 */
}
/* 响应式布局 */
@media (max-width: 2000px) {
.masonry-container {
column-count: 6;
}
}
@media (max-width: 1600px) {
.masonry-container {
column-count: 5;
}
}
@media (max-width: 1400px) {
.masonry-container {
column-count: 4;
}
}
@media (max-width: 1200px) {
.masonry-container {
column-count: 4;
}
}
@media (max-width: 900px) {
.masonry-container {
column-count: 2;
}
}
@media (max-width: 600px) {
.masonry-container {
column-count: 2;
}
}
/* 卡片样式 */
.masonry-item {
break-inside: avoid;
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: all 0.3s ease;
cursor: pointer;
margin-bottom: 20px; /* 添加底部间距 */
display: inline-block;
width: 100%;
}
/* 图片容器 */
.masonry-item .img-container {
width: 100%;
position: relative;
overflow: hidden;
}
.masonry-item .img-container img {
width: 100%;
height: auto;
display: block;
transition: transform 0.3s ease;
}
.masonry-item:hover {
transform: translateY(-5px);
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
}
.masonry-item:hover .img-container img {
transform: scale(1.05);
}
/* 信息容器样式 */
.masonry-item .info {
padding: 16px;
cursor: pointer;
}
/* 标题样式 */
.masonry-item .title {
font-size: 1rem;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 12px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
/* 详情样式 */
.masonry-item .details {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
font-size: 0.875rem;
color: #666;
}
.masonry-item .details div {
display: flex;
align-items: center;
gap: 4px;
}
/* 标签样式 */
.masonry-item .tag {
display: inline-block;
padding: 4px 8px;
background: #f5f5f5;
border-radius: 4px;
font-size: 0.75rem;
color: #666;
margin-right: 6px;
margin-bottom: 6px;
}
/* 弹出层样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.modal-overlay.active {
opacity: 1;
visibility: visible;
}
.modal-content {
background: #fff;
border-radius: 12px;
max-width: 800px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
transform: translateY(20px);
transition: all 0.3s ease;
}
.modal-overlay.active .modal-content {
transform: translateY(0);
}
/* 关闭按钮 */
.modal-close {
position: absolute;
top: 20px;
right: 20px;
width: 30px;
height: 30px;
background: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* 预览按钮容器 */
.masonry-item .preview-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
opacity: 0;
transition: opacity 0.3s ease;
}
.masonry-item:hover .preview-container {
opacity: 1;
}
/* 预览按钮样式 */
.preview-btn {
padding: 8px 16px;
background: #fff;
border: none;
border-radius: 20px;
color: #333;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 6px;
}
.preview-btn:hover {
background: #f0f0f0;
transform: scale(1.05);
}
/* 预览按钮图标 */
.preview-btn svg {
width: 16px;
height: 16px;
}
/* 修改卡片整体的cursor */
.masonry-item {
cursor: default;
}
`);
function applyMasonryLayout() {
const table = document.querySelector('table');
if (!table) return;
// 移除现有的瀑布流容器
const existingMasonry = document.querySelector('.masonry-container');
if (existingMasonry) {
existingMasonry.remove();
}
if (isMasonryEnabled) {
const masonryContainer = document.createElement('div');
masonryContainer.classList.add('masonry-container');
table.parentNode.insertBefore(masonryContainer, table);
const rows = Array.from(table.querySelectorAll('tr')).slice(1); // 跳过表头
rows.forEach(row => {
const imgCell = row.querySelector('td:nth-child(2) img');
const titleCell = row.querySelector('td:nth-child(3)');
const dateCell = row.querySelector('td:nth-child(5)');
const sizeCell = row.querySelector('td:nth-child(6)');
const uploadsCell = row.querySelector('td:nth-child(7)');
const downloadsCell = row.querySelector('td:nth-child(8)');
if (imgCell && titleCell && dateCell && sizeCell && downloadsCell && uploadsCell) {
const item = createMasonryItem({
img: imgCell,
title: titleCell.textContent.trim(),
date: dateCell.textContent.trim(),
size: sizeCell.textContent.trim(),
downloads: downloadsCell.textContent.trim(),
uploads: uploadsCell.textContent.trim(),
originalRow: row
});
masonryContainer.appendChild(item);
}
});
// 隐藏原始表格
table.style.display = 'none';
} else {
// 恢复原始表格布局
table.style.display = '';
}
}
// 获取详情页连接
function getDetailUrl(tdElement) {
// 首先尝试找到 a 标签
const linkElement = tdElement.querySelector('a[href^="/detail/"]');
if (linkElement && linkElement.href) {
// 使用 URL 对象解析链接
try {
const url = new URL(linkElement.href);
// 确保路径以 /detail/ 开头
if (url.pathname.startsWith('/detail/')) {
return url.pathname;
}
} catch (e) {
console.error('URL解析错误:', e);
}
}
return null;
}
// 创建瀑布流项
function createMasonryItem(data) {
const item = document.createElement('div');
item.classList.add('masonry-item');
// 图片容器
const imgContainer = document.createElement('div');
imgContainer.classList.add('img-container');
// 创建新的 img 元素,而不是直接使用 data.img
const img = document.createElement('img');
img.src = data.img.src;
img.alt = data.img.alt || '';
imgContainer.appendChild(img);
// 添加预览按钮容器
const previewContainer = document.createElement('div');
previewContainer.classList.add('preview-container');
// 创建预览按钮
const previewBtn = document.createElement('button');
previewBtn.classList.add('preview-btn');
previewBtn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
预览
`;
// 绑定预览按钮点击事件
previewBtn.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止事件冒泡
showModal(data);
});
previewContainer.appendChild(previewBtn);
imgContainer.appendChild(previewContainer);
// 信息容器
const info = document.createElement('div');
info.classList.add('info');
// 标题
const title = document.createElement('div');
title.classList.add('title');
title.textContent = data.title;
// 详情
const details = document.createElement('div');
details.classList.add('details');
details.innerHTML = `
<div>📅 ${data.date}</div>
<div>💾 ${data.size}</div>
<div>⬇️ ${data.downloads}</div>
<div>⬆️ ${data.uploads}</div>
`;
// 组装
info.appendChild(title);
info.appendChild(details);
item.appendChild(imgContainer);
item.appendChild(info);
// 让整个卡片可以点击链接到原始页面(如果需要的话)
if (data.originalRow) {
const href = getDetailUrl(data.originalRow.querySelector('td:nth-child(3)'));
if (href) {
item.addEventListener('click', () => {
window.open(href, '_blank');
});
}
}
return item;
}
// 创建并显示模态框
async function showModal(data) {
// 创建遮罩层和基础结构
const overlay = document.createElement('div');
overlay.classList.add('modal-overlay');
const content = document.createElement('div');
content.classList.add('modal-content');
// 先显示基础内容
content.innerHTML = `
<div style="padding: 24px;">
<div style="text-align: center; margin-bottom: 20px;">
<img src="${data.img.src}" style="max-width: 100%; max-height: 400px; object-fit: contain;">
</div>
<h2 style="margin-bottom: 16px; color: #1a1a1a;">${data.title}</h2>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-bottom: 20px;">
<div>📅 发布日期:${data.date}</div>
<div>💾 文件大小:${data.size}</div>
<div>⬇️ 下载量:${data.downloads}</div>
<div>⬆️ 做种量:${data.uploads}</div>
</div>
</div>
`;
// 添加关闭按钮
const closeButton = document.createElement('div');
closeButton.classList.add('modal-close');
closeButton.innerHTML = '✕';
closeButton.onclick = () => {
overlay.classList.remove('active');
setTimeout(() => overlay.remove(), 300);
};
overlay.appendChild(content);
overlay.appendChild(closeButton);
document.body.appendChild(overlay);
// 显示模态框
requestAnimationFrame(() => {
overlay.classList.add('active');
});
// 添加关闭事件
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
overlay.classList.remove('active');
setTimeout(() => overlay.remove(), 300);
}
});
}
// 监听分页按钮点击事件
function setupPaginationListener() {
const pagination = document.querySelector('.pagination');
if (pagination) {
pagination.addEventListener('click', (e) => {
if (e.target.tagName === 'A' || e.target.parentElement.tagName === 'A') {
// 分页按钮点击后,等待页面内容加载完成,然后重新应用瀑布流布局
setTimeout(() => {
applyMasonryLayout();
}, 1000); // 延迟 1 秒以确保内容加载完成
}
});
}
}
// 初始化
function init() {
if (location.pathname.indexOf('/browse') !== -1) {
// 延迟 2 秒执行,确保页面内容加载完成
setTimeout(() => {
const table = document.querySelector('table');
if (table) {
loadScript();
applyMasonryLayout();
setupPaginationListener();
}
}, 3000); // 延迟 3 秒
}
}
// 监听页面变化
let oldPushState = history.pushState;
history.pushState = function () {
init();
return oldPushState.apply(history, arguments);
};
window.addEventListener('popstate', init);
// 初始化控制面板和功能
createControlPanel();
init();
})();