一键进入漫画阅读器,支持自动生成系列目录,智能匹配章节标题。
// ==UserScript== // @name Yamibo 漫画阅读器 // @namespace https://bbs.yamibo.com/ // @version 3.5.7 // @author hitori酱 // @description 一键进入漫画阅读器,支持自动生成系列目录,智能匹配章节标题。 // @match https://bbs.yamibo.com/thread-* // @match https://bbs.yamibo.com/forum.php?mod=viewthread&* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_listValues // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/cn2t.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/t2cn.js // @run-at document-end // @noframes // @license MIT License // @icon data:image/png;base64,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 // ==/UserScript== (function() { 'use strict'; // 旧版本缓存清理 const currentVersion = GM_info.script.version; const lastVersion = GM_getValue('script_version', '0.0.0'); if (currentVersion !== lastVersion) { console.log(`检测到脚本更新: 从 v${lastVersion} 更新到 v${currentVersion}`); const keys = GM_listValues(); keys.forEach(key => { if (key !== 'script_version') { // 保留版本号字段 GM_deleteValue(key); } }); console.log('已清除旧版本缓存数据'); GM_setValue('script_version', currentVersion);} // 阅读器全局变量定义 let isReading = false; let images = []; let bgIsBlack = true; let zoomLevel = 35; // 默认缩放比例35% let autoReaderEnabled = false; // 全局阅读器开关 // 目录识别相关变量 let seriesDirectory = []; let currentSeriesKey = ''; let isDirectoryMode = false; let savedThreadTitle = ''; let originalSeriesTitle = ''; let searchCache = new Map(); let directoryMemoryCache = new Map(); // 保存原始页面HTML let originalPageHTML = ''; let originalPageTitle = ''; // 事件处理与状态 let readerEventHandlers = {}; let readerEventsBound = false; let readerToolsTimer = null; let unbindReaderEvents = null; // 缓存前缀与过期时长 const CACHE_PREFIX = 'yamibo-directory-cache-'; const CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 持久化缓存索引键 const GM_INDEX_KEY = CACHE_PREFIX + 'gm-index'; // 通用存储封装:优先使用 GM_getValue/GM_setValue/GM_deleteValue(若环境支持),否则降级到 sessionStorage function storageGet(key, defaultVal = null) { try { if (typeof GM_getValue === 'function') { let val; try { val = GM_getValue(key, undefined); } catch (e) { val = undefined; } if (val === undefined) { } else if (val === null) { return defaultVal; } else if (typeof val === 'string') { try { return JSON.parse(val); } catch (e) { return val; } } else { return val; } } } catch (e) { } try { const raw = sessionStorage.getItem(key); return raw ? JSON.parse(raw) : defaultVal; } catch (e) { return defaultVal; } } function storageSet(key, value) { try { if (typeof GM_setValue === 'function') { try { GM_setValue(key, value); } catch (e) { try { GM_setValue(key, JSON.stringify(value)); } catch (ee) { throw ee; } } try { let idx = GM_getValue(GM_INDEX_KEY, undefined); if (idx === undefined || idx === null) idx = []; if (typeof idx === 'string') { try { idx = JSON.parse(idx); } catch (e) { idx = []; } } if (!Array.isArray(idx)) idx = []; if (!idx.includes(key)) { idx.push(key); GM_setValue(GM_INDEX_KEY, idx); } } catch (e) { } return true; } } catch (e) { } try { sessionStorage.setItem(key, JSON.stringify(value)); return true; } catch (e) { console.warn('⚠️ storageSet 失败:', e); return false; } } function storageRemove(key) { try { if (typeof GM_deleteValue === 'function') { try { GM_deleteValue(key); } catch (e) { try { GM_setValue(key, null); } catch (ee) {} } try { let idx = GM_getValue(GM_INDEX_KEY, undefined); if (typeof idx === 'string') { try { idx = JSON.parse(idx); } catch (e) { idx = []; } } if (!Array.isArray(idx)) idx = []; const newIdx = idx.filter(k => k !== key); GM_setValue(GM_INDEX_KEY, newIdx); } catch (e) { } return true; } } catch (e) { } try { sessionStorage.removeItem(key); return true; } catch (e) { return false; } } // 清理过期缓存:同时支持 sessionStorage 前缀与 GM 索引 function cleanExpiredCache() { let cleanedCount = 0; // 1) 清理 sessionStorage 前缀缓存 try { const sKeys = Object.keys(sessionStorage).filter(k => k.startsWith(CACHE_PREFIX)); sKeys.forEach(key => { try { const raw = sessionStorage.getItem(key); if (!raw) return; const obj = JSON.parse(raw); const ts = obj.ts || obj.timestamp || 0; if (Date.now() - ts > CACHE_EXPIRY) { sessionStorage.removeItem(key); cleanedCount++; } } catch (e) { try { sessionStorage.removeItem(key); cleanedCount++; } catch (ee) {} } }); } catch (e) { } // 2) 清理 GM_* 存储(使用索引枚举) try { if (typeof GM_getValue === 'function' && typeof GM_setValue === 'function') { let idx = GM_getValue(GM_INDEX_KEY, undefined); if (typeof idx === 'string') { try { idx = JSON.parse(idx); } catch (e) { idx = []; } } if (!Array.isArray(idx)) idx = []; const remaining = []; for (const key of idx) { try { const stored = storageGet(key, null); if (!stored) continue; const ts = stored.ts || stored.timestamp || 0; if (Date.now() - ts > CACHE_EXPIRY) { storageRemove(key); cleanedCount++; } else { remaining.push(key); } } catch (e) { try { storageRemove(key); cleanedCount++; } catch (ee) {} } } // 更新索引为剩余项 try { GM_setValue(GM_INDEX_KEY, remaining); } catch (e) {} } } catch (e) { } if (cleanedCount > 0) console.log('🧹 清理了', cleanedCount, '个过期缓存 (session+GM)'); return cleanedCount; } // 在配额不足或需要收缩时清理最旧的缓存(同时支持 sessionStorage 与 GM) function cleanOldestCache() { try { const entries = []; // 收集 sessionStorage 条目 try { Object.keys(sessionStorage).forEach(key => { if (!key.startsWith(CACHE_PREFIX)) return; try { const obj = JSON.parse(sessionStorage.getItem(key)); const ts = obj.ts || obj.timestamp || 0; entries.push({ key, ts, source: 'session' }); } catch (e) { entries.push({ key, ts: 0, source: 'session' }); } }); } catch (e) {} // 收集 GM_* 条目 via index try { if (typeof GM_getValue === 'function') { let idx = GM_getValue(GM_INDEX_KEY, undefined); if (typeof idx === 'string') { try { idx = JSON.parse(idx); } catch (e) { idx = []; } } if (!Array.isArray(idx)) idx = []; for (const key of idx) { try { const stored = storageGet(key, null); const ts = stored ? (stored.ts || stored.timestamp || 0) : 0; entries.push({ key, ts, source: 'gm' }); } catch (e) { entries.push({ key, ts: 0, source: 'gm' }); } } } } catch (e) {} if (entries.length === 0) return 0; // 按时间戳排序,最旧的在前 entries.sort((a, b) => (a.ts || 0) - (b.ts || 0)); const toDelete = Math.ceil(entries.length / 2); for (let i = 0; i < toDelete; i++) { const e = entries[i]; try { if (e.source === 'session') sessionStorage.removeItem(e.key); else storageRemove(e.key); } catch (err) {} } return toDelete; } catch (e) { console.warn('cleanOldestCache failed', e); return 0; } } // 检查是否启用了全局阅读器模式 function checkAutoReaderStatus() { const stored = localStorage.getItem('yamibo-auto-reader'); return stored === 'true'; } // 设置全局阅读器模式 function setAutoReaderStatus(enabled) { autoReaderEnabled = enabled; localStorage.setItem('yamibo-auto-reader', enabled.toString()); console.log('🔧 全局阅读器模式:', enabled ? '开启' : '关闭'); } /*** 标题标准化函数 ***/ function normalizeSeriesTitle(rawTitle) { if (!rawTitle) return ''; // 首先清理页面title的后缀 let title = rawTitle; if (title.includes(' - ')) { title = title.split(' - ')[0].trim(); } // 移除页数标注 title = title.replace(/[((]\s*\d+\s*p\s*[))]\s*$/i, ''); // 移除章节标识 - 参考脚本的模式 title = title.replace(/第[\d一二三四五六七八九十百千万〇零兩两1234567890\.]+[话話章节節回卷篇]/gi, ' '); title = title.replace(/\d+(?:\.\d+)?\s*[话話章节節回卷篇]/gi, ' '); title = title.replace(/\d+(?:\.\d+)?\s*[上下前后前後左右中篇部卷期全完]+(?:\s*[++&和及與并並,,/]\s*[上下前后前後左右中篇部卷期全完]+)+/gi, ' '); title = title.replace(/[((][上下前后前後中全完]+(?:\s*[,,++&和及與并並/]\s*[上下前后前後中全完]+)*[))]/g, ' '); title = title.replace(/\d+(?:\.\d+)?\s*[上下前后前後左右中篇部卷期全完]+/gi, ' '); title = title.replace(/(?:\s+|[-‐‑‒–—―-~~·•_、::])?\d+(?:\.\d+)*(?:\s*[上下前后前後左右中篇部卷期話话节節全完])?\s*$/g, ' '); title = title.replace(/[--—–~~\u2013\u2014\s]+$/g, ' '); title = title.replace(/[\[\]【】()()]/g, ' '); title = title.replace(/\s+/g, ' ').trim(); if (!title) { return rawTitle.trim(); } return title; } function buildSeriesKey(title) { const normalized = normalizeSeriesTitle(title || ''); const base = normalized || (title || '').trim(); return base.toLowerCase(); } function generateSeriesKey(title) { try { const k = buildSeriesKey(title || ''); // 限制长度并移除特殊字符,避免存储键过长或包含非法字符 return k.replace(/[\s\/:\\#\?&=\+%\*\|<>"'`]/g, '-').substring(0, 120); } catch (e) { return String(title || '').toLowerCase().substring(0, 120); } } // 统一的排除词汇列表,避免重复定义 const EXCLUDE_WORDS = [ '个人汉化', '汉化组', '汉化', '个人翻译', '個人翻譯', '个汉', '翻译', '生肉', '未翻译', '填坑组', '粮食组', '保护协会','創作百合', '创作百合', '熟肉', '字幕', '工作室', '提灯喵', '社团', '官方中字', '出版', '汉化工房', '猫岛汉化组', '和菓子漫画屋', '大友同好會', '透明声彩汉化组', '翻译组', '汉化委员会', '渣翻渣嵌', '合作汉化', '完结','连载','短篇', '合集','同人','raw','韩漫','杂志','英译','单行本','插画','特典','番外','外传','彩页','合订本' ].map(s => s.toLowerCase()); // 统一的文本过滤函数:检查是否为噪音词汇 function isNoiseText(text) { if (!text || typeof text !== 'string') return true; const t = text.toLowerCase(); // 检查是否包含排除词汇 for (const word of EXCLUDE_WORDS) { if (t.includes(word)) return true; } // 常见格式判断:纯数字视为噪音 if (/^\d+$/.test(t)) return true; // 智能长度检查:CJK字符2个即有效,其他字符3个以上有效 const hasCJK = /[\u4e00-\u9fa5\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF]/.test(text); const minLength = hasCJK ? 2 : 3; if (t.length < minLength) return true; return false; } // 保持向后兼容的 isGenericTag 函数 function isGenericTag(text) { return isNoiseText(text); } function getCachedDirectory(seriesKey) { // 🚫 临时禁用缓存读取,强制测试双语解析逻辑 console.log('🔄 缓存读取已禁用,将执行新的双语解析逻辑'); return null; if (!seriesKey) return null; // 1) 先检查内存缓存 try { if (directoryMemoryCache && directoryMemoryCache.has(seriesKey)) { const cached = directoryMemoryCache.get(seriesKey); if (cached && cached.ts && (Date.now() - cached.ts) < CACHE_EXPIRY) { return cached.data || cached.d || null; } else { directoryMemoryCache.delete(seriesKey); } } } catch (e) { } const key = CACHE_PREFIX + seriesKey; // 2) 尝试 GM_getValue try { if (typeof GM_getValue === 'function') { let stored = GM_getValue(key, undefined); if (typeof stored === 'string') { try { stored = JSON.parse(stored); } catch (e) { /* leave as-is */ } } if (stored && (stored.d || stored.data)) { const ts = stored.ts || stored.timestamp || 0; if (Date.now() - ts < CACHE_EXPIRY) { try { directoryMemoryCache.set(seriesKey, { d: stored.d || stored.data, data: stored.d || stored.data, ts: ts || Date.now() }); } catch (e) {} return stored.d || stored.data; } else { try { storageRemove(key); } catch (e) {} return null; } } } } catch (e) { } // 3) 尝试 sessionStorage try { const raw = sessionStorage.getItem(key); if (raw) { let obj = raw; try { obj = JSON.parse(raw); } catch (e) { obj = raw; } if (obj && (obj.d || obj.data)) { const ts = obj.ts || obj.timestamp || 0; if (Date.now() - ts < CACHE_EXPIRY) { try { directoryMemoryCache.set(seriesKey, { d: obj.d || obj.data, data: obj.d || obj.data, ts: ts || Date.now() }); } catch (e) {} return obj.d || obj.data; } else { try { sessionStorage.removeItem(key); } catch (e) {} return null; } } } } catch (e) { } return null; } function setCachedDirectory(seriesKey, directory) { if (!seriesKey || !directory) return false; const key = CACHE_PREFIX + seriesKey; const payload = { ts: Date.now(), data: directory, d: directory }; try { directoryMemoryCache.set(seriesKey, { d: directory, data: directory, ts: payload.ts }); } catch (e) {} try { if (typeof GM_setValue === 'function') { try { GM_setValue(key, payload); } catch (e) { try { GM_setValue(key, JSON.stringify(payload)); } catch (ee) { /* ignore */ } } return true; } } catch (e) { } try { sessionStorage.setItem(key, JSON.stringify(payload)); return true; } catch (e) { console.warn('⚠️ setCachedDirectory 写入失败:', e); return false; } } /*** 相似度计算辅助函数 ***/ // 简繁体标准化函数 - 使用 OpenCC 库 let converter = null; function normalizeChineseVariants(text) { try { // 初始化转换器(繁体转简体) if (!converter && typeof OpenCC !== 'undefined') { converter = OpenCC.Converter({ from: 'tw', to: 'cn' }); } if (converter) { return converter(text); } return text; } catch (error) { console.warn('简繁转换失败,使用原文本:', error); return text; } } //中外语种统一判断 function isForeignText(str) { return /[A-Za-z\u3040-\u30FF\uAC00-\uD7AF]/.test(str) && !/[\u4e00-\u9fa5]/.test(str); } // 提取有意义的关键词(排除常见无关词汇) function extractMeaningfulKeywords(title) { const core = extractCoreWorkName(title); if (typeof core === 'object' && core._dualLanguage) { return [core._dualLanguage.chinese, core._dualLanguage.foreign].filter(Boolean); } if (Array.isArray(core)) { return core.filter(Boolean); } return [core.toString().trim()]; } // 生成搜索关键词 function generateSearchTerms(seriesTitle) { if (!seriesTitle) return []; const core = extractCoreWorkName(seriesTitle); if (typeof core === 'object' && core._dualLanguage) { return [core._dualLanguage.foreign, core._dualLanguage.chinese].filter(Boolean); } if (Array.isArray(core)) { return core.filter(Boolean); } return [core.toString().trim()]; } // 计算核心作品名相似度 function calculateCoreWorkSimilarity(core1, core2) { const arr1 = Array.isArray(core1) ? core1 : [core1]; const arr2 = Array.isArray(core2) ? core2 : [core2]; let maxScore = 0; for (const s1 of arr1) { for (const s2 of arr2) { const a = s1.toString().trim().toLowerCase(); const b = s2.toString().trim().toLowerCase(); if (!a || !b) continue; if (a === b) return 1.0; if (a.includes(b) || b.includes(a)) { const longer = a.length > b.length ? a : b; const shorter = a.length <= b.length ? a : b; maxScore = Math.max(maxScore, shorter.length / longer.length * 0.95); } // 最长公共子串 let maxCommonLength = 0; for (let i = 0; i < a.length; i++) { for (let j = 0; j < b.length; j++) { let commonLength = 0; while (i + commonLength < a.length && j + commonLength < b.length && a[i + commonLength] === b[j + commonLength]) { commonLength++; } maxCommonLength = Math.max(maxCommonLength, commonLength); } } const minLength = Math.min(a.length, b.length); if (maxCommonLength >= 4) { const similarity = (maxCommonLength / minLength) * 0.9; maxScore = Math.max(maxScore, similarity); } } } return maxScore; } // 检查关键词匹配(排除翻译组等无关信息) function checkKeywordMatch(core1, core2) { const kws1 = extractMeaningfulKeywords(core1); const kws2 = extractMeaningfulKeywords(core2); let bestMatch = 0; for (const kw1 of kws1) { for (const kw2 of kws2) { if (kw1.length >= 2 && kw2.length >= 2) { if (kw1 === kw2) { return 0.95; } else if (kw1.includes(kw2) || kw2.includes(kw1)) { const longer = kw1.length > kw2.length ? kw1 : kw2; const shorter = kw1.length <= kw2.length ? kw1 : kw2; const match = shorter.length / longer.length; bestMatch = Math.max(bestMatch, match * 0.9); } } } } return bestMatch; } // 提取核心作品名 function extractCoreWorkName(title) { console.log(` 🔍 重构版标题处理: "${title}"`); if (!title || typeof title !== 'string') return ''; // === 0. 基础常量定义 === const FATAL_KEYWORDS = [ '汉化', '翻译', '个人', '组', '工作室', '社团', '同人', '英译', '中字', '生肉', 'raw', 'sample', '合同志', '填坑', '粮食', '发布', '校对', '嵌字', '图源', '扫图', '合集', '短篇', '短篇集', '连载' ]; const NUM_CHAR = '[\\d①-⑳一二三四五六七八九十百千万零〇]'; function getScriptType(str) { const hasKana = /[\u3040-\u30FF]/.test(str); const hasHanzi = /[\u4e00-\u9fa5]/.test(str); const hasLatin = /[a-zA-Z]/.test(str); const hasKorean = /[\uAC00-\uD7AF]/.test(str); if (hasKorean) return 'KR'; if (hasKana) return 'JP'; if (hasHanzi && !hasKana) return 'ZH'; if (hasLatin) return 'EN'; return 'OTHER'; } function isFatalNoise(text) { const t = text.toLowerCase(); return FATAL_KEYWORDS.some(k => t.includes(k)); } // === 1. 去头:移除左侧的方框元数据 === let body = title.trim(); while ( body.match(/^(\s*【[^】]*】\s*)+/) || body.match(/^(\s*\[[^\]]*\]\s*)+/) || body.match(/^(\s*[\[【((][^\]】))]*[\]】))]\s*)+/) ) { body = body .replace(/^(\s*【[^】]*】\s*)+/g, '') .replace(/^(\s*\[[^\]]*\]\s*)+/g, '') .replace(/^(\s*[\[【((][^\]】))]*[\]】))]\s*)+/g, '') .replace(/^[\]】))]\s*/, '') .trim(); } if (!body) body = title.trim(); // === 1.5 枢轴切割 (处理中间的汉化组方框) === const pivotRegex = new RegExp(`[【\\[][^\\]】]*(?:${FATAL_KEYWORDS.join('|')})[^\\]】]*[】\\]]`, 'gi'); const pivotParts = body.split(pivotRegex); if (pivotParts.length > 1) { const lastSegment = pivotParts[pivotParts.length - 1].trim(); if (/[a-zA-Z0-9\u4e00-\u9fa5]/.test(lastSegment)) { console.log(` ✂️ Step 1.5 枢轴切割: 丢弃左侧元数据,保留 "${lastSegment}"`); body = lastSegment; } } else { console.log(` ✂️ Step 1 去头结果: "${body}"`); } // === 2. 断尾:通过章节词切割 === let coreCandidate = body; const explicitChapterPattern = new RegExp( `(?:\\s|^)(第?${NUM_CHAR}{1,4}[话話章节節回卷篇期部]|Vol\\.?\\s*${NUM_CHAR}+|[##]${NUM_CHAR}+|Part\\.?\\s*${NUM_CHAR}+)`, 'i' ); const chapMatch = body.match(explicitChapterPattern); if (chapMatch) { const potentialTitle = body.substring(0, chapMatch.index).trim(); if (potentialTitle.length >= 1) { console.log(` 🗡️ Step 2 章节切割: 保留左侧 "${potentialTitle}"`); coreCandidate = potentialTitle; } } else { const spaceNumMatch = body.match(/\s+(\d+)(\s+|$)/); if (spaceNumMatch) { const potentialTitle = body.substring(0, spaceNumMatch.index).trim(); if (potentialTitle.length >= 2) { console.log(` 🗡️ Step 2 隐式数字切割: 保留左侧 "${potentialTitle}"`); coreCandidate = potentialTitle; } } } // === 3. 分词 (含智能连字符处理) === // [核心修正] 智能将 "JP-CN" 或 "Title-标题" 中的连字符替换为 / // 保护 "Spider-Man" (EN-EN) coreCandidate = coreCandidate.replace(/\s+[-_—–]\s+/g, '/'); // 空格包围的连字符 coreCandidate = coreCandidate.replace(/([^\x00-\x7F])\s*[-_—–]\s*([^\x00-\x7F])/g, '$1/$2'); // CJK-CJK coreCandidate = coreCandidate.replace(/([^\x00-\x7F])\s*[-_—–]\s*([a-zA-Z0-9])/g, '$1/$2'); // CJK-EN coreCandidate = coreCandidate.replace(/([a-zA-Z0-9])\s*[-_—–]\s*([^\x00-\x7F])/g, '$1/$2'); // EN-CJK const SPLIT_RE = /[\/||/()()\[\]【】]+/; let rawParts = coreCandidate.split(SPLIT_RE).map(p => p.trim()).filter(Boolean); // === 4. 清洗 === function cleanTitlePart(str) { if (!str) return ''; let s = str.trim(); if (isFatalNoise(s)) return ''; s = s.replace(/「[^」]*」/g, ''); s = s.replace(/『[^』]*』/g, ''); s = s.replace(/C\d{2,3}/ig, ''); // 移除 C106 for (const word of EXCLUDE_WORDS) { s = s.replace(new RegExp(word, 'ig'), ''); } s = s.replace(new RegExp(`第?${NUM_CHAR}{1,4}[话話章节節回卷篇期部](?:[\\s\\d]+)?`, 'g'), ''); s = s.replace(new RegExp(`其${NUM_CHAR}+`, 'g'), ''); s = s.replace(new RegExp(`Vol\\.?${NUM_CHAR}+`, 'ig'), ''); s = s.replace(new RegExp(`[##]${NUM_CHAR}+`, 'g'), ''); s = s.trim(); s = s.replace(/v\d+(\.\d+)?/ig, ''); s = s.replace(new RegExp(`\\s+${NUM_CHAR}+(\\.\\d+)?$`, 'g'), ''); s = s.replace(new RegExp(`(${NUM_CHAR}+(\\.\\d+)?)$`, 'g'), ''); return s.trim(); } let validParts = []; let seen = new Set(); for (const p of rawParts) { const cleaned = cleanTitlePart(p); const isCJK = /[\u4e00-\u9fa5\u3040-\u30FF]/.test(cleaned); if (cleaned && !/^\d+$/.test(cleaned) && !seen.has(cleaned)) { if ((isCJK && cleaned.length >= 2) || (!isCJK && cleaned.length >= 3)) { validParts.push(cleaned); seen.add(cleaned); } } } console.log(` 🧺 Step 3 清洗后候选词: ${JSON.stringify(validParts)}`); // === 5. 输出 === const zhParts = []; const foreignParts = []; for (const part of validParts) { const type = getScriptType(part); if (type === 'ZH') zhParts.push(part); else if (type === 'JP' || type === 'EN' || type === 'KR') foreignParts.push(part); else { if (/[\u4e00-\u9fa5]/.test(part)) zhParts.push(part); else foreignParts.push(part); } } if (zhParts.length > 0 && foreignParts.length > 0) { const bestChinese = zhParts.sort((a, b) => b.length - a.length)[0]; const bestForeign = foreignParts[0]; const result = new String(bestChinese); result._dualLanguage = { chinese: bestChinese, foreign: bestForeign }; return result; } if (zhParts.length > 0) { if (zhParts.length >= 2) { const result = zhParts; result._multiChinese = true; return result; } return zhParts[0]; } if (foreignParts.length > 0) { if (foreignParts.length >= 2) { if (foreignParts[1].includes(foreignParts[0])) return foreignParts[1]; if (foreignParts[0].includes(foreignParts[1])) return foreignParts[0]; const result = [foreignParts[0], foreignParts[1]]; result._multiForeign = true; return result; } return foreignParts[0]; } if (validParts.length === 0) { return coreCandidate || body; } return validParts[0]; } // 智能相似度匹配函数 - 专注核心作品名匹配 function calculateSimilarity(searchTitle, resultTitle) { console.log(`🔄 计算相似度: "${searchTitle}" vs "${resultTitle}"`); // 提取两个标题的核心作品名(去除翻译组、章节号等干扰信息) const searchCore = extractCoreWorkName(searchTitle); const resultCore = extractCoreWorkName(resultTitle); console.log(` 📝 搜索核心: "${searchCore}"`); console.log(` 📝 结果核心: "${resultCore}"`); // 调试:检查是否有双语信息 if (typeof searchCore === 'object' && searchCore._dualLanguage) { console.log(` 🌐 搜索核心双语信息: 中文="${searchCore._dualLanguage.chinese}" 外文="${searchCore._dualLanguage.foreign}"`); } if (typeof resultCore === 'object' && resultCore._dualLanguage) { console.log(` 🌐 结果核心双语信息: 中文="${resultCore._dualLanguage.chinese}" 外文="${resultCore._dualLanguage.foreign}"`); } // 智能长度检查:CJK字符2个即有效,英文需要3个 function isValidCore(core) { if (!core) return false; const coreStr = core.toString(); const hasCJK = /[\u4e00-\u9fa5\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF]/.test(coreStr); return hasCJK ? coreStr.length >= 2 : coreStr.length >= 3; } if (!isValidCore(searchCore) || !isValidCore(resultCore)) { console.log(` ❌ 核心标题无效,跳过`); return 0; } // 检查是否有关键词直接匹配(排除翻译组等无关词汇) const keywordMatch = checkKeywordMatch(searchCore, resultCore); if (keywordMatch > 0) { console.log(` ✅ 关键词匹配度: ${(keywordMatch * 100).toFixed(1)}%`); return keywordMatch; } // 计算核心作品名的相似度 const similarity = calculateCoreWorkSimilarity(searchCore, resultCore); console.log(` ✅ 核心作品相似度: ${(similarity * 100).toFixed(1)}%`); return similarity; } /*** 目录识别功能 ***/ async function searchSeriesDirectory(seriesTitle) { if (!seriesTitle) { console.log('❌ 搜索失败: 标题为空'); return []; } console.log('🔍 开始搜索系列目录:', seriesTitle); // 动态获取当前帖子所属版块fid(30/37等) const currentForumId = detectForumId(); const forumName = getForumName(currentForumId); console.log(`🎯 搜索限定在: ${forumName} (fid=${currentForumId})`); // 生成搜索关键词 let searchTerms = generateSearchTerms(seriesTitle); // 检查是否有双语搜索标记(仅在真正的双语对照时启用) if (searchTerms && searchTerms._dualLanguageSearch && !searchTerms._pureEnglish) { const { chinese, foreign } = searchTerms._dualLanguageSearch; console.log(`🌐 检测到中外文对照,启用双语搜索: 中文"${chinese}" vs 外文"${foreign}"`); try { // 分别搜索中文和外文,比较结果数量 const chineseResults = await performSingleSearch(chinese, currentForumId); const foreignResults = await performSingleSearch(foreign, currentForumId); console.log(`📊 搜索结果对比: 中文"${chinese}"=${chineseResults.length}个 / 外文"${foreign}"=${foreignResults.length}个`); // 根据搜索结果优化搜索词顺序 if (foreignResults.length > chineseResults.length) { console.log(`✅ 外文搜索结果更多,优先使用外文关键词: "${foreign}"`); searchTerms = [foreign, chinese]; } else { console.log(`✅ 中文搜索结果更优,优先使用中文关键词: "${chinese}"`); searchTerms = [chinese, foreign]; } delete searchTerms._dualLanguageSearch; console.log('🎯 双语搜索优化后关键词:', searchTerms); } catch (error) { console.warn('⚠️ 双语搜索比较失败,使用默认策略:', error); delete searchTerms._dualLanguageSearch; } } if (!searchTerms || searchTerms.length === 0) { searchTerms = generateSmartSearchTerms(seriesTitle); console.log('📝 使用传统搜索逻辑生成关键词:', searchTerms); } console.log('📝 使用搜索方式获取完整系列目录...'); const searchResults = new Map(); try { const cacheKey = seriesTitle; if (searchCache.has(cacheKey)) { console.log('📦 使用内存缓存结果:', searchCache.get(cacheKey).length, '个章节'); return searchCache.get(cacheKey); } // 检查持久化缓存 const seriesKey = generateSeriesKey(seriesTitle); const cachedDirectory = getCachedDirectory(seriesKey); if (cachedDirectory && cachedDirectory.length > 0) { console.log('📦 使用持久化缓存结果:', cachedDirectory.length, '个章节'); searchCache.set(cacheKey, cachedDirectory); return cachedDirectory; } // 串行遍历2个关键词分别搜索,合并去重 let consecutiveFailures = 0; const maxFailures = 1; for (let i = 0; i < Math.min(searchTerms.length, 2); i++) { const searchTerm = searchTerms[i]; if (!searchTerm) continue; // 如果不是第一个搜索,等待12秒避免频率限制 if (i > 0) { console.log(`⏱️ 等待12秒后进行下一个搜索 (${i+1}/2)...`); await new Promise(resolve => setTimeout(resolve, 12000)); } try { console.log('🔎 当前搜索关键词:', searchTerm); // ...原有的搜索请求逻辑... console.log('📝 获取搜索页面...'); const searchPageResponse = await fetch('https://bbs.yamibo.com/search.php', { method: 'GET', credentials: 'include', headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache', 'Referer': window.location.href, 'User-Agent': navigator.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' } }); let formHash = ''; if (searchPageResponse.ok) { const searchPageHtml = await searchPageResponse.text(); const formHashMatch = searchPageHtml.match(/name="formhash"\s+value="([^"]+)"/); if (formHashMatch) { formHash = formHashMatch[1]; console.log('✅ 获取到新的FormHash:', formHash); } } // 等待一小段时间模拟人工操作 await new Promise(resolve => setTimeout(resolve, Math.random() * 2000 + 1000)); // 使用动态获取的fid const formData = new FormData(); formData.append('formhash', formHash); formData.append('srchtxt', searchTerm); formData.append('srchfid[]', currentForumId); // 动态版块ID formData.append('orderby', 'dateline'); formData.append('ascdesc', 'desc'); formData.append('searchsubmit', 'yes'); console.log('🔍 发送搜索请求...'); console.log('📝 搜索参数详情:', { formhash: formHash, srchtxt: searchTerm, srchfid: currentForumId, orderby: 'dateline', ascdesc: 'desc' }); // 先尝试GET搜索(模拟手动搜索) const encodedSearchTerm = encodeURIComponent(searchTerm); const getUrl = `https://bbs.yamibo.com/search.php?mod=forum&srchtxt=${encodedSearchTerm}&formhash=${formHash}&srchfid=${currentForumId}&orderby=dateline&ascdesc=desc&searchsubmit=yes`; console.log('🌐 尝试GET搜索URL:', getUrl); let response = await fetch(getUrl, { method: 'GET', credentials: 'include', headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3', 'Cache-Control': 'no-cache', 'Referer': 'https://bbs.yamibo.com/search.php', 'User-Agent': navigator.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' } }); // 如果GET失败,回退到POST if (!response.ok) { console.log('❌ GET搜索失败,尝试POST搜索...'); response = await fetch('https://bbs.yamibo.com/search.php?mod=forum', { method: 'POST', credentials: 'include', headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3', 'Origin': 'https://bbs.yamibo.com', 'Referer': 'https://bbs.yamibo.com/search.php', 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-User': '?1', 'Upgrade-Insecure-Requests': '1', 'User-Agent': navigator.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' }, body: formData }); } else { console.log('✅ GET搜索请求成功'); } if (!response.ok) { console.log(`❌ 搜索请求失败: ${response.status} - ${response.statusText}`); consecutiveFailures++; if (response.status === 503) { console.log('⚠️ 搜索服务暂时不可用,可能是论坛维护中'); } continue; } const html = await response.text(); if (html.includes('需要登录')) { console.log('❌ 需要登录'); continue; } const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // 调试:检查页面标题和主要内容 const pageTitle = doc.querySelector('title')?.textContent || '无标题'; console.log('📄 搜索结果页面标题:', pageTitle); // 检查是否有搜索结果统计信息 const resultStats = doc.querySelector('.pg .xi2, .ptm .xi2, .pages .xi2'); if (resultStats) { console.log('📊 搜索统计信息:', resultStats.textContent.trim()); } // 改进的搜索结果提取 - 支持多页和多种选择器 const pageSearchResults = []; const selectors = [ '#threadlist li.pbw h3.xs3 a', '#threadlist ul li.pbw h3.xs3 a', '#threadlist .slst ul li.pbw h3 a', '#threadlist li h3.xs3 a', '#threadlist .xst', 'a.xst', '.xst', 'a[href*="thread-"]', 'a[href*="tid="]', '.s.xst', '#threadlist tbody .subject a', '.threadlist .title a', 'h3 a[href*="thread"]', 'tbody[id^="normalthread"] .xst', 'tbody[id^="normalthread"] a[href*="thread"]', '.ptm .xst', '#threadlisttableid .xst', '#threadlisttableid a[href*="thread"]', '.bl .xst', 'table .subject a', '.ptm a[href*="thread"]' ]; let foundResults = false; selectors.forEach(selector => { const elements = doc.querySelectorAll(selector); if (elements.length > 0) { console.log(`✅ 选择器 "${selector}" 找到 ${elements.length} 个结果`); pageSearchResults.push(...Array.from(elements)); foundResults = true; } }); // 去重处理 const uniqueResults = []; const seenUrls = new Set(); pageSearchResults.forEach(link => { const href = link.href; if (href && !seenUrls.has(href)) { seenUrls.add(href); uniqueResults.push(link); } }); // 检查是否有下一页,并获取前2页的结果 const hasNextPage = doc.querySelector('.nxt, .next, a[href*="page=2"]'); if (hasNextPage && uniqueResults.length > 0) { console.log('📄 检测到搜索结果有多页,尝试获取第2页...'); try { await new Promise(resolve => setTimeout(resolve, 2000)); const currentUrl = new URL(response.url); currentUrl.searchParams.set('page', '2'); const page2Response = await fetch(currentUrl.toString(), { method: 'GET', credentials: 'include', headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3', 'Referer': response.url, 'User-Agent': navigator.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' } }); if (page2Response.ok) { const page2Html = await page2Response.text(); const page2Doc = new DOMParser().parseFromString(page2Html, 'text/html'); const page2Results = []; selectors.forEach(selector => { const elements = page2Doc.querySelectorAll(selector); if (elements.length > 0) { page2Results.push(...Array.from(elements)); } }); page2Results.forEach(link => { const href = link.href; if (href && !seenUrls.has(href)) { seenUrls.add(href); uniqueResults.push(link); } }); console.log(`✅ 第2页找到额外 ${page2Results.length} 个结果,总计 ${uniqueResults.length} 个`); } else { console.log('❌ 获取第2页失败:', page2Response.status); } } catch (error) { console.warn('⚠️ 获取第2页时出错:', error.message); } } // 使用去重后的结果 const finalResults = uniqueResults; const currentResults = finalResults.map(link => { const originalTitle = link.textContent.trim(); let url = link.href; if (!originalTitle || originalTitle === '快速') return null; if (url.startsWith('/')) { url = `https://bbs.yamibo.com${url}`; } else if (url.startsWith('forum.php')) { url = `https://bbs.yamibo.com/${url}`; } const threadId = url.match(/tid=(\d+)/)?.[1] || url.match(/thread-(\d+)-/)?.[1]; if (!threadId) return null; // 直接使用原始标题进行相似度计算,不要预先清理 const similarity = calculateSimilarity(seriesTitle, originalTitle); console.log(` [DEBUG] "${originalTitle}" 相似度: ${similarity}`); return { threadId, title: originalTitle, originalTitle: originalTitle, url, normalizedTitle: normalizeSeriesTitle(originalTitle), similarity, searchTerm }; }).filter(Boolean); // 合并当前搜索结果(以 threadId 去重,保留相似度高的) currentResults.forEach(item => { const existing = searchResults.get(item.threadId); if (!existing || item.similarity > existing.similarity) { searchResults.set(item.threadId, item); } }); console.log(`✅ 搜索词 "${searchTerm}" 完成,找到 ${currentResults.length} 个结果`); consecutiveFailures = 0; } catch (error) { console.warn(`❌ 搜索词 "${searchTerm}" 失败:`, error.message); consecutiveFailures++; } } // 智能筛选和排序 let directory = Array.from(searchResults.values()) .filter(item => item.similarity > 0.7); // 简单按发帖时间排序(threadId小的是老帖子,排在前面) directory.sort((a, b) => { const threadIdA = parseInt(a.threadId); const threadIdB = parseInt(b.threadId); return threadIdA - threadIdB; }); console.log(`📊 智能筛选后: 找到 ${directory.length} 个相关章节`); directory.forEach(item => { const chapterNum = extractChapterNumber(item.title); console.log(` "${item.title}" (章节号: ${chapterNum}, 相似度: ${(item.similarity * 100).toFixed(1)}%)`); }); if (directory.length > 0) { // 缓存成功的搜索结果 searchCache.set(cacheKey, directory); // 同时保存到持久化缓存 const seriesKey = generateSeriesKey(seriesTitle); setCachedDirectory(seriesKey, directory); return directory; } console.log('❌ 所有搜索词都未找到相关章节'); // 只有在第一轮搜索完全失败时才尝试备用策略 if (directory.length === 0 && consecutiveFailures < maxFailures) { console.log('🔄 第一轮搜索无结果,尝试简单备用搜索...'); // 生成一个简单的备用搜索词 let backupTerm = ''; // 优先提取最长的中文片段(过滤噪音词) const chineseMatches = seriesTitle.match(/[\u4e00-\u9fa5]{4,}/g); if (chineseMatches && chineseMatches.length > 0) { // 找到第一个不是噪音的中文片段 const validChinese = chineseMatches .sort((a, b) => b.length - a.length) .find(match => !isNoiseText(match)); if (validChinese) { backupTerm = validChinese.substring(0, 5); console.log('🔤 备用搜索使用中文片段:', backupTerm); } else { console.log('❌ 所有中文片段都被判定为噪音,跳过备用搜索'); } } else { // 如果没有中文,提取外文片段 const foreignMatches = seriesTitle.match(/[A-Za-z\u3040-\u30FF\uAC00-\uD7AF]{3,}/g); if (foreignMatches && foreignMatches.length > 0) { const validForeign = foreignMatches.find(match => !isNoiseText(match)); if (validForeign) { backupTerm = validForeign.substring(0, 5); console.log('🔤 备用搜索使用外文片段:', backupTerm); } else { console.log('❌ 所有外文片段都被判定为噪音,跳过备用搜索'); } } } if (backupTerm) { try { console.log('🔍 执行备用搜索:', backupTerm); // 等待10秒 await new Promise(resolve => setTimeout(resolve, 10000)); const formData = new FormData(); formData.append('formhash', getFormHash() || ''); formData.append('srchtxt', backupTerm); formData.append('srchfid[]', '30'); formData.append('orderby', 'dateline'); formData.append('ascdesc', 'desc'); formData.append('searchsubmit', 'yes'); const response = await fetch('https://bbs.yamibo.com/search.php?mod=forum', { method: 'POST', credentials: 'include', headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3', 'Referer': 'https://bbs.yamibo.com/search.php', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' }, body: formData }); if (response.ok) { const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const backupPageResults = []; const threadList = doc.querySelectorAll('#threadlist li.pbw h3.xs3 a'); if (threadList.length > 0) { backupPageResults.push(...Array.from(threadList)); } else { backupPageResults.push(...Array.from(doc.querySelectorAll('a.xst, a[href*="thread-"]'))); } const backupResults = backupPageResults.map(link => { const title = link.textContent.trim(); let url = link.href; if (!title || title === '快速') return null; if (url.startsWith('/')) { url = `https://bbs.yamibo.com${url}`; } else if (url.startsWith('forum.php')) { url = `https://bbs.yamibo.com/${url}`; } const threadId = url.match(/tid=(\d+)/)?.[1] || url.match(/thread-(\d+)-/)?.[1]; if (!threadId) return null; const similarity = calculateSimilarity(seriesTitle, title); return { threadId, title, originalTitle: title, url, normalizedTitle: normalizeSeriesTitle(title), similarity, searchTerm: `backup:${backupTerm}` }; }).filter(Boolean); backupResults.forEach(item => { const existing = searchResults.get(item.threadId); if (!existing || item.similarity > existing.similarity) { searchResults.set(item.threadId, item); } }); console.log(`✅ 备用搜索找到 ${backupResults.length} 个结果`); directory = Array.from(searchResults.values()) .filter(item => item.similarity > 0.05); } else { console.log(`❌ 备用搜索失败: ${response.status}`); } } catch (error) { console.warn('❌ 备用搜索失败:', error.message); } } } // 如果搜索完全失败 if (directory.length === 0) { console.log('❌ 所有搜索词都未找到相关章节'); } // 最后兜底:提供当前章节作为基础目录 const currentThreadId = getCurrentThreadId(); if (directory.length === 0 && currentThreadId) { console.log('🔧 提供当前章节作为基础目录'); return [{ threadId: currentThreadId, title: seriesTitle, originalTitle: seriesTitle, url: window.location.href, normalizedTitle: normalizeSeriesTitle(seriesTitle), similarity: 1.0, searchTerm: 'current' }]; } return directory; } catch (error) { console.error('❌ 搜索目录失败:', error); console.error('错误堆栈:', error.stack); return []; } } // 提取章节号(支持小数格式和复杂排序) function extractChapterNumber(title) { if (!title || typeof title !== 'string') return 0; // 特殊章节(番外/彩页等)放到最后 const specialPatterns = [ /番外|外传|特别篇|SP|特典|彩页|后记|前记|预告|PV|CM/i, /extra|special|omake|bonus/i ]; for (const p of specialPatterns) { if (p.test(title)) return 99999; } const patterns = [ { regex: /(?:第\s*)?(\d+\.\d+)\s*[话話章节節回卷篇期]?/i, priority: 1, type: 'decimal' }, { regex: /第\s*(\d+)\s*[话話章节節回卷篇]/i, priority: 2, type: 'standard' }, { regex: /(\d+)\s*[话話章节節]/i, priority: 3, type: 'numbered' }, { regex: /[\[\((](\d+(?:\.\d+)?)[\]\))]/, priority: 4, type: 'bracketed' }, { regex: /(?:第\s*)?(\d+)\s*卷/i, priority: 5, type: 'volume' }, { regex: /\s(\d+(?:\.\d+)?)$/, priority: 6, type: 'trailing' }, { regex: /^(\d+(?:\.\d+)?)\s/, priority: 7, type: 'leading' }, { regex: /(\d+(?:\.\d+)?)/, priority: 8, type: 'any' } ]; let bestMatch = null; let bestPriority = Infinity; for (const p of patterns) { const m = title.match(p.regex); if (m && p.priority < bestPriority) { bestMatch = m[1]; bestPriority = p.priority; } } if (bestMatch != null) { if (bestMatch.includes('.')) { // 小数章如 9.5 -> 9500(保持整数比较) const v = parseFloat(bestMatch); if (!isNaN(v)) return Math.round(v * 1000); } else { const v = parseInt(bestMatch, 10); if (!isNaN(v)) return v; } } // 简单中文数字映射(可扩展) const chineseNumbers = { '零': 0, '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9, '十': 10, '十一': 11, '十二': 12, '十三': 13, '十四': 14, '十五': 15, '十六': 16, '十七': 17, '十八': 18, '十九': 19, '二十': 20 }; for (const [ch, num] of Object.entries(chineseNumbers)) { if (title.includes(`第${ch}`) || title.includes(`${ch}话`) || title.includes(`${ch}章`)) { return num; } } return 0; } function getFormHash() { if (isReading) { console.log('🔑 FormHash: 阅读模式下跳过FormHash'); return ''; } const formHashInput = document.querySelector('input[name="formhash"]'); return formHashInput ? formHashInput.value : ''; } // 从URL识别帖子所属版块 function detectForumId(url = window.location.href) { // 方式1: 从URL参数中提取 fid const fidMatch = url.match(/[?&]fid=(\d+)/); if (fidMatch) { const fid = fidMatch[1]; console.log(`📍 从URL参数识别版块: fid=${fid}`); return fid; } // 方式2: 从forum.php路径中提取 const forumMatch = url.match(/forum-(\d+)-/); if (forumMatch) { const fid = forumMatch[1]; console.log(`📍 从forum路径识别版块: fid=${fid}`); return fid; } // 方式3: 从thread URL中推断(访问页面时检查页面元素) try { const breadcrumbs = document.querySelectorAll('.z a[href*="forum"]'); const forumIds = []; for (const breadcrumb of breadcrumbs) { // 尝试从 forum-数字- 格式提取 const breadcrumbMatch1 = breadcrumb.href.match(/forum-(\d+)-/); if (breadcrumbMatch1) { const fid = breadcrumbMatch1[1]; const name = breadcrumb.textContent.trim(); forumIds.push({ fid, name, element: breadcrumb }); continue; } // 尝试从 fid= 参数提取 const breadcrumbMatch2 = breadcrumb.href.match(/fid=(\d+)/); if (breadcrumbMatch2) { const fid = breadcrumbMatch2[1]; const name = breadcrumb.textContent.trim(); forumIds.push({ fid, name, element: breadcrumb }); } } // ✨ 选择最后一个版块(最精确的子版块) if (forumIds.length > 0) { const lastForum = forumIds[forumIds.length - 1]; console.log(`📍 从面包屑导航识别版块: fid=${lastForum.fid} (${lastForum.name})`); if (forumIds.length > 1) { console.log(` 💡 跳过了 ${forumIds.length - 1} 个父级版块,选择最精确的子版块`); } return lastForum.fid; } } catch (e) { console.log('⚠️ 无法从面包屑导航识别版块:', e.message); } // 方式4: 从当前页面的返回链接中提取 try { const backLinks = document.querySelectorAll('a[href*="forum"]'); for (const backLink of backLinks) { const linkMatch = backLink.href.match(/fid=(\d+)/); if (linkMatch) { const fid = linkMatch[1]; console.log(`📍 从返回链接识别版块: fid=${fid}`); return fid; } } } catch (e) { // ignore } // 默认返回中文百合漫画区 (fid=30) console.log('📍 无法识别版块,默认使用中文百合漫画区 (fid=30)'); return '30'; } // 获取版块名称(用于日志显示) function getForumName(fid) { const forumNames = { '30': '中文百合漫画区', '37': '百合漫画图源区' }; return forumNames[fid] || `版块${fid}`; } // 检查两个标题是否包含相似词汇 function containsSimilarWords(title1, title2) { const words1 = title1.toLowerCase().split(/[\s\-_]+/).filter(w => w.length > 1); const words2 = title2.toLowerCase().split(/[\s\-_]+/).filter(w => w.length > 1); console.log(`🔍 词汇比较: "${title1}" vs "${title2}"`); console.log(` 词汇1: [${words1.join(', ')}]`); console.log(` 词汇2: [${words2.join(', ')}]`); let commonWords = 0; const matchedPairs = []; for (const word1 of words1) { for (const word2 of words2) { if (word1.includes(word2) || word2.includes(word1)) { commonWords++; matchedPairs.push(`"${word1}" ⟷ "${word2}"`); break; } } } const threshold = Math.min(2, Math.min(words1.length, words2.length)); const isMatch = commonWords >= threshold; console.log(` 匹配词汇: ${matchedPairs.join(', ')}`); console.log(` 匹配数量: ${commonWords}/${threshold} (需要: ${threshold})`); console.log(` 结果: ${isMatch ? '✅ 匹配' : '❌ 不匹配'}`); return isMatch; } /*** 悬浮按钮 ***/ const ioniconsModule = document.createElement('script'); ioniconsModule.type = 'module'; ioniconsModule.src = 'https://unpkg.com/[email protected]/dist/ionicons/ionicons.esm.js'; document.head.appendChild(ioniconsModule); const ioniconsNomodule = document.createElement('script'); ioniconsNomodule.noModule = true; ioniconsNomodule.src = 'https://unpkg.com/[email protected]/dist/ionicons/ionicons.js'; document.head.appendChild(ioniconsNomodule); const fontLink = document.createElement('link'); fontLink.href = 'https://fonts.googleapis.com/css2?family=Poppins&display=swap'; fontLink.rel = 'stylesheet'; document.head.appendChild(fontLink); const button = document.createElement('button'); button.id = 'reader-toggle'; button.innerHTML = ` <span class="icon"><ion-icon name="book-outline"></ion-icon></span> <span class="title">进入阅读模式</span> `; document.body.appendChild(button); GM_addStyle(` #reader-toggle { position: fixed; top: 20%; right: 0px; transform: translateY(-50%); z-index: 99999; list-style: none; width: 60px; height: 60px; background: white; border-radius: 60px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); cursor: pointer; display: flex; justify-content: center; align-items: center; transition: 0.5s; border: none; font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", "Source Han Sans SC", sans-serif; } #reader-toggle::before { content: ""; position: absolute; inset: 0; border-radius: 40px; background: linear-gradient(45deg, #56CCF2, #2F80ED); opacity: 0; transition: 0.5s; } #reader-toggle::after { content: ""; position: absolute; top: 10px; width: 100%; height: 100%; border-radius: 40px; background: linear-gradient(45deg, #56CCF2, #2F80ED); filter: blur(15px); z-index: -1; opacity: 0; transition: 0.5s; } #reader-toggle:hover { width: 180px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0); } #reader-toggle:hover::before { opacity: 1; } #reader-toggle:hover::after { opacity: 0.5; } #reader-toggle .icon { position: absolute; left: 0; top: 0; width: 60px; height: 60px; color: #777; font-size: 2em; transition: 0.5s; transition-delay: 0.25s; display: flex; align-items: center; justify-content: center; } #reader-toggle .icon ion-icon { width: 24px; height: 24px; } #reader-toggle:hover .icon { transform: scale(0); color: #fff; transition-delay: 0s; } #reader-toggle .title { position: absolute; color: #fff; font-size: 1.4em; text-transform: uppercase; letter-spacing: 0.1em; transform: scale(0); transition: 0.5s; transition-delay: 0s; } #reader-toggle:hover .title { transform: scale(1); transition-delay: 0.25s; } #directory-sidebar { position: fixed; top: 0; right: -400px; width: 400px; height: 100%; background: linear-gradient(145deg, #1e2837 0%, #2a3441 25%, #1f2937 50%, #374151 75%, #1e293b 100%); z-index: 2147483647; transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: -15px 0 40px rgba(0, 0, 0, 0.4); display: flex; flex-direction: column; color: white; backdrop-filter: blur(20px); border: none; } #directory-sidebar.open { right: 0; } /* 遮罩层 */ #directory-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.6); z-index: 2147483646; opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0.3s ease; backdrop-filter: blur(2px); } #directory-overlay.show { opacity: 1; visibility: visible; } #directory-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 20px; background: linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(99, 102, 241, 0.1) 100%); backdrop-filter: blur(15px); border: none; position: relative; } #directory-header::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent 0%, rgba(59, 130, 246, 0.4) 50%, transparent 100%); } #directory-title { font-size: 16px; font-weight: 600; color: white; letter-spacing: 0.5px; } #directory-close { background: rgba(255, 255, 255, 0.1); border: none; font-size: 18px; cursor: pointer; color: white; padding: 8px; border-radius: 8px; transition: all 0.2s ease; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(10px); } #directory-close:hover { background: rgba(255, 255, 255, 0.2); transform: scale(1.05); } #directory-close:active { transform: scale(0.95); } #directory-content { flex: 1; overflow-y: auto; padding: 16px 0; scrollbar-width: thin; scrollbar-color: rgba(255, 255, 255, 0.3) transparent; } #directory-content::-webkit-scrollbar { width: 6px; } #directory-content::-webkit-scrollbar-track { background: transparent; } #directory-content::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.3); border-radius: 3px; } #directory-content::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.5); } #directory-list { list-style: none; padding: 0 16px; margin: 0; } .directory-item { display: block; padding: 14px 16px; margin: 0 0 8px 0; background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.04) 100%); border: none; border-radius: 12px; text-decoration: none !important; color: rgba(255, 255, 255, 0.9); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer; font-size: 14px; font-weight: 500; backdrop-filter: blur(10px); position: relative; overflow: hidden; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .directory-item::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.1), transparent); transform: translateX(-100%); transition: transform 0.6s; } .directory-item:hover::before { transform: translateX(100%); } .directory-item:hover { background: rgba(255, 255, 255, 0.15); transform: translateX(6px); color: white; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } .directory-item.current { background: linear-gradient(135deg, #FF6B6B, #FF8E53); color: white; box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3); font-weight: 600; } .directory-item.current:hover { transform: translateX(6px) scale(1.02); box-shadow: 0 6px 20px rgba(255, 107, 107, 0.4); } .directory-item.mainpost { background: linear-gradient(135deg, #4CAF50, #45a049); color: white; } .directory-item.mainpost:hover { background: linear-gradient(135deg, #45a049, #3d8b40); } #directory-loading { text-align: center; padding: 60px 20px; color: rgba(255, 255, 255, 0.7); font-size: 16px; font-weight: 500; } #directory-loading::before { content: '📚'; display: block; font-size: 32px; margin-bottom: 16px; animation: pulse 2s infinite; } @keyframes pulse { 0%, 100% { opacity: 0.7; } 50% { opacity: 1; } } #directory-empty { text-align: center; padding: 60px 20px; color: rgba(255, 255, 255, 0.6); font-size: 16px; } #directory-empty::before { content: '📖'; display: block; font-size: 32px; margin-bottom: 16px; opacity: 0.6; } /* 目录侧边栏的tooltip样式 */ #directory-sidebar [data-tooltip]:hover::after { content: attr(data-tooltip); position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.6); color: white; padding: 8px 12px; border-radius: 6px; font-size: 12px; white-space: nowrap; z-index: 2147483650; pointer-events: none; margin-bottom: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); opacity: 0; animation: tooltipFadeIn 0.2s ease forwards; } #directory-sidebar [data-tooltip]:hover::before { content: ''; position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); border: 6px solid transparent; border-top-color: rgba(0, 0, 0, 0.6); z-index: 2147483650; pointer-events: none; margin-bottom: 2px; opacity: 0; animation: tooltipFadeIn 0.2s ease forwards; } `); // 添加快捷键:Ctrl+Shift+R 切换阅读模式 document.addEventListener('keydown', e => { if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'r') { e.preventDefault(); if (!isReading) enterReader(); else exitReader(); } }); button.addEventListener('click', () => { if (!isReading) { enterReader(); setAutoReaderStatus(true); // 开启全局阅读器模式 } else { exitReader(); } }); /*** 目录功能 ***/ async function showDirectoryModal() { // 创建遮罩层 const overlay = document.createElement('div'); overlay.id = 'directory-overlay'; // 创建侧边栏 const sidebar = document.createElement('div'); sidebar.id = 'directory-sidebar'; sidebar.innerHTML = ` <div id="directory-header"> <div id="directory-title">目录</div> <button id="directory-close">×</button> </div> <div id="directory-content"> <div id="directory-loading">正在搜索相关章节...</div> <ul id="directory-list" style="display: none;"></ul> <div id="directory-empty" style="display: none;">未找到相关章节</div> </div> `; document.body.appendChild(overlay); document.body.appendChild(sidebar); // 显示遮罩和侧边栏 setTimeout(() => { overlay.classList.add('show'); sidebar.classList.add('open'); }, 10); // 关闭按钮事件 const closeBtn = sidebar.querySelector('#directory-close'); closeBtn.addEventListener('click', closeSidebar); // 点击遮罩关闭 overlay.addEventListener('click', closeSidebar); function closeSidebar() { overlay.classList.remove('show'); sidebar.classList.remove('open'); setTimeout(() => { overlay.remove(); sidebar.remove(); }, 300); } // 开始搜索目录 loadSeriesDirectory(sidebar); } async function loadSeriesDirectory(modal) { console.log('📖 开始加载系列目录...'); // 如果已有预加载的目录,直接使用 if (seriesDirectory.length > 0) { console.log('✅ 使用预加载的目录:', seriesDirectory.length, '个章节'); displayDirectory(modal, seriesDirectory); return; } // 如果预加载目录为空,先检查持久化缓存 if (originalSeriesTitle || savedThreadTitle) { const titleToCheck = originalSeriesTitle || savedThreadTitle; const seriesKey = generateSeriesKey(titleToCheck); const cachedDirectory = getCachedDirectory(seriesKey); if (cachedDirectory && cachedDirectory.length > 0) { console.log('✅ 从持久化缓存加载目录:', cachedDirectory.length, '个章节'); seriesDirectory = cachedDirectory; displayDirectory(modal, seriesDirectory); return; } } // 尝试多种方式获取页面标题 let titleElement = null; let threadTitle = ''; console.log('🔍 尝试查找页面标题...'); // 简化标题提取逻辑 const h1Element = document.querySelector('h1.ts'); if (h1Element) { const subjectSpan = h1Element.querySelector('#thread_subject, span[id^="thread_subject"]'); if (subjectSpan) { threadTitle = subjectSpan.textContent.trim(); console.log(`✅ 找到标题: "${threadTitle}"`); } else { threadTitle = h1Element.textContent.trim(); console.log(`✅ 找到h1文本: "${threadTitle}"`); } } else { // 兜底:使用原有逻辑 const titleSelectors = [ '#thread_subject', 'span[id^="thread_subject"]', '.ts a', 'h1.ts', '.ntn a', '.ntn', '.bm .mbm h1', '.ts' ]; for (const selector of titleSelectors) { titleElement = document.querySelector(selector); if (titleElement) { threadTitle = titleElement.textContent.trim(); console.log(`✅ 找到标题元素: "${selector}" -> "${threadTitle}"`); break; } else { console.log(`❌ 未找到: "${selector}"`); } } } // 在阅读模式下,优先使用原始系列标题进行目录搜索(避免使用错误的标题) if (isReading) { if (originalSeriesTitle) { threadTitle = originalSeriesTitle; console.log('🎯 阅读模式下使用原始系列标题:', threadTitle); } else if (savedThreadTitle) { threadTitle = savedThreadTitle; console.log('📦 阅读模式下使用预存标题:', threadTitle); } } else if (!threadTitle && originalSeriesTitle) { threadTitle = originalSeriesTitle; console.log('🎯 使用原始系列标题:', threadTitle); } else if (!threadTitle && savedThreadTitle) { threadTitle = savedThreadTitle; console.log('📦 使用预存标题:', threadTitle); } // 如果还是没找到,尝试从页面title获取 if (!threadTitle) { const pageTitle = document.title; console.log('🔍 尝试从页面title获取:', pageTitle); // 移除论坛名称等后缀,只保留漫画标题部分 if (pageTitle.includes(' - ')) { threadTitle = pageTitle.split(' - ')[0].trim(); console.log('📝 从页面title提取:', threadTitle); } else { threadTitle = pageTitle.replace(/\s*-\s*百合会.*$/, '').trim(); console.log('📝 清理后的标题:', threadTitle); } } const currentThreadId = getCurrentThreadId(); console.log('📋 当前页面信息:'); console.log(' 标题元素:', titleElement ? '✅ 找到' : '❌ 未找到'); console.log(' 页面标题:', threadTitle); console.log(' 线程ID:', currentThreadId); // 调试:打印页面中所有可能的标题元素 if (!titleElement) { console.log('🔍 调试:查找所有可能的标题元素...'); const allH1 = document.querySelectorAll('h1'); const allSpans = document.querySelectorAll('span[id*="thread"], span[id*="subject"]'); const allTs = document.querySelectorAll('.ts, .ntn'); console.log(' - h1元素数量:', allH1.length); allH1.forEach((h1, index) => { if (index < 5) { console.log(` h1[${index}]: "${h1.textContent.trim().substring(0, 50)}" (class: ${h1.className})`); } }); console.log(' - 包含thread/subject的span数量:', allSpans.length); allSpans.forEach((span, index) => { if (index < 5) { console.log(` span[${index}]: id="${span.id}", text="${span.textContent.trim().substring(0, 50)}"`); } }); console.log(' - .ts/.ntn元素数量:', allTs.length); allTs.forEach((ts, index) => { if (index < 5) { console.log(` ts[${index}]: class="${ts.className}", text="${ts.textContent.trim().substring(0, 50)}"`); } }); } if (!threadTitle) { console.log('❌ 页面标题为空,显示空状态'); showDirectoryEmpty(modal); return; } try { console.log('🔍 开始搜索目录...'); const directory = await searchSeriesDirectory(threadTitle); console.log('📊 搜索结果:', directory.length, '个章节'); if (directory.length === 0) { console.log('❌ 未找到章节,显示空状态'); showDirectoryEmpty(modal); return; } console.log('✅ 找到章节,开始显示目录界面'); displayDirectory(modal, directory, threadTitle); } catch (error) { console.error('❌ 加载目录失败:', error); console.error('错误详情:', error.message); console.error('错误堆栈:', error.stack); showDirectoryEmpty(modal, '加载失败,请稍后重试'); } } function displayDirectory(modal, directory, threadTitle = '') { // 显示目录 const loading = modal.querySelector('#directory-loading'); const list = modal.querySelector('#directory-list'); const title = modal.querySelector('#directory-title'); loading.style.display = 'none'; list.style.display = 'block'; // 获取页面标题(如果没有提供) if (!threadTitle) { if (savedThreadTitle) { threadTitle = savedThreadTitle; } else { threadTitle = document.title.split(' - ')[0] || '漫画目录'; } } // 简化标题,只显示"目录" title.textContent = '目录'; console.log('📝 目录标题: 目录'); list.innerHTML = ''; const currentThreadId = getCurrentThreadId(); directory.forEach((item, index) => { const li = document.createElement('li'); const link = document.createElement('a'); link.className = 'directory-item'; if (item.source === 'mainpost') { link.classList.add('mainpost'); } link.href = item.url; // 为主楼提取的目录优化显示格式 if (item.source === 'mainpost') { let displayText = item.title; // 识别不同的章节格式并优化显示 if (item.title.match(/^\d{1,2}$/)) { displayText = `第${String(item.title).padStart(2, '0')}话`; } else if (item.title.match(/^\d+\.\d+$/)) { displayText = `第${item.title}话`; } else if (item.title.match(/^\d+[话話章节節回卷篇]/i)) { displayText = item.title; } else if (item.title.match(/卷|番外|彩页|特典/i)) { displayText = item.title; } else if (item.title.match(/^0\d+$/)) { displayText = `第${item.title}话`; } link.textContent = displayText; } else { link.textContent = item.title; } // 添加点击事件处理,无缝加载新章节内容 link.addEventListener('click', async (e) => { e.preventDefault(); console.log('🔄 正在加载章节:', item.title); // 关闭侧边栏 const overlay = document.getElementById('directory-overlay'); const sidebar = document.getElementById('directory-sidebar'); if (overlay && sidebar) { overlay.classList.remove('show'); sidebar.classList.remove('open'); setTimeout(() => { overlay.remove(); sidebar.remove(); }, 300); } // 无缝加载新章节内容 await loadNewChapter(item.url, item.title); }); // 高亮当前章节 if (item.threadId === currentThreadId) { link.classList.add('current'); console.log(`⭐ 当前章节: ${item.title}`); } li.appendChild(link); list.appendChild(li); console.log(`📄 章节 ${index + 1}: ${item.title}`); }); console.log('✅ 目录界面显示完成'); } function showDirectoryEmpty(modal, message = '未找到相关章节') { const loading = modal.querySelector('#directory-loading'); const empty = modal.querySelector('#directory-empty'); loading.style.display = 'none'; empty.style.display = 'block'; empty.textContent = message; } function getCurrentThreadId() { const href = window.location.href; let match = href.match(/thread-(\d+)-/); if (match) { return match[1]; } try { const url = new URL(href); const tidParam = url.searchParams.get('tid'); if (tidParam) { return tidParam; } } catch (e) { // ignore } match = href.match(/[?&]tid=(\d+)/); return match ? match[1] : null; } // 清理所有缓存,并清空内存缓存 function clearAllCache() { console.log('🧹 清理所有目录缓存 (session + GM) ...'); let cleanedCount = 0; // 1) 清理 sessionStorage 前缀条目 try { const sKeys = Object.keys(sessionStorage).filter(k => k.startsWith(CACHE_PREFIX)); sKeys.forEach(k => { try { sessionStorage.removeItem(k); cleanedCount++; } catch (e) {} }); } catch (e) { } // 2) 清理 GM_* 条目(通过索引枚举) try { if (typeof GM_getValue === 'function' && typeof GM_setValue === 'function') { let idx = GM_getValue(GM_INDEX_KEY, undefined); if (typeof idx === 'string') { try { idx = JSON.parse(idx); } catch (e) { idx = []; } } if (!Array.isArray(idx)) idx = []; for (const key of idx) { try { storageRemove(key); cleanedCount++; } catch (e) {} } try { GM_setValue(GM_INDEX_KEY, []); } catch (e) {} } } catch (e) { } // 3) 清理内存缓存 try { if (searchCache && searchCache.clear) searchCache.clear(); } catch (e) {} try { if (directoryMemoryCache && directoryMemoryCache.clear) directoryMemoryCache.clear(); } catch (e) {} console.log(`✅ 已清理 ${cleanedCount} 个持久化缓存条目,且已清空内存缓存`); return cleanedCount; } // 查看缓存状态(session + GM 索引 + 内存统计) function viewCacheStatus() { console.log('📊 缓存状态统计:'); // sessionStorage 条目 let sessionList = []; try { sessionList = Object.keys(sessionStorage).filter(k => k.startsWith(CACHE_PREFIX)); } catch (e) { sessionList = []; } // GM 索引条目 let gmList = []; try { if (typeof GM_getValue === 'function') { let idx = GM_getValue(GM_INDEX_KEY, undefined); if (typeof idx === 'string') { try { idx = JSON.parse(idx); } catch (e) { idx = []; } } if (Array.isArray(idx)) gmList = idx.slice(); } } catch (e) { gmList = []; } console.log(`📦 sessionStorage 缓存数量: ${sessionList.length}`); console.log(`📦 GM_* 缓存数量 (index): ${gmList.length}`); console.log(`🧠 内存缓存 (searchCache): ${typeof searchCache !== 'undefined' && searchCache.size !== undefined ? searchCache.size : '未知'}`); console.log(`🧠 内存目录缓存 (directoryMemoryCache): ${typeof directoryMemoryCache !== 'undefined' && directoryMemoryCache.size !== undefined ? directoryMemoryCache.size : '未知'}`); if (sessionList.length > 0) { console.log('\n📋 sessionStorage 详情:'); sessionList.forEach((k, i) => { try { const obj = JSON.parse(sessionStorage.getItem(k)); const age = Math.round((Date.now() - (obj.ts || obj.timestamp || 0)) / (1000*60*60)); const count = (obj.d || obj.data) ? (obj.d || obj.data).length : '未知'; console.log(` ${i+1}. ${k.replace(CACHE_PREFIX+'session-','')}: ${count}章, ${age}小时前`); } catch (e) { console.log(` ${i+1}. ${k} (无法解析)`); } }); } if (gmList.length > 0) { console.log('\n📋 GM_* 详情 (由索引列出):'); gmList.forEach((k, i) => { try { const obj = storageGet(k, null); const age = obj ? Math.round((Date.now() - (obj.ts || obj.timestamp || 0)) / (1000*60*60)) : '未知'; const count = obj ? ((obj.d || obj.data) ? (obj.d || obj.data).length : '未知') : '未知'; console.log(` ${i+1}. ${k.replace(CACHE_PREFIX,'')}: ${count}章, ${age}小时前`); } catch (e) { console.log(` ${i+1}. ${k} (无法读取)`); } }); } return { sessionCount: sessionList.length, gmCount: gmList.length, memorySearchCount: typeof searchCache !== 'undefined' && searchCache.size !== undefined ? searchCache.size : null, memoryDirCount: typeof directoryMemoryCache !== 'undefined' && directoryMemoryCache.size !== undefined ? directoryMemoryCache.size : null }; } // 必须识别作者 UID,按楼层顺序收集该作者的图片;识别失败直接返回空数组,避免全页搜索 function collectImagesFromDocument(doc = document) { try { const normalizeUrl = u => { if (!u) return null; u = String(u).trim(); if (!u) return null; if (u.startsWith('//')) u = (location.protocol || 'https:') + u; if (!/^https?:\/\//i.test(u)) u = 'https://bbs.yamibo.com/' + u.replace(/^\/+/, ''); u = u.replace(/^https?:\/\/https?:\/\//, 'https://'); return u; }; const isPlaceholder = (img, src) => { if (!src) return true; if (src.includes('static/image/common/none.gif')) { if (img.getAttribute('file') || img.getAttribute('zoomfile') || img.getAttribute('aid')) return false; return true; } return false; }; // 识别并忽略头像/评分用户小图等噪音图片 const isIgnoredImage = (img, src) => { try { if (!src && !img) return true; const s = String(src || '').toLowerCase(); // 明确的头像路径(示例:/uc_server/data/avatar/...) if (/\/uc_server\/data\/avatar\//i.test(s)) return true; // 常见 avatar 文件名 / avatar_small if (/avatar(?:_small|_middle)?\.(jpg|jpeg|png|gif|webp)$/i.test(s)) return true; if (s.includes('/avatar/') || s.includes('/avatars/')) return true; // class 名称包含 avatar 或 user_avatar 等 const cls = (img && img.className) ? String(img.className).toLowerCase() : ''; if (cls.includes('user_avatar') || cls.includes('avatar') || cls.includes('userpic') || cls.includes('authoravatar')) return true; // 如果图片位于明显的评分/头像容器,忽略 if (img && (img.closest('.postrate') || img.closest('.post-ratings') || img.closest('.postratedby') || img.closest('.poster') || img.closest('.author') || img.closest('.avatar') || img.closest('.user')) ) { return true; } // 若图片尺寸非常小(通常为头像),忽略阈值可调整 const wAttr = img && (img.getAttribute('width') || img.width); const hAttr = img && (img.getAttribute('height') || img.height); const w = wAttr ? parseInt(wAttr, 10) : 0; const h = hAttr ? parseInt(hAttr, 10) : 0; if ((w > 0 && w < 80) || (h > 0 && h < 80)) return true; // 若 src 含有明显的评分/头像关键词 if (s.includes('rating') || s.includes('score') || s.includes('grade') || s.includes('usericon')) return true; return false; } catch (e) { return false; } }; const preferredSelectors = [ 'ignore_js_op img', '.savephotop img', 'div.t_f img', 'td.t_f img', '.pcb img', 'img[zoomfile]', 'img[file]', 'img[aid]', 'img.zoom', 'img' ]; // 定位主楼容器,尝试从中识别作者 UID let mainPost = null; try { const posts = doc.querySelectorAll('[id^="post_"], .post'); if (posts && posts.length) { for (const p of posts) { if (p.id && /^post_\d+/.test(p.id)) { mainPost = p; break; } } if (!mainPost) mainPost = posts[0]; } } catch (e) { mainPost = null; } // 必须识别作者 UID,否则直接返回空数组(避免全页抓取) let authorUid = null; if (mainPost) { try { const a = mainPost.querySelector('a[href*="space-uid-"], a[href*="uid="], a[href*="space.php?uid="]'); if (a && a.getAttribute('href')) { const href = a.getAttribute('href'); const m1 = href.match(/space-uid-(\d+)\.html/); const m2 = href.match(/uid=(\d+)/); if (m1) authorUid = m1[1]; else if (m2) authorUid = m2[1]; } } catch (e) { authorUid = null; } } if (!authorUid) { console.log('⚠️ collectImagesFromDocument: 无法识别作者 UID,跳过图片收集'); return []; } const seen = new Set(); const results = []; // 遍历所有 post_* 楼层,按文档顺序只收集该作者的图片 const postsAll = Array.from(doc.querySelectorAll('[id^="post_"]')); for (const p of postsAll) { try { const authorLink = p.querySelector(`a[href*="space-uid-${authorUid}"], a[href*="uid=${authorUid}"], a[href*="space.php?uid=${authorUid}"]`); if (!authorLink) continue; // 非楼主发言,跳过 for (const sel of preferredSelectors) { const imgs = p.querySelectorAll(sel); for (const img of imgs) { const u = normalizeUrl( img.getAttribute('zoomfile') || img.getAttribute('file') || img.getAttribute('src') || img.getAttribute('data-src') ); if (!u) continue; if (isIgnoredImage(img, u)) continue; if (!seen.has(u)) { seen.add(u); results.push(u); } } } } catch (e) { continue; } } return results; } catch (e) { console.warn('collectImagesFromDocument error', e); return []; } } // 初始化时检查全局阅读器状态 function initAutoReader() { autoReaderEnabled = checkAutoReaderStatus(); console.log('🚀 初始化全局阅读器状态:', autoReaderEnabled); // 清理过期缓存 cleanExpiredCache(); // 检查是否有漫画内容,优先检测主楼 const comicImages = collectImagesFromDocument(document); if (comicImages && comicImages.length > 0) { console.log('🎯 检测到漫画内容,立即开始预取目录...'); // 立即开始预取目录信息,不管是否进入阅读模式 preloadDirectoryInfo(); // 如果全局阅读器开启,自动进入阅读模式 if (autoReaderEnabled && !isReading) { console.log('🎯 自动进入阅读模式'); setTimeout(() => { if (button && button.parentNode) { button.remove(); } enterReader(); }, 1000); } } else { console.log('🔍 尝试预取目录信息...'); preloadDirectoryInfo(); } } // 预加载目录信息 async function preloadDirectoryInfo() { if (seriesDirectory.length > 0) { console.log('� 目录已预加载,跳过重复加载'); return; } try { console.log('�📚 开始预加载目录信息...'); // 获取当前页面标题 let threadTitle = ''; const h1Element = document.querySelector('h1.ts'); if (h1Element) { const subjectSpan = h1Element.querySelector('#thread_subject, span[id^="thread_subject"]'); if (subjectSpan) { threadTitle = subjectSpan.textContent.trim(); } else { threadTitle = h1Element.textContent.trim(); } } else { const titleElement = document.querySelector('#thread_subject, span[id^="thread_subject"]'); if (titleElement) { threadTitle = titleElement.textContent.trim(); } else { const pageTitle = document.title; threadTitle = pageTitle.includes(' - ') ? pageTitle.split(' - ')[0].trim() : pageTitle.replace(/\s*-\s*百合会.*$/, '').trim(); } } if (threadTitle) { // 保存标题信息 savedThreadTitle = threadTitle; originalSeriesTitle = threadTitle; console.log('📖 开始预取目录,使用标题:', threadTitle); // 先检查持久化缓存 const seriesKey = generateSeriesKey(threadTitle); const cachedDirectory = getCachedDirectory(seriesKey); if (cachedDirectory && cachedDirectory.length > 0) { console.log('✅ 从持久化缓存预加载完成:', cachedDirectory.length, '个章节'); seriesDirectory = cachedDirectory; return; } // 如果没有缓存,立即开始异步搜索目录 searchSeriesDirectory(threadTitle).then(directory => { if (directory && directory.length > 0) { seriesDirectory = directory; console.log('✅ 预加载完成,获得', directory.length, '个章节'); } else { console.log('⚠️ 预加载未找到目录'); } }).catch(error => { console.warn('⚠️ 预加载目录失败:', error.message); }); console.log('🚀 预加载任务已启动,后台进行中...'); } else { console.log('⚠️ 无法获取页面标题,跳过预加载'); } } catch (error) { console.warn('⚠️ 预加载目录失败:', error.message); } } // 页面加载完成后初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(initAutoReader, 1000); // 确保页面元素完全加载 }); } else { setTimeout(initAutoReader, 1000); // 给页面充足时间渲染 } /*** 进入阅读模式 ***/ function enterReader() { isReading = true; // 保存原始页面HTML和标题 originalPageHTML = document.body.innerHTML; originalPageTitle = document.title; console.log('💾 已保存原始页面内容'); button.remove(); // 在替换页面内容之前,先提取并保存标题 console.log('🔍 进入阅读模式,保存页面标题...'); // 简化标题提取逻辑 const h1Element = document.querySelector('h1.ts'); if (h1Element) { const subjectSpan = h1Element.querySelector('#thread_subject, span[id^="thread_subject"]'); if (subjectSpan) { savedThreadTitle = subjectSpan.textContent.trim(); console.log('💾 保存标题:', savedThreadTitle); } else { savedThreadTitle = h1Element.textContent.trim(); console.log('💾 保存h1文本:', savedThreadTitle); } } else { // 兜底:尝试其他方式 const threadTitleElement = document.querySelector('#thread_subject, span[id^="thread_subject"]'); if (threadTitleElement) { savedThreadTitle = threadTitleElement.textContent.trim(); console.log('💾 保存标题元素:', savedThreadTitle); } else { // 最后兜底用<title> const pageTitle = document.title; savedThreadTitle = pageTitle.includes(' - ') ? pageTitle.split(' - ')[0].trim() : pageTitle.replace(/\s*-\s*百合会.*$/, '').trim(); console.log('💾 从页面title保存:', savedThreadTitle); } } // 保存原始系列标题,用于后续目录搜索 originalSeriesTitle = savedThreadTitle; console.log('🎯 保存原始系列标题:', originalSeriesTitle); // 检查是否已有预加载的目录数据 if (seriesDirectory.length > 0) { console.log('✅ 使用预加载的目录数据:', seriesDirectory.length, '个章节'); } else { // 检查持久化缓存 const seriesKey = generateSeriesKey(savedThreadTitle); const cachedDirectory = getCachedDirectory(seriesKey); if (cachedDirectory && cachedDirectory.length > 0) { console.log('📦 从持久化缓存加载目录:', cachedDirectory.length, '个章节'); seriesDirectory = cachedDirectory; } else { // 缓存也没有,异步搜索 console.log('🔍 开始异步搜索目录...'); searchSeriesDirectory(savedThreadTitle).then(directory => { seriesDirectory = directory; console.log('📋 异步获取到目录:', seriesDirectory.length, '个章节'); }).catch(error => { console.error('❌ 异步获取目录失败:', error); }); } } // 提取漫画图片 images = collectImagesFromDocument(document) .map(u => { if (!u) return null; let url = String(u); if (url.startsWith('//')) url = 'https:' + url; if (!/^https?:\/\//i.test(url)) url = 'https://bbs.yamibo.com/' + url.replace(/^\/+/, ''); if (url.startsWith('http://')) url = url.replace(/^http:\/\//, 'https://'); url = url.replace(/^https?:\/\/https?:\/\//, 'https://'); return url; }) .filter(Boolean); console.log('📸 提取到', images.length, '张图片,URL已处理为HTTPS'); document.body.innerHTML = ` <button id="cw-exit" data-tooltip="退出阅读模式">⏻</button> <div id="cw-toolbar"> <button id="cw-directory" data-tooltip="查看目录 (M)">☰</button> <button id="cw-full" data-tooltip="全屏模式 (F)">⛶</button> <button id="cw-bg" data-tooltip="切换背景色 (B)">◐</button> <div id="cw-zoom"> <button id="cw-zoom-in" data-tooltip="放大图片 (+)">﹢</button> <button id="cw-zoom-out" data-tooltip="缩小图片 (-)">﹣</button> </div> </div> <div id="cw-container"></div> <div id="cw-bottom-bar"> <span id="cw-page-info">1/${images.length}</span> <button id="cw-to-top" data-tooltip="返回顶部 (T)">⬆</button> </div> `; GM_addStyle(` html, body { margin: 0; padding: 0; background: #616161; overflow-y: auto; color: #fff; font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", "Source Han Sans SC", sans-serif !important; } /* 容器样式:负责整体布局 */ #cw-container { display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: flex-start !important; padding: 70px 0 60px !important; gap: 20px !important; margin: 0 auto !important; } /* 图片基础样式 */ #cw-container img { display: block !important; width: var(--img-width, 35vw) !important; /* 使用CSS变量控制宽度 */ height: auto !important; border-radius: 0 !important; user-select: none !important; transition: all 0.3s ease !important; margin: 0 auto !important; } /* 小图保持原始大小 */ #cw-container img[data-original-size="small"] { width: auto !important; max-width: var(--img-width, 35vw) !important; } #cw-exit { position: fixed; top: 15px; left: 20px; z-index: 2147483647; background: rgba(0,0,0,0.6); color: #fff; border: none; padding: 6px 10px; border-radius: 5px; cursor: pointer; font-size: 14px; } #cw-toolbar { position: fixed; top: 15px; right: 20px; display: flex; flex-direction: column; gap: 10px; z-index: 2147483647; pointer-events: auto; } #cw-toolbar button { background: rgba(0,0,0,0.6); color: #fff; border: none; padding: 6px; width: 32px; height: 32px; border-radius: 5px; cursor: pointer; font-size: 14px; transition: background-color 0.2s; display: flex; align-items: center; justify-content: center; } #cw-toolbar button:hover { background: rgba(0,0,0,0.8); transform: scale(1.05); } #cw-toolbar button:active { transform: scale(0.95); } /* 放大缩小按钮的特殊样式 */ #cw-zoom { display: flex; flex-direction: column; gap: 5px; } #cw-zoom-in, #cw-zoom-out { color: white !important; font-size: 20px !important; font-weight: bold !important; } /* ESC按钮的特殊样式 */ #cw-full[data-fullscreen="true"] { font-size: 24px !important; line-height: 16px !important; padding: 6px 10px !important; } /* 底部工具栏 */ #cw-bottom-bar { position: fixed; bottom: 15px; right: 20px; display: flex; align-items: center; gap: 15px; z-index: 2147483647; pointer-events: auto; background: rgba(0,0,0,0.6); padding: 8px 12px; border-radius: 5px; font-size: 14px; } #cw-bottom-bar button { background: rgba(255,255,255,0.2); color: #fff; border: none; padding: 4px 8px; border-radius: 3px; cursor: pointer; font-size: 12px; } #cw-bottom-bar button:hover { background: rgba(255,255,255,0.3); } #cw-page-info { color: #fff; font-weight: bold; } /* 全屏状态下的基础样式 */ :fullscreen #cw-toolbar, :-webkit-full-screen #cw-toolbar, :fullscreen #cw-bottom-bar, :-webkit-full-screen #cw-bottom-bar, :fullscreen #cw-exit, :-webkit-full-screen #cw-exit { display: flex !important; position: fixed !important; z-index: 2147483647 !important; transition: all 0.3s ease !important; opacity: 0 !important; pointer-events: none !important; } /* 鼠标移动时显示工具栏 */ :fullscreen.tools-visible #cw-toolbar, :-webkit-full-screen.tools-visible #cw-toolbar, :fullscreen.tools-visible #cw-bottom-bar, :-webkit-full-screen.tools-visible #cw-bottom-bar, :fullscreen.tools-visible #cw-exit, :-webkit-full-screen.tools-visible #cw-exit { opacity: 1 !important; pointer-events: auto !important; } :fullscreen #cw-toolbar, :-webkit-full-screen #cw-toolbar { top: 15px !important; right: 20px !important; } :fullscreen #cw-bottom-bar, :-webkit-full-screen #cw-bottom-bar { bottom: 15px !important; right: 20px !important; } /* 确保全屏状态下按钮样式保持一致 */ :fullscreen #cw-toolbar button:not(#cw-zoom-in):not(#cw-zoom-out), :-webkit-full-screen #cw-toolbar button:not(#cw-zoom-in):not(#cw-zoom-out) { padding: 6px !important; width: 32px !important; height: 32px !important; font-size: 14px !important; line-height: 16px !important; display: flex !important; align-items: center !important; justify-content: center !important; } /* 全屏状态下的缩放按钮样式 */ :fullscreen #cw-zoom-in, :fullscreen #cw-zoom-out, :-webkit-full-screen #cw-zoom-in, :-webkit-full-screen #cw-zoom-out { padding: 6px !important; width: 32px !important; height: 32px !important; font-size: 18px !important; line-height: 16px !important; color: #ffffff !important; } /* ESC 符号单独设置大小 */ :fullscreen #cw-full[data-fullscreen="true"], :-webkit-full-screen #cw-full[data-fullscreen="true"] { font-size: 24px !important; } /* 自定义tooltip样式,禁用原生tooltip */ [data-tooltip] { position: relative; } [data-tooltip]:hover::after { content: attr(data-tooltip); position: absolute; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.9); color: white; padding: 8px 12px; border-radius: 6px; font-size: 12px; white-space: nowrap; z-index: 2147483648; pointer-events: none; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); opacity: 0; animation: tooltipFadeIn 0.2s ease forwards; } [data-tooltip]:hover::before { content: ''; position: absolute; left: 50%; transform: translateX(-50%); border: 6px solid transparent; border-top-color: rgba(0, 0, 0, 0.9); z-index: 2147483648; pointer-events: none; opacity: 0; animation: tooltipFadeIn 0.2s ease forwards; } /* 顶部工具栏按钮 - tooltip显示在下方 */ #cw-toolbar [data-tooltip]:hover::after, #cw-exit[data-tooltip]:hover::after { top: 100%; margin-top: 8px; } #cw-toolbar [data-tooltip]:hover::before, #cw-exit[data-tooltip]:hover::before { top: 100%; border-bottom-color: rgba(0, 0, 0, 0.9); margin-top: 2px; } /* 底部按钮 - tooltip显示在上方 */ #cw-bottom-bar [data-tooltip]:hover::after { bottom: 100%; margin-bottom: 8px; } #cw-bottom-bar [data-tooltip]:hover::before { bottom: 100%; border-top-color: rgba(0, 0, 0, 0.9); margin-bottom: 2px; } /* 右侧按钮tooltip防止超出屏幕 */ #cw-toolbar [data-tooltip]:hover::after { right: 0; left: auto; transform: translateX(-20px); } #cw-toolbar [data-tooltip]:hover::before { right: 20px; left: auto; transform: none; } /* 左侧退出按钮tooltip */ #cw-exit[data-tooltip]:hover::after { left: 0; transform: translateX(20px); } #cw-exit[data-tooltip]:hover::before { left: 20px; transform: none; } /* 确保tooltip在全屏状态下也能正常显示 */ :fullscreen [data-tooltip]:hover::after, :-webkit-full-screen [data-tooltip]:hover::after, :fullscreen [data-tooltip]:hover::before, :-webkit-full-screen [data-tooltip]:hover::before { z-index: 2147483649 !important; } `); // ====== 初始化阅读器UI(渲染图片并绑定按钮) ====== function initReaderUI() { const container = document.getElementById('cw-container'); if (!container) { console.warn('⚠️ cw-container 未找到,无法渲染图片'); return; } // 设置初始缩放(使用 CSS 变量控制宽度) const applyZoom = () => { const vw = Math.max(10, Math.min(90, zoomLevel)); document.documentElement.style.setProperty('--img-width', `${vw}vw`); }; applyZoom(); // 渲染图片列表 container.innerHTML = ''; images.forEach((url, idx) => { const img = document.createElement('img'); img.src = url; img.loading = 'lazy'; img.alt = `page-${idx + 1}`; img.dataset.index = String(idx + 1); container.appendChild(img); }); // 更新页码显示(靠近视口中心的图片) const pageInfo = document.getElementById('cw-page-info'); function updateCurrentPageInfo() { const imgs = container.querySelectorAll('img'); if (!imgs.length) return; const viewportMid = window.scrollY + window.innerHeight / 2; let current = 1; let minDist = Infinity; imgs.forEach((img, i) => { const rect = img.getBoundingClientRect(); const imgMid = window.scrollY + rect.top + rect.height / 2; const dist = Math.abs(imgMid - viewportMid); if (dist < minDist) { minDist = dist; current = i + 1; } }); if (pageInfo) pageInfo.textContent = `${current}/${imgs.length}`; } // 退出、目录、全屏、背景、缩放、回顶部 readerEventHandlers.onExitClick = () => exitReader(); readerEventHandlers.onDirectoryClick = () => showDirectoryModal(); readerEventHandlers.onFullClick = async () => { try { if (!document.fullscreenElement) await document.documentElement.requestFullscreen(); else await document.exitFullscreen(); } catch (e) { console.warn('全屏切换失败:', e); } }; readerEventHandlers.onBgClick = () => { bgIsBlack = !bgIsBlack; document.body.style.background = bgIsBlack ? '#616161' : '#f5f5f5'; document.body.style.color = bgIsBlack ? '#fff' : '#000'; }; readerEventHandlers.onZoomIn = () => { zoomLevel = Math.min(90, zoomLevel + 5); applyZoom(); }; readerEventHandlers.onZoomOut = () => { zoomLevel = Math.max(10, zoomLevel - 5); applyZoom(); }; readerEventHandlers.onToTop = () => window.scrollTo({ top: 0, behavior: 'smooth' }); const throttle = (fn, wait = 100) => { let t = null; return (...args) => { if (t) return; t = setTimeout(() => { fn(...args); t = null; }, wait); }; }; readerEventHandlers.updateCurrentPageInfo = throttle(updateCurrentPageInfo, 120); // 工具栏显示控制 readerEventHandlers.showTools = function() { try { document.documentElement.classList.add('tools-visible'); if (readerToolsTimer) clearTimeout(readerToolsTimer); readerToolsTimer = setTimeout(() => { document.documentElement.classList.remove('tools-visible'); readerToolsTimer = null; }, 3000); } catch (e) { // ignore } }; readerEventHandlers.onMousemove = readerEventHandlers.showTools; readerEventHandlers.onKeydown = readerEventHandlers.showTools; readerEventHandlers.onFullscreen = function() { if (document.fullscreenElement) { readerEventHandlers.showTools(); } else { document.documentElement.classList.remove('tools-visible'); if (readerToolsTimer) { clearTimeout(readerToolsTimer); readerToolsTimer = null; } } }; // 绑定 DOM 事件 const exitBtn = document.getElementById('cw-exit'); if (exitBtn) exitBtn.addEventListener('click', readerEventHandlers.onExitClick); const dirBtn = document.getElementById('cw-directory'); if (dirBtn) dirBtn.addEventListener('click', readerEventHandlers.onDirectoryClick); const fullBtn = document.getElementById('cw-full'); if (fullBtn) fullBtn.addEventListener('click', readerEventHandlers.onFullClick); const bgBtn = document.getElementById('cw-bg'); if (bgBtn) bgBtn.addEventListener('click', readerEventHandlers.onBgClick); const zoomIn = document.getElementById('cw-zoom-in'); const zoomOut = document.getElementById('cw-zoom-out'); if (zoomIn) zoomIn.addEventListener('click', readerEventHandlers.onZoomIn); if (zoomOut) zoomOut.addEventListener('click', readerEventHandlers.onZoomOut); const toTop = document.getElementById('cw-to-top'); if (toTop) toTop.addEventListener('click', readerEventHandlers.onToTop); // 全局事件绑定 if (!readerEventsBound) { window.addEventListener('scroll', readerEventHandlers.updateCurrentPageInfo, { passive: true }); window.addEventListener('mousemove', readerEventHandlers.onMousemove, { passive: true }); window.addEventListener('keydown', readerEventHandlers.onKeydown, { passive: true }); document.addEventListener('fullscreenchange', readerEventHandlers.onFullscreen); readerEventsBound = true; } // 立即更新一次页码并短暂显示工具栏 setTimeout(updateCurrentPageInfo, 200); readerEventHandlers.showTools(); // 提供解绑函数,供 exitReader 调用 unbindReaderEvents = function() { try { if (!readerEventsBound) return; // 移除 DOM 绑定 try { if (exitBtn) exitBtn.removeEventListener('click', readerEventHandlers.onExitClick); } catch (e) {} try { if (dirBtn) dirBtn.removeEventListener('click', readerEventHandlers.onDirectoryClick); } catch (e) {} try { if (fullBtn) fullBtn.removeEventListener('click', readerEventHandlers.onFullClick); } catch (e) {} try { if (bgBtn) bgBtn.removeEventListener('click', readerEventHandlers.onBgClick); } catch (e) {} try { if (zoomIn) zoomIn.removeEventListener('click', readerEventHandlers.onZoomIn); } catch (e) {} try { if (zoomOut) zoomOut.removeEventListener('click', readerEventHandlers.onZoomOut); } catch (e) {} try { if (toTop) toTop.removeEventListener('click', readerEventHandlers.onToTop); } catch (e) {} // 移除全局事件 try { window.removeEventListener('scroll', readerEventHandlers.updateCurrentPageInfo, { passive: true }); } catch (e) {} try { window.removeEventListener('mousemove', readerEventHandlers.onMousemove, { passive: true }); } catch (e) {} try { window.removeEventListener('keydown', readerEventHandlers.onKeydown, { passive: true }); } catch (e) {} try { document.removeEventListener('fullscreenchange', readerEventHandlers.onFullscreen); } catch (e) {} } catch (e) { // ignore } // 清理定时器与状态 if (readerToolsTimer) { clearTimeout(readerToolsTimer); readerToolsTimer = null; } readerEventHandlers = {}; readerEventsBound = false; unbindReaderEvents = null; }; } // 立即初始化阅读器 UI initReaderUI(); } /*** 退出阅读模式 ***/ function exitReader() { isReading = false; setAutoReaderStatus(false); // 关闭全局阅读器模式 // 解绑阅读器事件,避免残留监听 try { if (typeof unbindReaderEvents === 'function') unbindReaderEvents(); } catch (e) { // ignore } // 先退出全屏 if (document.fullscreenElement) { document.exitFullscreen().catch(err => { console.warn('退出全屏失败:', err); }); } // 如果有保存的原始页面内容,直接恢复 if (originalPageHTML) { console.log('🔄 恢复原始页面内容...'); // 移除阅读模式添加的样式 const readerStyles = document.querySelectorAll('style'); readerStyles.forEach(style => { // 移除包含阅读模式特定选择器的样式 if (style.textContent.includes('#cw-container') || style.textContent.includes('#cw-toolbar') || style.textContent.includes('#chapter-loader')) { style.remove(); console.log('🧹 移除阅读模式样式'); } }); document.body.innerHTML = originalPageHTML; document.title = originalPageTitle; // 重新绑定按钮事件 const restoredButton = document.getElementById('reader-toggle'); if (restoredButton) { restoredButton.addEventListener('click', () => { if (!isReading) { enterReader(); setAutoReaderStatus(true); } else { exitReader(); } }); } console.log('✅ 页面内容已恢复,目录缓存保留'); } else { // 如果没有保存的内容,刷新页面 console.log('⚠️ 没有保存的页面内容,刷新页面'); location.reload(); } } // 无缝章节加载:加载提示与错误显示 function showLoadingIndicator(message = '正在加载章节...') { try { // 移除已存在的加载提示 const existing = document.getElementById('chapter-loader'); if (existing) existing.remove(); const loader = document.createElement('div'); loader.id = 'chapter-loader'; loader.innerHTML = ` <div class="loader-content"> <div class="loader-spinner"></div> <div class="loader-text">${message}</div> </div> `; const style = document.createElement('style'); style.id = 'chapter-loader-style'; style.textContent = ` #chapter-loader { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.6); display:flex; align-items:center; justify-content:center; z-index:2147483648; } #chapter-loader .loader-content { color:#fff; padding:18px 24px; border-radius:10px; background: rgba(0,0,0,0.45); text-align:center; } #chapter-loader .loader-spinner { width:30px; height:30px; border:3px solid rgba(255,255,255,0.25); border-top:3px solid #fff; border-radius:50%; animation: yamibo-spin 1s linear infinite; margin:0 auto 8px } @keyframes yamibo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } #chapter-loader .loader-text { font-size:14px; } `; document.head.appendChild(style); document.body.appendChild(loader); } catch (e) { console.warn('showLoadingIndicator error', e); } } function hideLoadingIndicator() { try { const loader = document.getElementById('chapter-loader'); if (loader) { loader.remove(); } const style = document.getElementById('chapter-loader-style'); if (style) style.remove(); } catch (e) { // ignore } } function showErrorMessage(message) { try { const id = 'chapter-error-msg'; const existing = document.getElementById(id); if (existing) existing.remove(); const el = document.createElement('div'); el.id = id; el.style.cssText = 'position:fixed;left:50%;top:40%;transform:translate(-50%,-50%);background:#ff6b6b;color:#fff;padding:12px 18px;border-radius:8px;z-index:2147483649;font-size:14px;box-shadow:0 8px 30px rgba(0,0,0,0.4)'; el.textContent = message; document.body.appendChild(el); setTimeout(() => { try { el.style.opacity = '0'; setTimeout(() => el.remove(), 250); } catch (e) {} }, 2500); } catch (e) { console.warn('showErrorMessage error', e); } } // 无缝章节切换 async function loadNewChapter(chapterUrl, chapterTitle) { try { console.log('📖 开始无缝加载章节:', chapterTitle); showLoadingIndicator('正在加载章节...'); const response = await fetch(chapterUrl, { method: 'GET', credentials: 'include', headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3', 'Referer': window.location.href, 'User-Agent': navigator.userAgent } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // 提取图片 URL const newImages = collectImagesFromDocument(doc) .map(u => { if (!u) return null; let url = String(u); if (url.startsWith('//')) url = 'https:' + url; if (!/^https?:\/\//i.test(url)) url = 'https://bbs.yamibo.com/' + url.replace(/^\/+/, ''); if (url.startsWith('http://')) url = url.replace(/^http:\/\//, 'https://'); url = url.replace(/^https?:\/\/https?:\/\//, 'https://'); return url; }) .filter(Boolean); if (!newImages || newImages.length === 0) { hideLoadingIndicator(); showErrorMessage('未找到漫画图片,已跳转到原帖'); location.href = chapterUrl; return; } // 更新全局图片数组 images = newImages; // 尝试提取并保存新章节标题 const newH1 = doc.querySelector('h1.ts'); if (newH1) { const subjectSpan = newH1.querySelector('#thread_subject, span[id^="thread_subject"]'); savedThreadTitle = subjectSpan ? subjectSpan.textContent.trim() : newH1.textContent.trim(); } else { const newTitleEl = doc.querySelector('#thread_subject, span[id^="thread_subject"]'); if (newTitleEl) savedThreadTitle = newTitleEl.textContent.trim(); } // 更新浏览器地址栏(不刷新页面) try { window.history.pushState({}, '', chapterUrl); } catch (e) { /* ignore */ } // 只更新图片容器与页码 const container = document.getElementById('cw-container'); if (container) { container.innerHTML = ''; images.forEach((url, idx) => { const img = document.createElement('img'); img.src = url; img.loading = 'lazy'; img.alt = `page-${idx + 1}`; img.dataset.index = String(idx + 1); img.onload = function() { try { const naturalWidth = this.naturalWidth || 0; const viewportWidth = window.innerWidth || 1024; const oneThirdViewport = viewportWidth / 3; if (naturalWidth > 0 && naturalWidth < oneThirdViewport) this.setAttribute('data-original-size', 'small'); else this.removeAttribute('data-original-size'); } catch (e) { // ignore } }; container.appendChild(img); }); const pageInfo = document.getElementById('cw-page-info'); if (pageInfo) pageInfo.textContent = `1/${images.length}`; } // 滚动到顶部 window.scrollTo({ top: 0, behavior: 'smooth' }); hideLoadingIndicator(); console.log('🎉 章节加载完成:', chapterTitle); } catch (error) { console.error('❌ 加载章节失败:', error); hideLoadingIndicator(); showErrorMessage('加载章节失败: ' + (error && error.message ? error.message : String(error))); // 若失败,1s 后回退到原帖(避免卡死在阅读器) setTimeout(() => { try { location.href = chapterUrl; } catch (e) {} }, 1000); } } // 手动清理缓存功能 if (typeof unsafeWindow !== 'undefined') { unsafeWindow.clearAllCache = clearAllCache; unsafeWindow.viewCacheStatus = viewCacheStatus; } else { window.clearAllCache = clearAllCache; window.viewCacheStatus = viewCacheStatus; } })();