MTean 苹果壳 - 图片预览

MT 增加开关选项和图片预览

// ==UserScript==
// @name         MTean 苹果壳 - 图片预览
// @namespace    http://tampermonkey.net/
// @version      2025-06-21 5.0
// @description  MT 增加开关选项和图片预览
// @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/*
// @match        http*://next.m-team.cc/*
// @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_IGNORE_TIME = 'isIgnoreTimeEnabled';
	const STORAGE_KEY_PAGE_SIZE = 'pageSize';
	const STORAGE_KEY_PANEL_COLLAPSED = 'isPanelCollapsed';

	// 从 localStorage 中读取用户的选择,如果没有则使用默认值
	let isHoverEnabled = localStorage.getItem(STORAGE_KEY_HOVER) !== 'false'; // 默认值为 true
	let isSearchApiEnabled = localStorage.getItem(STORAGE_KEY_SEARCH_API) !== 'false'; // 默认值为 true
	let isIgnoreTimeEnabled = localStorage.getItem(STORAGE_KEY_IGNORE_TIME) === 'true'; // 默认值为 false
	let pageSize = localStorage.getItem(STORAGE_KEY_PAGE_SIZE) || '50'; // 默认值为 50
	let isPanelCollapsed = localStorage.getItem(STORAGE_KEY_PANEL_COLLAPSED) === 'true'; // 默认值为 false

	// 处理接口数据的方法
	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 tempData;

								if (isIgnoreTimeEnabled) {
									// 忽略时间,全部按 sum 值降序排序
									tempData = dataParse.data.data.map(item => {
										let sum = 0;
										if (item.status) {
											const seeders = parseInt(item.status.seeders, 10) || 0;
											const leechers = parseInt(item.status.leechers, 10) || 0;
											sum = seeders + leechers;
										}
										return { ...item, sum };
									}).sort((a, b) => b.sum - a.sum);
								} else {
									// 原有逻辑:24小时内优先,然后按活跃度排序
									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) {
											// 修复:确保数据转换为数字类型
											const seeders = parseInt(item.status.seeders, 10) || 0;
											const leechers = parseInt(item.status.leechers, 10) || 0;
											sum = seeders + leechers;
										}

										// 判断是否在当前时间和过去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));

									// 其他数据按 sum 值降序排序(活跃度高的在前面)
									otherData.sort((a, b) => b.sum - a.sum);

									// 合并 24小时内的数据和其他数据
									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) {
			// 如果是搜索请求,修改每页条数
			if (this.responseURL && this.responseURL.indexOf('/api/torrent/search') !== -1) {
				try {
					if (data && typeof data === 'string') {
						const requestData = JSON.parse(data);
						// 修改每页条数
						if (requestData && typeof requestData === 'object') {
							requestData.pageSize = parseInt(pageSize, 10);
							data = JSON.stringify(requestData);
						}
					}
				} catch (error) {
					console.error('Error modifying request data:', error);
				}
			}
			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.95)';
		panel.style.borderRadius = '8px';
		panel.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)';
		panel.style.minWidth = '200px';
		panel.style.fontSize = '14px';
		panel.style.overflow = 'hidden';
		panel.style.transition = 'all 0.3s ease';

		// 标题栏(可点击收起/展开)
		const titleBar = document.createElement('div');
		titleBar.style.display = 'flex';
		titleBar.style.justifyContent = 'space-between';
		titleBar.style.alignItems = 'center';
		titleBar.style.padding = '12px 15px';
		titleBar.style.backgroundColor = '#f8f9fa';
		titleBar.style.borderBottom = '1px solid #e9ecef';
		titleBar.style.cursor = 'pointer';
		titleBar.style.userSelect = 'none';

		const title = document.createElement('div');
		title.style.fontWeight = 'bold';
		title.style.color = '#333';
		title.textContent = 'MTean 控制面板';

		const toggleIcon = document.createElement('div');
		toggleIcon.style.fontSize = '12px';
		toggleIcon.style.color = '#666';
		toggleIcon.style.transition = 'transform 0.3s ease';
		toggleIcon.textContent = isPanelCollapsed ? '▼' : '▲';

		titleBar.appendChild(title);
		titleBar.appendChild(toggleIcon);

		// 内容区域
		const content = document.createElement('div');
		content.style.padding = '15px';
		content.style.display = 'flex';
		content.style.flexDirection = 'column';
		content.style.gap = '12px';
		content.style.transition = 'all 0.3s ease';

		if (isPanelCollapsed) {
			content.style.height = '0';
			content.style.padding = '0 15px';
			content.style.overflow = 'hidden';
			toggleIcon.style.transform = 'rotate(180deg)';
		}

		// 收起/展开功能
		titleBar.addEventListener('click', () => {
			isPanelCollapsed = !isPanelCollapsed;
			localStorage.setItem(STORAGE_KEY_PANEL_COLLAPSED, isPanelCollapsed);

			if (isPanelCollapsed) {
				content.style.height = '0';
				content.style.padding = '0 15px';
				content.style.overflow = 'hidden';
				toggleIcon.style.transform = 'rotate(180deg)';
				toggleIcon.textContent = '▼';
			} else {
				content.style.height = 'auto';
				content.style.padding = '15px';
				content.style.overflow = 'visible';
				toggleIcon.style.transform = 'rotate(0deg)';
				toggleIcon.textContent = '▲';
			}
		});

		// 悬停显示图片开关
		const hoverLabel = document.createElement('label');
		hoverLabel.style.display = 'flex';
		hoverLabel.style.alignItems = 'center';
		hoverLabel.style.gap = '8px';
		hoverLabel.style.cursor = 'pointer';

		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 = '8px';
		searchApiLabel.style.cursor = 'pointer';

		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 ignoreTimeLabel = document.createElement('label');
		ignoreTimeLabel.style.display = 'flex';
		ignoreTimeLabel.style.alignItems = 'center';
		ignoreTimeLabel.style.gap = '8px';
		ignoreTimeLabel.style.cursor = 'pointer';

		const ignoreTimeCheckbox = document.createElement('input');
		ignoreTimeCheckbox.type = 'checkbox';
		ignoreTimeCheckbox.checked = isIgnoreTimeEnabled;
		ignoreTimeCheckbox.onchange = (e) => {
			isIgnoreTimeEnabled = e.target.checked;
			localStorage.setItem(STORAGE_KEY_IGNORE_TIME, isIgnoreTimeEnabled);
		};

		ignoreTimeLabel.appendChild(ignoreTimeCheckbox);
		ignoreTimeLabel.appendChild(document.createTextNode('忽略时间按活跃度排序'));

		// 每页条数选择
		const pageSizeContainer = document.createElement('div');
		pageSizeContainer.style.display = 'flex';
		pageSizeContainer.style.flexDirection = 'column';
		pageSizeContainer.style.gap = '6px';

		const pageSizeLabel = document.createElement('div');
		pageSizeLabel.style.color = '#666';
		pageSizeLabel.style.fontSize = '13px';
		pageSizeLabel.textContent = '每页条数:';

		const pageSizeSelect = document.createElement('select');
		pageSizeSelect.style.padding = '4px 8px';
		pageSizeSelect.style.borderRadius = '4px';
		pageSizeSelect.style.border = '1px solid #ddd';
		pageSizeSelect.style.fontSize = '14px';
		pageSizeSelect.style.cursor = 'pointer';

		const pageSizeOptions = [
			{ value: '50', text: '50 条/页' },
			{ value: '100', text: '100 条/页' },
			{ value: '200', text: '200 条/页' }
		];

		pageSizeOptions.forEach(option => {
			const optionElement = document.createElement('option');
			optionElement.value = option.value;
			optionElement.textContent = option.text;
			optionElement.selected = pageSize === option.value;
			pageSizeSelect.appendChild(optionElement);
		});

		pageSizeSelect.onchange = (e) => {
			pageSize = e.target.value;
			localStorage.setItem(STORAGE_KEY_PAGE_SIZE, pageSize);
		};

		pageSizeContainer.appendChild(pageSizeLabel);
		pageSizeContainer.appendChild(pageSizeSelect);

		// 将所有控件添加到内容区域
		content.appendChild(hoverLabel);
		content.appendChild(searchApiLabel);
		content.appendChild(ignoreTimeLabel);
		content.appendChild(pageSizeContainer);

		// 组装面板
		panel.appendChild(titleBar);
		panel.appendChild(content);
		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 existingImages = document.querySelectorAll('.ant-image[data-mtean-loaded]');
		existingImages.forEach(img => {
			img.removeAttribute('data-mtean-loaded');
		});

		const hoverableImages = document.querySelectorAll('.ant-image');
		hoverableImages.forEach((image) => {
			// 避免重复绑定事件
			if (image.hasAttribute('data-mtean-loaded')) {
				return;
			}
			image.setAttribute('data-mtean-loaded', 'true');

			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%;z-index: 9999;';
					div.appendChild(img);
					document.body.appendChild(div);
				}
			});
		});

		// 绑定滚动事件清除图片(避免重复绑定)
		const appContent = document.getElementById('app-content');
		if (appContent && !appContent.hasAttribute('data-mtean-scroll-bound')) {
			appContent.setAttribute('data-mtean-scroll-bound', 'true');
			appContent.addEventListener('scroll', clearDom);
		}

		console.log(`MTean脚本: 已为 ${hoverableImages.length} 个图片元素绑定悬停事件`);
	}

	// 初始化
	function init() {
		if (location.pathname.indexOf('/browse') !== -1) {
			// 使用多次尝试的方式确保页面内容加载完成
			let attempts = 0;
			const maxAttempts = 10;
			const checkInterval = 500; // 每500ms检查一次

			function tryLoadScript() {
				attempts++;
				const table = document.querySelector('table');
				const hoverableImages = document.querySelectorAll('.ant-image');

				if (table && hoverableImages.length > 0) {
					// 页面元素都存在,执行loadScript
					loadScript();
					console.log(`MTean脚本初始化成功,尝试次数: ${attempts}`);
					return true;
				} else if (attempts < maxAttempts) {
					// 继续尝试
					setTimeout(tryLoadScript, checkInterval);
					return false;
				} else {
					// 达到最大尝试次数,记录失败但不影响其他功能
					console.warn('MTean脚本: 初始化超时,某些功能可能不可用');
					return false;
				}
			}

			// 开始尝试
			tryLoadScript();
		}
	}

	// 监听DOM变化,用于检测分页等动态内容更新
	function observePageChanges() {
		const targetNode = document.getElementById('app-content') || document.body;

		const observer = new MutationObserver((mutations) => {
			let shouldReinit = false;

			mutations.forEach((mutation) => {
				// 检测是否有新的表格或图片元素添加
				if (mutation.type === 'childList') {
					mutation.addedNodes.forEach((node) => {
						if (node.nodeType === Node.ELEMENT_NODE) {
							// 检查是否添加了表格或包含ant-image的元素
							if (node.querySelector && (
								node.querySelector('table') ||
								node.querySelector('.ant-image') ||
								node.tagName === 'TABLE' ||
								node.classList.contains('ant-image')
							)) {
								shouldReinit = true;
							}
						}
					});
				}
			});

			if (shouldReinit && location.pathname.indexOf('/browse') !== -1) {
				console.log('MTean脚本: 检测到页面内容变化,重新初始化...');
				// 延迟一点时间确保DOM完全更新
				setTimeout(() => {
					init();
				}, 200);
			}
		});

		observer.observe(targetNode, {
			childList: true,
			subtree: true
		});

		return observer;
	}

	// 监听页面变化
	let oldPushState = history.pushState;
	history.pushState = function () {
		setTimeout(() => {
			init();
		}, 500);
		return oldPushState.apply(history, arguments);
	};

	window.addEventListener('popstate', () => {
		setTimeout(() => {
			init();
		}, 500);
	});

	// 启动MutationObserver监听DOM变化
	let pageObserver = null;

	// 初始化控制面板和功能
	createControlPanel();
	init();

	// 启动页面变化监听
	if (location.pathname.indexOf('/browse') !== -1) {
		pageObserver = observePageChanges();
	}
})();