// ==UserScript== // @name 草榴Manager // @namespace http://tampermonkey.net/ // @version 1.8.0016 // @description 草榴搜索/板块悬停放大封面、标题预览图、品质徽章与 qBittorrent 一键发送和下载按钮。 // @author truclocphung1713 // @match https://t66y.com/search.php* // @match https://t66y.com/thread0806.php* // @match https://t66y.com/htm_data/* // @match https://t66y.com/htm_mob/* // @match http://t66y.com/search.php* // @match http://t66y.com/thread0806.php* // @match http://t66y.com/htm_data/* // @match http://t66y.com/htm_mob/* // @icon none // @run-at document-end // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_listValues // @grant unsafeWindow // @connect www.rmdown.com // @connect * // @updateURL https://raw.githubusercontent.com/truclocphung1713/CLManager/refs/heads/main/CLManager.user.js // @downloadURL https://raw.githubusercontent.com/truclocphung1713/CLManager/refs/heads/main/CLManager.user.js // ==/UserScript== (function () { 'use strict'; const pageWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; // 统一 CLM 命名空间到页面 window,确保主脚本和远程模块共享同一个对象 const CLM = pageWindow.CLM || (pageWindow.CLM = {}); if (pageWindow !== window) { window.CLM = CLM; } /** * ======================================== * 核心工具函数 * ======================================== */ /** * 检测是否是手机端页面 */ function isMobilePage() { const href = window.location.href; if (href.indexOf('mobile.php?ismobile=yes') !== -1 || href.indexOf('mobile.php?ismobile=1') !== -1) { return true; } if (href.indexOf('mobile.php?ismobile=no') !== -1 || href.indexOf('mobile.php?ismobile=0') !== -1) { return false; } if (href.indexOf('/htm_mob/') !== -1) { return true; } const viewportMeta = document.querySelector('meta[name="viewport"]'); if (viewportMeta && viewportMeta.content.indexOf('user-scalable=no') !== -1) { return true; } const mobileIndicator = document.querySelector('.mobile-only, #mobile-content, .m-header'); if (mobileIndicator) { return true; } return false; } /** * 检测页面类型 */ function detectPageType() { const href = window.location.href; const isMobile = isMobilePage(); if (href.indexOf('/htm_mob/') !== -1 || href.indexOf('/htm_data/') !== -1) { return isMobile ? 'mobile-thread' : 'desktop-thread'; } if (href.indexOf('search.php') !== -1) { return isMobile ? 'mobile-search' : 'desktop-search'; } if (href.indexOf('thread0806.php') !== -1) { return isMobile ? 'mobile-forum' : 'desktop-forum'; } return 'unknown'; } /** * 向页面注入 CSS */ function injectStyle(css) { const style = document.createElement('style'); style.type = 'text/css'; style.textContent = css; (document.head || document.getElementsByTagName('head')[0]).appendChild(style); } /** * 获取绝对 URL */ function getAbsoluteUrl(relativeUrl) { if (!relativeUrl) return ''; if (relativeUrl.startsWith('http://') || relativeUrl.startsWith('https://')) { return relativeUrl; } const base = window.location.origin; if (relativeUrl.startsWith('/')) { return base + relativeUrl; } const currentPath = window.location.pathname; const currentDir = currentPath.substring(0, currentPath.lastIndexOf('/') + 1); return base + currentDir + relativeUrl; } /** * 标准化帖子 key */ function normalizeThreadKey(threadUrl) { if (!threadUrl) return null; const match = threadUrl.match(/\/(\d+)\.html/); return match ? match[1] : null; } /** * ======================================== * Toast 通知 * ======================================== */ let toastContainer = null; let toastStyleInjected = false; function showToast(message, type = 'info') { if (!toastContainer) { toastContainer = document.createElement('div'); toastContainer.className = 'clm-toast-container'; document.body.appendChild(toastContainer); if (!toastStyleInjected) { toastStyleInjected = true; injectStyle(` .clm-toast-container { position: fixed; right: 20px; top: 80px; z-index: 100000; display: flex; flex-direction: column; gap: 10px; pointer-events: none; } .clm-toast { background: rgba(30, 30, 40, 0.95); color: #fff; padding: 12px 20px; border-radius: 8px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); font-size: 14px; line-height: 1.5; max-width: 320px; word-wrap: break-word; animation: clm-toast-slide-in 0.3s ease-out; pointer-events: auto; border-left: 4px solid #3b82f6; } .clm-toast.success { border-left-color: #10b981; } .clm-toast.error { border-left-color: #ef4444; } .clm-toast.warning { border-left-color: #f59e0b; } @keyframes clm-toast-slide-in { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes clm-toast-slide-out { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } `); } } const toast = document.createElement('div'); toast.className = `clm-toast ${type}`; toast.textContent = message; toastContainer.appendChild(toast); setTimeout(() => { toast.style.animation = 'clm-toast-slide-out 0.3s ease-in forwards'; setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, 300); }, 3000); } /** * ======================================== * 远程模块加载器 * ======================================== */ const MANIFEST_URL = 'https://raw.githubusercontent.com/truclocphung1713/CLManager/main/manifest.json'; const MODULE_CACHE_PREFIX = 'CLM_MODULE_'; const MANIFEST_CACHE_KEY = 'CLM_MANIFEST'; // 清除指定模块的所有旧版本缓存 function clearOldModuleVersions(moduleName, currentVersion) { try { const allKeys = []; // 收集所有 GM_getValue 的键 for (let i = 0; i < 1000; i++) { try { const key = GM_getValue(`__test_key_${i}__`); if (key === undefined) break; } catch (e) { break; } } // 使用 GM_listValues 如果可用 if (typeof GM_listValues === 'function') { const keys = GM_listValues(); keys.forEach(key => { if (key.startsWith(`${MODULE_CACHE_PREFIX}${moduleName}_v`) && !key.endsWith(`_v${currentVersion}`)) { console.log(`草榴Manager: 清除旧版本缓存 ${key}`); GM_deleteValue(key); } }); } } catch (e) { console.warn('草榴Manager: 清除旧版本缓存失败', e); } } async function fetchWithCache(url, cacheKey, version) { try { const cached = GM_getValue(cacheKey); if (cached) { console.log(`草榴Manager: 使用缓存 ${cacheKey}`); // 兼容旧缓存格式 {data, timestamp} 和新格式(直接字符串) try { const parsed = JSON.parse(cached); // 如果是旧格式(有 data 和 timestamp 字段) if (parsed && typeof parsed === 'object' && 'data' in parsed) { console.log(`草榴Manager: 检测到旧缓存格式,清除并重新加载 ${cacheKey}`); GM_deleteValue(cacheKey); // 继续执行下面的远程加载逻辑 } else { // 新格式:直接返回字符串 return parsed; } } catch (e) { // JSON 解析失败,可能是纯字符串,直接返回 return cached; } } } catch (e) { console.warn(`草榴Manager: 读取缓存失败 ${cacheKey}`, e); } return new Promise((resolve, reject) => { console.log(`草榴Manager: 从远程加载 ${cacheKey}`); GM_xmlhttpRequest({ method: 'GET', url: url + '?t=' + Date.now(), // 添加时间戳避免 CDN 缓存 timeout: 10000, onload: (response) => { if (response.status === 200) { const data = response.responseText; try { GM_setValue(cacheKey, JSON.stringify(data)); console.log(`草榴Manager: 缓存已保存 ${cacheKey}`); } catch (e) { console.warn(`草榴Manager: 保存缓存失败 ${cacheKey}`, e); } resolve(data); } else { reject(new Error(`HTTP ${response.status}`)); } }, onerror: (error) => reject(error), ontimeout: () => reject(new Error('请求超时')) }); }); } function shouldLoadModuleForPage(moduleTargets, pageType) { if (!moduleTargets || !Array.isArray(moduleTargets)) return false; return moduleTargets.includes(pageType); } async function initRemoteModules(pageType) { console.log(`草榴Manager: 当前页面类型 = ${pageType}`); try { const manifestText = await fetchWithCache(MANIFEST_URL, MANIFEST_CACHE_KEY); const manifest = JSON.parse(manifestText); console.log('草榴Manager: manifest 加载成功', manifest); // 使用顶层统一版本号 const globalVersion = manifest.version || '1.0.0'; const modulesToLoad = []; for (const [moduleName, moduleConfig] of Object.entries(manifest.modules || {})) { if (shouldLoadModuleForPage(moduleConfig.targets, pageType)) { modulesToLoad.push({ name: moduleName, config: moduleConfig }); } } console.log(`草榴Manager: 需要加载 ${modulesToLoad.length} 个模块`, modulesToLoad.map(m => m.name)); for (const { name, config } of modulesToLoad) { try { // 清除该模块的旧版本缓存(使用全局版本号) clearOldModuleVersions(name, globalVersion); const cacheKey = `${MODULE_CACHE_PREFIX}${name}_v${globalVersion}`; const moduleCode = await fetchWithCache(config.url, cacheKey, globalVersion); console.log(`草榴Manager: 模块 ${name} 加载成功`); // 通过往页面注入