MTean 苹果壳 - 图片预览瀑布流(延迟加载)

MT 增加开关选项和图片预览瀑布流布局,延迟加载修复初始化问题(2025-01-29)

// ==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();
})();