您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
漫画柜双页浏览,全屏显示,从右到左显示。横图跨页;点击半屏翻页;记忆进度;
// ==UserScript== // @name Manhuagui 阅读增强 · 全量加载/沉浸式双页排版/自动跨页显示 // @namespace http://tampermonkey.net/ // @version 4.7.0 // @description 漫画柜双页浏览,全屏显示,从右到左显示。横图跨页;点击半屏翻页;记忆进度; // @author akira0245 // @match https://www.manhuagui.com/comic/*/*.html // @match https://tw.manhuagui.com/comic/*/*.html // @match https://cn.manhuagui.com/comic/*/*.html // @icon https://www.google.com/s2/favicons?sz=64&domain=manhuagui.com // @run-at document-idle // @grant none // @license GPLv3 // ==/UserScript== (function () { 'use strict'; // ===================== 开发者常量(仅此处修改) ===================== const CONCURRENCY = 3; // 并发上限 const TIMEOUT_SEC = 15; // 单张图片超时(秒) // 默认阅读外观/行为(可在设置中修改并持久化) const DEFAULTS = { // 交互 clickNav: 'lr', // 点击翻页:off / lr(左右半屏) / ud(上下半屏) autoHideToolbar: false, // 自动隐藏菜单(右上角唤醒/短暂移动显示) snapWheel: false, // 一屏滚动:滚轮一次跨页 snapSmooth: true, // 一屏滚动时平滑滚动 showSpreadProgress: true, // 底部跨页进度 // 布局 crossAspect: 1.0, // 跨页判定阈值:w/h >= 阈值 => 跨页(默认 1.0) containerMaxW: 2200, // 阅读容器最大宽(px) pageGapH: 16, // 双页左右间距(px) spreadGapV: 16, // 行间上下间距(px) pageHeight: '100vh', // 单页高度(支持 calc) // 外观 bodyBg: '#0a0a0a', // 页面背景 galleryBg: '#0d0d0d', // 阅读区背景 showPN: true, // 显示页码 shadow: false, // 图片阴影 // 视图模式 double: true, // 双页模式 rtl: true, // 右到左 firstSingle: true // 首页单页 }; const LS_CFG = 'tmkReaderCfg.v43'; const LS_POS_PREFIX = 'tmkPos:'; // 记忆阅读位置 key 前缀 // ===================== 工具/检测 ===================== const pageSelect = document.querySelector('#pageSelect'); if (!pageSelect) return; const firstImg = document.querySelector('#mangaBox img.mangaFile, #mangaFile, #mangaMoreBox img[data-tag="mangaFile"]'); if (!firstImg) return; const firstURL = new URL(firstImg.src, location.href); const fallbackOrigin = firstURL.origin; const fallbackPath = firstURL.pathname.replace(/\/[^/]+$/, '/'); function waitFor(cond, timeout = 8000, interval = 60) { return new Promise((resolve) => { const t0 = Date.now(); const timer = setInterval(() => { if (cond()) { clearInterval(timer); resolve(true); } else if (Date.now() - t0 > timeout) { clearInterval(timer); resolve(false); } }, interval); }); } function getTotalPages() { const opts = Array.from(pageSelect.options); return opts.length ? parseInt(opts[opts.length - 1].value, 10) : 0; } function loadCfg() { try { const raw = localStorage.getItem(LS_CFG); return raw ? { ...DEFAULTS, ...JSON.parse(raw) } : { ...DEFAULTS }; } catch { return { ...DEFAULTS }; } } function saveCfg(cfg) { localStorage.setItem(LS_CFG, JSON.stringify(cfg)); } function chapterKey() { return LS_POS_PREFIX + location.pathname; } function savePos(pageIdx) { try { localStorage.setItem(chapterKey(), String(pageIdx)); } catch {} } function loadPos() { try { const n = parseInt(localStorage.getItem(chapterKey()) || '', 10); return Number.isFinite(n) ? n : null; } catch { return null; } } // ===================== SMH/pVars 读取真实URL ===================== function tryExtractConfFromPVars() { const pv = window.pVars; if (!pv || !pv.manga) return null; const m = pv.manga; const path = m.path || m.PATH || m.p || null; const sl = m.sl || m.SL || m.sign || null; let files = m.files || m.images || m.fs || null; const normalizeFiles = (val) => { if (!val) return null; if (Array.isArray(val)) return val.slice(); if (typeof val === 'string') { try { const arr = JSON.parse(val); if (Array.isArray(arr)) return arr; } catch {} let dec = null; if (window.LZString?.decompressFromBase64) { try { dec = LZString.decompressFromBase64(val); } catch {} } if (!dec && window.M?.W?.Z) { try { dec = window.M.W.Z(val); } catch {} } if (dec && typeof dec === 'string') { try { const arr = JSON.parse(dec); if (Array.isArray(arr)) return arr; } catch {} let arr = dec.split('|'); if (arr.length <= 1) arr = dec.split(','); arr = arr.map(s => s.trim()).filter(Boolean); if (arr.length) return arr; } } return null; }; const list = normalizeFiles(files); return { path: path || null, sl: sl || null, files: list }; } async function collectUrlsByGoPage(total) { const urls = new Array(total + 1); const hasSMH = !!(window.SMH && window.SMH.utils && typeof window.SMH.utils.goPage === 'function'); if (!hasSMH) return null; const conf = tryExtractConfFromPVars() || {}; const origin = fallbackOrigin; const path = conf.path || fallbackPath; const sl = conf.sl || {}; const waitCurFileChange = (prev, timeout = 1500) => new Promise((resolve) => { const t0 = Date.now(); const timer = setInterval(() => { if (window.pVars?.curFile && window.pVars.curFile !== prev) { clearInterval(timer); resolve(true); } else if (Date.now() - t0 > timeout) { clearInterval(timer); resolve(false); } }, 30); }); const readCurrentImgSrc = () => { const cur = document.querySelector('#mangaBox img.mangaFile, #mangaFile'); if (cur?.src) return cur.src; const more = document.querySelectorAll('#mangaMoreBox img[data-tag="mangaFile"]'); if (more && more.length) return more[more.length - 1].src; return null; }; ['#mangaBox', '#mangaMoreBox', '.pager', '.main-btn', '#imgLoading'].forEach(sel => { const el = document.querySelector(sel); if (el) el.style.display = 'none'; }); for (let i = 1; i <= total; i++) { const prev = window.pVars?.curFile || ''; try { window.SMH.utils.goPage(i); } catch { pageSelect.value = String(i); pageSelect.dispatchEvent(new Event('change', { bubbles: true })); } await waitCurFileChange(prev, 1500); const curFile = window.pVars?.curFile; if (curFile && (sl.e || sl.m)) { const q = new URLSearchParams(); if (sl.e) q.set('e', sl.e); if (sl.m) q.set('m', sl.m); urls[i] = `${origin}${path}${curFile}?${q.toString()}`; } else { urls[i] = readCurrentImgSrc() || ''; } } return urls; } // ===================== 主流程 ===================== (async function main() { const ok = await waitFor(() => window.SMH && window.pVars && document.querySelector('#mangaFile')); if (!ok) return; const total = getTotalPages(); if (!total) return; let conf = tryExtractConfFromPVars(); let pageUrls = null; if (conf && conf.files && conf.sl) { const origin = fallbackOrigin; const path = conf.path || fallbackPath; const q = new URLSearchParams(); if (conf.sl.e) q.set('e', conf.sl.e); if (conf.sl.m) q.set('m', conf.sl.m); pageUrls = conf.files.map(name => `${origin}${path}${name}?${q.toString()}`); pageUrls.unshift(''); if (pageUrls.length - 1 !== total) pageUrls = null; } if (!pageUrls) { pageUrls = await collectUrlsByGoPage(total); if (!pageUrls) return; } const cfg = loadCfg(); buildReaderUI(pageUrls, total, cfg); })(); // ===================== 阅读 UI + 加载器 ===================== function buildReaderUI(urls, totalPages, cfg) { // 样式(现代毛玻璃 + 自动隐藏 + 进度条 + 100vh + 贴中间 + 设置分组 + tooltip) const css = ` :root{ --page-height: ${cfg.pageHeight}; --spread-gap-v: ${cfg.spreadGapV}px; --page-gap-h: ${cfg.pageGapH}px; --max-container-width: ${cfg.containerMaxW}px; --gallery-bg: ${cfg.galleryBg}; } body { background: ${cfg.bodyBg}; } .tbCenter { border: none; } .tmk-toolbar { position: fixed; right: 16px; top: 16px; z-index: 99999; color: #fff; font-size: 13px; padding: 10px 12px; border-radius: 12px; line-height: 1.6; background: rgba(24,24,24,.55); border: 1px solid rgba(255,255,255,.08); backdrop-filter: blur(10px); box-shadow: 0 10px 28px rgba(0,0,0,.35); transition: opacity .25s ease, transform .25s ease; } .tmk-toolbar.auto-hide { opacity: 0; transform: translateY(-8px); pointer-events: none; } .tmk-toolbar.auto-hide.show { opacity: 1; transform: translateY(0); pointer-events: auto; } .tmk-reveal { position: fixed; right: 0; top: 0; width: 70px; height: 70px; z-index: 99998; } .tmk-toolbar .row { display:flex; align-items:center; gap:8px; flex-wrap:wrap; } .tmk-toolbar .btn { color:#fff; background: linear-gradient(180deg,#515151,#444); border: 1px solid rgba(255,255,255,.12); padding: 6px 10px; border-radius: 8px; cursor:pointer; } .tmk-toolbar .btn:hover { filter: brightness(1.08); } .tmk-toolbar .sep { width:1px; height:18px; background: rgba(255,255,255,.15); margin: 0 6px; } .tmk-toolbar label { display:flex; align-items:center; gap:6px; user-select:none; cursor:pointer; } .tmk-gallery { width: min(98vw, var(--max-container-width)); margin: 0 auto; padding: 8px 8px 120px; background: var(--gallery-bg); transition: background .2s ease; } .tmk-gallery .tmk-spread { width: 100%; margin-bottom: var(--spread-gap-v); } .tmk-gallery img { display: block; height: var(--page-height); width: auto; object-fit: contain; background: transparent; } .tmk-shadow img { box-shadow: 0 8px 24px rgba(0,0,0,.45); transition: box-shadow .2s ease; } .tmk-gallery.hide-pn .pn { display:none; } /* 单页模式:一行一页,居中 */ .tmk-gallery.single .tmk-spread.single { display:flex; justify-content:center; } .tmk-gallery.single .tmk-spread.single .page-slot { position: relative; height: var(--page-height); } /* 双页:pair 组整体居中,“加载前半屏占位,加载后按图宽收缩” */ .tmk-gallery.double .tmk-spread.pair { display:flex; gap: var(--page-gap-h); justify-content:center; } .tmk-gallery.double .tmk-spread.pair.rtl { flex-direction: row-reverse; } .tmk-gallery.double .tmk-spread.pair .page-slot { position: relative; height: var(--page-height); flex: 0 0 auto; width: calc((100% - var(--page-gap-h)) / 2); max-width: calc((100% - var(--page-gap-h)) / 2); overflow: hidden; } .tmk-gallery.double .tmk-spread.pair .page-slot.loaded { width: auto; max-width: none; } /* 双页中的单页(half slot),靠边(通过row-reverse控制靠右) */ .tmk-gallery.double .tmk-spread.single-only { display:flex; justify-content:center; } .tmk-gallery.double .tmk-spread.single-only.rtl { flex-direction: row-reverse; } .tmk-gallery.double .tmk-spread.single-only .page-slot { position: relative; height: var(--page-height); flex: 0 0 auto; width: calc((100% - var(--page-gap-h)) / 2); max-width: calc((100% - var(--page-gap-h)) / 2); overflow: hidden; } .tmk-gallery.double .tmk-spread.single-only .page-slot.loaded { width: calc((100% - var(--page-gap-h)) / 2); max-width: calc((100% - var(--page-gap-h)) / 2); } /* 跨页:整行居中 */ .tmk-gallery.double .tmk-spread.full { display:flex; justify-content:center; } .tmk-gallery.double .tmk-spread.full .page-slot { position: relative; height: var(--page-height); width: 100%; max-width: 100%; } /* 占位符骨架 */ .page-slot .ph { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; flex-direction:column; color:#9aa; font-size:13px; background: linear-gradient(180deg,#0f0f0f,#0b0b0b); } .page-slot.loaded .ph { display:none; } .ph .ring { width: 28px; height: 28px; border: 3px solid rgba(255,255,255,0.2); border-top-color:#bbb; border-radius:50%; animation: tmk-spin 0.9s linear infinite; margin-bottom:8px; } @keyframes tmk-spin { to { transform: rotate(360deg); } } /* 页码徽章 */ .pn { position:absolute; left:50%; transform: translateX(-50%); bottom: 6px; padding: 2px 8px; font-size: 12px; color:#eee; background: rgba(0,0,0,.45); border-radius: 12px; pointer-events:none; } /* 底部跨页进度 */ .tmk-bottom-progress { position: fixed; left: 50%; transform: translateX(-50%); bottom: 14px; z-index: 99990; color:#eee; font-size: 12px; padding: 4px 10px; background: rgba(20,20,20,.55); backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,.08); border-radius: 999px; box-shadow: 0 6px 18px rgba(0,0,0,.35); } .tmk-bottom-progress.hidden { display:none; } /* 下一章提示按钮 */ .tmk-next-chapter { position: fixed; right: 24px; bottom: 20px; z-index: 99991; color:#fff; font-size: 14px; padding: 8px 12px; background: linear-gradient(180deg,#4e8a3f,#3f6d32); border: 1px solid rgba(255,255,255,.12); border-radius: 999px; box-shadow: 0 6px 18px rgba(0,0,0,.35); cursor: pointer; display:none; } .tmk-next-chapter.show { display:block; } /* 悬浮提示 */ .tmk-floating-hint { position: fixed; left: 50%; transform: translateX(-50%); bottom: 80px; z-index: 99992; color:#fff; font-size: 14px; padding: 12px 20px; background: rgba(20,20,20,.85); border: 1px solid rgba(255,255,255,.15); border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,.4); backdrop-filter: blur(8px); text-align: center; line-height: 1.4; opacity: 0; transform: translateX(-50%) translateY(20px); transition: opacity .3s ease, transform .3s ease; pointer-events: none; max-width: 300px; } .tmk-floating-hint.show { opacity: 1; transform: translateX(-50%) translateY(0); } /* 设置面板 */ .tmk-modal { position: fixed; inset:0; background: rgba(0,0,0,.35); z-index: 100000; display:none; align-items:center; justify-content:center; } .tmk-modal.show { display:flex; } .tmk-dialog { width: min(92vw, 720px); max-height: 88vh; overflow:auto; background: rgba(26,26,26,.7); color:#eee; border-radius:12px; box-shadow: 0 10px 28px rgba(0,0,0,.4); border: 1px solid rgba(255,255,255,.08); backdrop-filter: blur(16px); padding: 16px 18px 12px; } .tmk-dialog h3 { margin: 6px 0 6px; font-size: 16px; opacity: .9; } .tmk-section { margin: 10px 0 12px; border: 1px solid rgba(255,255,255,.08); border-radius: 10px; padding: 10px 12px; background: rgba(14,14,14,.35); } .tmk-section-title { font-size: 13px; opacity: .85; margin: 0 0 6px 0; } .tmk-dialog .row { display:flex; align-items:center; gap:10px; margin: 8px 0; flex-wrap:wrap; } .tmk-dialog label { min-width: 180px; display:flex; align-items:center; gap:6px; } .tmk-dialog input[type="number"], .tmk-dialog input[type="text"], .tmk-dialog select { padding: 6px 8px; border-radius: 8px; border: 1px solid #444; background:#111; color:#eee; width: 180px; } .tmk-dialog input[type="color"] { width: 44px; height: 28px; border: none; background: transparent; } .tmk-dialog .actions { display:flex; justify-content:flex-end; gap:8px; margin-top: 12px; } .tmk-dialog .btn { color:#fff; background: linear-gradient(180deg,#515151,#444); border:1px solid rgba(255,255,255,.12); padding: 6px 12px; border-radius:8px; cursor:pointer; } .tmk-dialog .btn:hover { filter: brightness(1.08); } .tmk-dialog .btn.warn { background: linear-gradient(180deg,#8a3838,#6a2b2b); } .tmk-dialog .btn.warn:hover { filter: brightness(1.08); } /* help 提示 */ .help { display:inline-flex; align-items:center; justify-content:center; width:18px; height:18px; font-size:12px; border-radius:50%; background: rgba(255,255,255,.15); color:#fff; cursor: help; position:relative; } .help:hover::after { content: attr(data-tip); white-space: pre-line; position:absolute; left: -20px; right: auto; transform: none; bottom: 130%; min-width: 220px; max-width: 420px; background: rgba(20,20,20,.95); color:#eee; padding: 8px 10px; border-radius: 8px; border: 1px solid rgba(255,255,255,.08); box-shadow: 0 10px 28px rgba(0,0,0,.35); text-align: left; line-height: 1.4; z-index: 1; } /* 关掉原站元素 */ #mangaBox, #mangaMoreBox, #loading, .pager, .main-btn { display: none !important; } `; const style = document.createElement('style'); style.textContent = css; document.head.appendChild(style); // 工具条 const toolbar = document.createElement('div'); toolbar.className = 'tmk-toolbar'; toolbar.innerHTML = ` <div class="row"> <label><input type="checkbox" id="tmk-double" ${cfg.double ? 'checked' : ''}> 双页</label> <label><input type="checkbox" id="tmk-rtl" ${cfg.rtl ? 'checked' : ''}> 右到左</label> <label><input type="checkbox" id="tmk-first-single" ${cfg.firstSingle ? 'checked' : ''}> 首页单页</label> <div class="sep"></div> <button class="btn" id="tmk-prev">上一跨页</button> <button class="btn" id="tmk-next">下一跨页</button> <div class="sep"></div> <button class="btn" id="tmk-settings">设置</button> <div id="tmk-progress" style="margin-left:6px;opacity:.85;">加载 0/${totalPages}</div> </div> `; document.body.appendChild(toolbar); // 自动隐藏唤醒区 const reveal = document.createElement('div'); reveal.className = 'tmk-reveal'; document.body.appendChild(reveal); // 设置面板(分组+帮助+四个按钮) const modal = document.createElement('div'); modal.className = 'tmk-modal'; modal.innerHTML = ` <div class="tmk-dialog"> <h3>阅读设置</h3> <div class="tmk-section"> <div class="tmk-section-title">交互</div> <div class="row"> <label>点击翻页 <span class="help" data-tip="点击屏幕的半区来翻页。左右半屏:左=上一跨页,右=下一跨页。上下半屏:上=上一跨页,下=下一跨页。">?</span></label> <select id="cfg-click-nav"> <option value="off" ${cfg.clickNav==='off'?'selected':''}>禁用</option> <option value="lr" ${cfg.clickNav==='lr'?'selected':''}>左右半屏</option> <option value="ud" ${cfg.clickNav==='ud'?'selected':''}>上下半屏</option> </select> </div> <div class="row"> <label>自动隐藏菜单栏 <span class="help" data-tip="开启后,右上角菜单仅在鼠标移动或将鼠标移到右上角唤醒区时显示。">?</span></label> <input type="checkbox" id="cfg-autohide" ${cfg.autoHideToolbar?'checked':''}> </div> <div class="row"> <label>一屏滚动 <span class="help" data-tip="开启后,滚轮一次滚动将按“跨页”为单位进行。">?</span></label> <input type="checkbox" id="cfg-snap" ${cfg.snapWheel?'checked':''}> </div> <div class="row"> <label>一屏滚动时平滑滚动 <span class="help" data-tip="与“一屏滚动”配合:若开启,将平滑滚动到下一跨页;关闭则瞬间跳转。">?</span></label> <input type="checkbox" id="cfg-snap-smooth" ${cfg.snapSmooth?'checked':''}> </div> <div class="row"> <label>底部跨页进度 <span class="help" data-tip="在底部显示当前跨页序号/总跨页数。">?</span></label> <input type="checkbox" id="cfg-sp" ${cfg.showSpreadProgress?'checked':''}> </div> </div> <div class="tmk-section"> <div class="tmk-section-title">快捷键说明 <span class="help" data-tip="← / →:上一/下一页 Space / Enter:下一页 D:切换单双页 S:切换页面奇偶 F:全屏开/关 Q:打开/关闭设置面板 W:切换显示页码 E:切换‘右到左’方向 G:跳转到页码 Home / End:跳到首/尾跨页">?</span></div> </div> <div class="tmk-section"> <div class="tmk-section-title">布局</div> <div class="row"> <label>跨页判定阈值(w/h) <span class="help" data-tip="当图片宽高比≥此阈值时视为“横图”,在双页模式下占据整行(跨页)。默认1.0即横图触发。">?</span></label> <input type="number" id="cfg-cross" min="1.0" max="2.0" step="0.01" value="${cfg.crossAspect}"> </div> <div class="row"> <label>双页左右间距(px)</label><input type="number" id="cfg-gap-h" min="0" max="64" step="1" value="${cfg.pageGapH}"> </div> <div class="row"> <label>行间上下间距(px)</label><input type="number" id="cfg-gap-v" min="0" max="64" step="1" value="${cfg.spreadGapV}"> </div> <div class="row"> <label>单页高度(CSS) <span class="help" data-tip="单个页面的显示高度。例如 100vh 表示屏幕高;也可写 calc(100vh - 64px)。">?</span></label> <input type="text" id="cfg-page-height" value="${cfg.pageHeight}"> </div> <div class="row"> <label>最大容器宽(px) <span class="help" data-tip="阅读区域的最大宽度上限。可根据屏幕调整。">?</span></label> <input type="number" id="cfg-maxw" min="800" max="5000" step="10" value="${cfg.containerMaxW}"> </div> </div> <div class="tmk-section"> <div class="tmk-section-title">外观</div> <div class="row"><label>显示页码</label><input type="checkbox" id="cfg-pn" ${cfg.showPN ? 'checked' : ''}></div> <div class="row"><label>图片阴影</label><input type="checkbox" id="cfg-shadow" ${cfg.shadow ? 'checked' : ''}></div> <div class="row"> <label>页面背景色</label><input type="color" id="cfg-body-bg" value="${toColor(cfg.bodyBg)}"><span>${cfg.bodyBg}</span> </div> <div class="row"> <label>容器背景色 <span class="help" data-tip="阅读容器(图片区域)背景色。">?</span></label><input type="color" id="cfg-gallery-bg" value="${toColor(cfg.galleryBg)}"><span>${cfg.galleryBg}</span> </div> </div> <div class="tmk-section"> <div class="tmk-section-title">视图模式</div> <div class="row"> <label><input type="checkbox" id="tmk-double" ${cfg.double ? 'checked' : ''}> 双页</label> <label><input type="checkbox" id="tmk-rtl" ${cfg.rtl ? 'checked' : ''}> 右到左</label> <label><input type="checkbox" id="tmk-first-single" ${cfg.firstSingle ? 'checked' : ''}> 首页单页</label> </div> </div> <div class="actions"> <button class="btn warn" id="cfg-reset">重置</button> <button class="btn" id="cfg-cancel">取消</button> <button class="btn" id="cfg-apply">应用</button> <button class="btn" id="cfg-save-close">保存并关闭</button> </div> </div> `; document.body.appendChild(modal); function toColor(v){const s=String(v||'').trim();return /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(s)?s:'#000000';} // 底部跨页进度 const spreadProg = document.createElement('div'); spreadProg.className = 'tmk-bottom-progress' + (cfg.showSpreadProgress?'':' hidden'); spreadProg.textContent = ''; document.body.appendChild(spreadProg); // "下一章"按钮 const nextChapterBtn = document.createElement('div'); nextChapterBtn.className = 'tmk-next-chapter'; nextChapterBtn.textContent = '下一章 ▶'; nextChapterBtn.addEventListener('click', goNextChapter); document.body.appendChild(nextChapterBtn); // 悬浮提示 const floatingHint = document.createElement('div'); floatingHint.className = 'tmk-floating-hint'; floatingHint.textContent = '再次翻页进入下一章'; document.body.appendChild(floatingHint); // 容器 const gallery = document.createElement('div'); gallery.className = 'tmk-gallery'; if (cfg.shadow) gallery.classList.add('tmk-shadow'); if (!cfg.showPN) gallery.classList.add('hide-pn'); document.querySelector('#tbBox')?.appendChild(gallery) || document.body.appendChild(gallery); // 状态 let isDouble = cfg.double; let isRTL = cfg.rtl; let firstSingle = cfg.firstSingle; let active = 0; let loadedCount = 0; // 悬浮提示状态 let isFloatingHintVisible = false; let floatingHintTimer = null; const state = new Array(totalPages + 1).fill(0); // 0 pending, 1 loading, 2 done const attempts = new Array(totalPages + 1).fill(0); const nextTime = new Array(totalPages + 1).fill(0); const dims = new Array(totalPages + 1).fill(null); const isWide = new Array(totalPages + 1).fill(null); let spreadsCache = []; let lastLayoutRows = []; // 工具条按钮 const progressEl = document.querySelector('#tmk-progress'); const btnPrev = toolbar.querySelector('#tmk-prev'); const btnNext = toolbar.querySelector('#tmk-next'); const btnSettings = toolbar.querySelector('#tmk-settings'); btnPrev.addEventListener('click', () => scrollToSpread(nearestSpread() - 1, true)); btnNext.addEventListener('click', () => scrollToSpread(nearestSpread() + 1, true)); btnSettings.addEventListener('click', () => { if (cfg.autoHideToolbar) toolbar.classList.add('show'); modal.classList.add('show'); }); // 右上角工具栏复选框事件监听 const toolbarDouble = toolbar.querySelector('#tmk-double'); const toolbarRTL = toolbar.querySelector('#tmk-rtl'); const toolbarFirst = toolbar.querySelector('#tmk-first-single'); toolbarDouble.addEventListener('change', () => { isDouble = toolbarDouble.checked; cfg.double = isDouble; saveCfg(cfg); // 同步设置面板 modal.querySelector('#tmk-double').checked = isDouble; render(true, { anchorMode: 'page' }); }); toolbarRTL.addEventListener('change', () => { isRTL = toolbarRTL.checked; cfg.rtl = isRTL; saveCfg(cfg); // 同步设置面板 modal.querySelector('#tmk-rtl').checked = isRTL; render(true, { anchorMode: 'page' }); }); toolbarFirst.addEventListener('change', () => { firstSingle = toolbarFirst.checked; cfg.firstSingle = firstSingle; saveCfg(cfg); // 同步设置面板 modal.querySelector('#tmk-first-single').checked = firstSingle; render(true, { anchorMode: 'spread' }); }); // 自动隐藏:唤醒逻辑 function toolbarShow(show){ if (!cfg.autoHideToolbar) return; toolbar.classList.toggle('show', !!show); } function applyAutohide(){ toolbar.classList.toggle('auto-hide', cfg.autoHideToolbar); if (!cfg.autoHideToolbar) toolbar.classList.add('show'); } applyAutohide(); let hoverTimer = null, lastMove = 0; let isToolbarHover = false; const showFor = (ms)=>{ toolbarShow(true); clearTimeout(hoverTimer); hoverTimer = setTimeout(()=>{ if (!isToolbarHover) toolbarShow(false); }, ms); }; reveal.addEventListener('mouseenter', ()=> toolbarShow(true)); reveal.addEventListener('mouseleave', ()=> toolbarShow(false)); toolbar.addEventListener('mouseenter', ()=> { isToolbarHover = true; toolbarShow(true); clearTimeout(hoverTimer); }); toolbar.addEventListener('mouseleave', ()=> { isToolbarHover = false; toolbarShow(false); }); window.addEventListener('mousemove', ()=>{ if (!cfg.autoHideToolbar) return; if (Date.now() - lastMove > 800) toolbarShow(true); lastMove = Date.now(); showFor(1500); }); // 设置面板逻辑 const ui = { clickNav: modal.querySelector('#cfg-click-nav'), autohide: modal.querySelector('#cfg-autohide'), snap: modal.querySelector('#cfg-snap'), snapSmooth: modal.querySelector('#cfg-snap-smooth'), gapH: modal.querySelector('#cfg-gap-h'), gapV: modal.querySelector('#cfg-gap-v'), pageH: modal.querySelector('#cfg-page-height'), maxW: modal.querySelector('#cfg-maxw'), cross: modal.querySelector('#cfg-cross'), pn: modal.querySelector('#cfg-pn'), shadow: modal.querySelector('#cfg-shadow'), bodyBg: modal.querySelector('#cfg-body-bg'), galleryBg: modal.querySelector('#cfg-gallery-bg'), sp: modal.querySelector('#cfg-sp'), cancel: modal.querySelector('#cfg-cancel'), reset: modal.querySelector('#cfg-reset'), apply: modal.querySelector('#cfg-apply'), saveClose: modal.querySelector('#cfg-save-close'), chkDouble: modal.querySelector('#tmk-double'), chkRTL: modal.querySelector('#tmk-rtl'), chkFirst: modal.querySelector('#tmk-first-single') }; modal.addEventListener('click', (e)=>{ if(e.target===modal) modal.classList.remove('show'); }); ui.reset.addEventListener('click', ()=>{ // 重置输入框到默认值,但不立即应用 ui.clickNav.value = DEFAULTS.clickNav; ui.autohide.checked = DEFAULTS.autoHideToolbar; ui.snap.checked = DEFAULTS.snapWheel; ui.snapSmooth.checked = DEFAULTS.snapSmooth; ui.gapH.value = DEFAULTS.pageGapH; ui.gapV.value = DEFAULTS.spreadGapV; ui.pageH.value = DEFAULTS.pageHeight; ui.maxW.value = DEFAULTS.containerMaxW; ui.cross.value = DEFAULTS.crossAspect; ui.pn.checked = DEFAULTS.showPN; ui.shadow.checked = DEFAULTS.shadow; ui.bodyBg.value = toColor(DEFAULTS.bodyBg); ui.bodyBg.nextElementSibling.textContent = DEFAULTS.bodyBg; ui.galleryBg.value = toColor(DEFAULTS.galleryBg); ui.galleryBg.nextElementSibling.textContent = DEFAULTS.galleryBg; ui.sp.checked = DEFAULTS.showSpreadProgress; ui.chkDouble.checked = DEFAULTS.double; ui.chkRTL.checked = DEFAULTS.rtl; ui.chkFirst.checked = DEFAULTS.firstSingle; }); ui.cancel.addEventListener('click', ()=> modal.classList.remove('show')); function pickFormToCfg() { return { ...cfg, clickNav: ui.clickNav.value, autoHideToolbar: !!ui.autohide.checked, snapWheel: !!ui.snap.checked, snapSmooth: !!ui.snapSmooth.checked, pageGapH: clamp(+ui.gapH.value||0,0,64), spreadGapV: clamp(+ui.gapV.value||0,0,64), pageHeight: (ui.pageH.value||'100vh').trim(), containerMaxW: clamp(+ui.maxW.value||DEFAULTS.containerMaxW,800,5000), crossAspect: clamp(+ui.cross.value||DEFAULTS.crossAspect,1.0,2.0), showPN: !!ui.pn.checked, shadow: !!ui.shadow.checked, bodyBg: ui.bodyBg.value || DEFAULTS.bodyBg, galleryBg: ui.galleryBg.value || DEFAULTS.galleryBg, showSpreadProgress: !!ui.sp.checked, double: !!ui.chkDouble.checked, rtl: !!ui.chkRTL.checked, firstSingle: !!ui.chkFirst.checked }; } ui.apply.addEventListener('click', ()=>{ Object.assign(cfg, pickFormToCfg()); // 同步运行时状态(与保存一致) isDouble = cfg.double; isRTL = cfg.rtl; firstSingle = cfg.firstSingle; applyCfg(); rejudgeWide(); render(true, { anchorMode: 'page' }); showFor(1200); // 提示显示菜单 }); ui.saveClose.addEventListener('click', ()=>{ Object.assign(cfg, pickFormToCfg()); // 同步运行时状态 isDouble = cfg.double; isRTL = cfg.rtl; firstSingle = cfg.firstSingle; saveCfg(cfg); applyCfg(); rejudgeWide(); render(true, { anchorMode: 'page' }); modal.classList.remove('show'); showFor(1200); }); function clamp(v,a,b){return Math.min(b,Math.max(a,v));} // 外观/行为应用 function applyCfg(){ // 根变量刷新 style.textContent = style.textContent.replace(/:root\{[\s\S]*?\}/, () => { return `:root{ --page-height: ${cfg.pageHeight}; --spread-gap-v: ${cfg.spreadGapV}px; --page-gap-h: ${cfg.pageGapH}px; --max-container-width: ${cfg.containerMaxW}px; --gallery-bg: ${cfg.galleryBg}; }`; }); document.body.style.background = cfg.bodyBg; gallery.style.background = cfg.galleryBg; gallery.classList.toggle('tmk-shadow', cfg.shadow); gallery.classList.toggle('hide-pn', !cfg.showPN); spreadProg.classList.toggle('hidden', !cfg.showSpreadProgress); toolbar.classList.toggle('auto-hide', cfg.autoHideToolbar); if (!cfg.autoHideToolbar) toolbar.classList.add('show'); // color旁边显示文本 ui.bodyBg.nextElementSibling.textContent = cfg.bodyBg; ui.galleryBg.nextElementSibling.textContent = cfg.galleryBg; // 视图模式(复选框同步到主工具条) const mainDouble = toolbar.querySelector('#tmk-double'); const mainRTL = toolbar.querySelector('#tmk-rtl'); const mainFirst = toolbar.querySelector('#tmk-first-single'); if (mainDouble) mainDouble.checked = cfg.double; if (mainRTL) mainRTL.checked = cfg.rtl; if (mainFirst) mainFirst.checked = cfg.firstSingle; // 一屏滚动绑定 bindSnapWheel(cfg.snapWheel); } // ========== 布局 & 渲染 ========== function computeLayout() { const rows = []; if (!isDouble) { for (let i = 1; i <= totalPages; i++) rows.push({ type: 'single', pages: [i] }); return rows; } let i = 1; // 期望的配对奇偶性:当首页单页启用时,配对应从偶数页开始(即 1 单,后续 2-3、4-5...)。 // 若首页单页关闭,则配对从奇数页开始(即 1-2、3-4...)。 const desiredPairStartParity = firstSingle ? 0 /* even */ : 1 /* odd */; if (i <= totalPages) { const w = isWide[i] === true; if (firstSingle && !w) { rows.push({ type: 'single-only', pages: [i] }); i++; } } while (i <= totalPages) { const w = isWide[i] === true; if (w) { rows.push({ type: 'full', pages: [i] }); i++; continue; } // 当下一页是跨页(full)时,仍需在进入pair前按期望奇偶性对齐 if ((i % 2) !== desiredPairStartParity) { rows.push({ type: 'single-only', pages: [i] }); i++; continue; } if (i + 1 <= totalPages && isWide[i + 1] === true) { rows.push({ type: 'single-only', pages: [i] }); i++; continue; } rows.push({ type: 'pair', pages: [i, i + 1].filter(x => x <= totalPages) }); i += 2; } return rows; } function getAnchorPageIdx() { const slots = Array.from(document.querySelectorAll('.page-slot[data-idx]')); if (!slots.length) return 1; let best=1, dst=Infinity; for (const el of slots) { const d = Math.abs(el.getBoundingClientRect().top); if (d < dst) { dst=d; best=parseInt(el.dataset.idx,10); } } return best; } function pageToSpreadIndex(pageIdx) { for (let s=0; s<lastLayoutRows.length; s++) { if (lastLayoutRows[s].pages.includes(pageIdx)) return s; } return 0; } function render(preserve=false, opts={}) { const anchorMode = opts && opts.anchorMode === 'spread' ? 'spread' : 'page'; // 预记录锚点:页面或跨页容器 let beforeScrollTop = 0; let beforeOffset = 0; let beforeSpreadIdx = null; let anchorPage = null; if (preserve) { beforeScrollTop = window.scrollY; if (anchorMode === 'spread') { beforeSpreadIdx = nearestSpread(); if (beforeSpreadIdx != null && spreadsCache[beforeSpreadIdx]) { const el = spreadsCache[beforeSpreadIdx]; const r = el.getBoundingClientRect(); beforeOffset = Math.min(Math.max(-r.top, 0), Math.max(0, r.height - 1)); } } else { anchorPage = getAnchorPageIdx(); if (anchorPage != null) { const el = document.querySelector(`.page-slot[data-idx="${anchorPage}"]`); if (el) { const r = el.getBoundingClientRect(); beforeOffset = Math.min(Math.max(-r.top, 0), Math.max(0, r.height - 1)); } } } } gallery.classList.toggle('double', isDouble); gallery.classList.toggle('single', !isDouble); // 收集现有的 page-slot,按 idx 复用,避免图片重新加载 const existingSlots = new Map(); const oldSlots = Array.from(gallery.querySelectorAll('.page-slot[data-idx]')); for (const el of oldSlots) { const idx = parseInt(el.getAttribute('data-idx')||'0',10); if (idx) existingSlots.set(idx, el); } function getOrCreateSlot(i){ let slot = existingSlots.get(i); if (slot) { // 确保有页码徽章节点(若之前未创建且当前需要显示) if (cfg.showPN && !slot.querySelector('.pn')) { const pn = document.createElement('div'); pn.className = 'pn'; pn.textContent = i; slot.appendChild(pn); } // 若该页已加载完成,但 img 仍无 src(可能因DOM搬移时机导致onload阶段未能写入),则在此补齐 if (state[i] === 2) { const img = slot.querySelector('img[data-idx]'); if (img && !img.getAttribute('src')) img.setAttribute('src', urls[i]); slot.classList.add('loaded'); } return slot; } // 不存在则新建(仅首次或极少数情况) return makePageSlot(i); } spreadsCache = []; lastLayoutRows = computeLayout(); const frag = document.createDocumentFragment(); for (const row of lastLayoutRows) { const spread = document.createElement('div'); spread.className = `tmk-spread ${row.type}`; if (isDouble && (row.type === 'pair' || row.type === 'single-only') && isRTL) spread.classList.add('rtl'); if (row.type === 'single') { spread.appendChild(getOrCreateSlot(row.pages[0])); } else if (row.type === 'full') { spread.appendChild(getOrCreateSlot(row.pages[0])); } else if (row.type === 'single-only') { spread.appendChild(getOrCreateSlot(row.pages[0])); } else if (row.type === 'pair') { const [a,b] = row.pages; spread.appendChild(getOrCreateSlot(a)); if (b) spread.appendChild(getOrCreateSlot(b)); } frag.appendChild(spread); spreadsCache.push(spread); } // 用新布局替换旧内容(节点移动,不重新创建已有图片节点) while (gallery.firstChild) gallery.removeChild(gallery.firstChild); gallery.appendChild(frag); if (preserve) { if (anchorMode === 'spread' && beforeSpreadIdx != null && spreadsCache[beforeSpreadIdx]) { const el = spreadsCache[beforeSpreadIdx]; const r = el.getBoundingClientRect(); const top = beforeScrollTop + r.top + beforeOffset; window.scrollTo({ top, behavior: 'auto' }); } else if (anchorMode === 'page' && anchorPage != null) { const el = document.querySelector(`.page-slot[data-idx="${anchorPage}"]`); if (el) { const r = el.getBoundingClientRect(); const top = beforeScrollTop + r.top + beforeOffset; window.scrollTo({ top, behavior: 'auto' }); } } } else { const saved = loadPos(); if (saved && saved>=1 && saved<=totalPages) { const si = pageToSpreadIndex(saved); setTimeout(()=> scrollToSpread(si, true), 60); } } tick(); // 加载调度 updateSpreadProgress(); checkEndHint(); } function makePageSlot(i) { const slot = document.createElement('div'); slot.className = `page-slot ${state[i] === 2 ? 'loaded' : ''}`; slot.dataset.idx = String(i); const ph = document.createElement('div'); ph.className = 'ph'; ph.innerHTML = `<div class="ring"></div><div class="txt">加载中…</div>`; slot.appendChild(ph); const img = document.createElement('img'); img.alt = `第 ${i} 页`; img.dataset.idx = String(i); if (state[i] === 2) img.src = urls[i]; slot.appendChild(img); if (cfg.showPN) { const pn = document.createElement('div'); pn.className = 'pn'; pn.textContent = i; slot.appendChild(pn); } return slot; } function nearestSpread() { let nearest = 0, best = Infinity; spreadsCache.forEach((el,i)=>{ const d = Math.abs(el.getBoundingClientRect().top); if (d < best) {best=d; nearest=i;} }); return nearest; } function scrollToSpread(idx, smooth = true) { if (!spreadsCache.length) return; const clamped = Math.max(0, Math.min(idx, spreadsCache.length - 1)); // 检查是否尝试翻到最后一页之后 if (idx >= spreadsCache.length) { // 如果悬浮提示已显示,则进入下一章 if (isFloatingHintVisible) { hideFloatingHint(); goNextChapter(); return; } else { // 否则显示悬浮提示,并滚动到最后一页 showFloatingHint(); spreadsCache[spreadsCache.length - 1].scrollIntoView({ behavior: smooth ? 'smooth' : 'auto', block: 'start' }); } } else { // 正常翻页,隐藏悬浮提示 hideFloatingHint(); spreadsCache[clamped].scrollIntoView({ behavior: smooth ? 'smooth' : 'auto', block: 'start' }); } setTimeout(()=>{ updateSpreadProgress(); checkEndHint(); }, 60); } function updateSpreadProgress(){ const si = nearestSpread(); const row = lastLayoutRows[si]; spreadProg.textContent = `跨页 ${si+1} / ${spreadsCache.length || 0}`; if (row && row.pages && row.pages.length) savePos(row.pages[0]); } function checkEndHint() { // 当最后一个跨页已在视口内底部区域,显示"下一章" const last = spreadsCache[spreadsCache.length - 1]; if (!last) { nextChapterBtn.classList.remove('show'); hideFloatingHint(); return; } const rect = last.getBoundingClientRect(); const vh = window.innerHeight; const nearBottom = rect.top < vh * 0.6 && rect.bottom <= vh + 80; // or 已经滚到底 const atEnd = (window.scrollY + vh) >= (document.documentElement.scrollHeight - 4); if (nearBottom || atEnd) { nextChapterBtn.classList.add('show'); // 注意:悬浮提示只在用户主动翻页时显示,这里不显示 } else { nextChapterBtn.classList.remove('show'); hideFloatingHint(); } } function goNextChapter() { if (window.SMH?.nextC) { try { window.SMH.nextC(); return; } catch {} } const a = document.querySelector('.main-btn .nextC, .pager .next'); if (a) { a.click(); return; } // 兜底:跳回目录 const list = document.querySelector('#viewList'); if (list) location.href = list.href; } // 悬浮提示控制函数 function showFloatingHint() { if (isFloatingHintVisible) return; isFloatingHintVisible = true; floatingHint.classList.add('show'); // 3秒后自动隐藏 clearTimeout(floatingHintTimer); floatingHintTimer = setTimeout(() => { hideFloatingHint(); }, 3000); } function hideFloatingHint() { if (!isFloatingHintVisible) return; isFloatingHintVisible = false; floatingHint.classList.remove('show'); clearTimeout(floatingHintTimer); } // 键盘 & 点击翻页 let fullscreenAnchor = null; window.addEventListener('keydown', (e)=>{ if (e.target && /INPUT|TEXTAREA|SELECT/.test(e.target.tagName)) return; switch (e.key) { case 'ArrowLeft': scrollToSpread(nearestSpread()-1, true); e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); break; case 'ArrowRight': scrollToSpread(nearestSpread()+1, true); e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); break; case ' ': scrollToSpread(nearestSpread()+1, true); e.preventDefault(); break; case 'Enter': scrollToSpread(nearestSpread()+1, true); e.preventDefault(); break; case 'Home': scrollToSpread(0, true); e.preventDefault(); break; case 'End': scrollToSpread(spreadsCache.length-1, true); e.preventDefault(); break; case 'e': case 'E': // 切换 RTL(E)=> 触发工具条复选框 if (toolbarRTL) { toolbarRTL.click(); e.preventDefault(); } break; case 'f': case 'F': fullscreenAnchor = { page: getAnchorPageIdx() }; if (!document.fullscreenElement) document.documentElement.requestFullscreen().catch(()=>{}); else document.exitFullscreen().catch(()=>{}); break; case 'g': case 'G': const to = prompt(`跳转到页码 (1-${totalPages}):`); if (!to) break; const n = parseInt(to,10); if (Number.isFinite(n) && n>=1 && n<=totalPages) scrollToSpread(pageToSpreadIndex(n), true); break; case 'd': case 'D': // 切换单双页(D)=> 触发工具条复选框 if (toolbarDouble) { toolbarDouble.click(); e.preventDefault(); } break; case 's': case 'S': // 切换首页单页(S)=> 触发工具条复选框 if (toolbarFirst) { toolbarFirst.click(); e.preventDefault(); } break; case 'q': case 'Q': // 打开/关闭设置(Q) if (modal.classList.contains('show')) modal.classList.remove('show'); else { if (cfg.autoHideToolbar) toolbar.classList.add('show'); modal.classList.add('show'); } break; case 'w': case 'W': // 切换显示页码(W) cfg.showPN = !cfg.showPN; saveCfg(cfg); applyCfg(); render(true); break; case 'f': case 'F': fullscreenAnchor = { page: getAnchorPageIdx() }; if (!document.fullscreenElement) document.documentElement.requestFullscreen().catch(()=>{}); else document.exitFullscreen().catch(()=>{}); break; } }, { passive:false, capture:true }); document.addEventListener('fullscreenchange', ()=>{ // 若未预先记录,则现场记录当前页作为锚点 if (!fullscreenAnchor) fullscreenAnchor = { page: getAnchorPageIdx() }; const page = fullscreenAnchor?.page; fullscreenAnchor = null; // 重新布局并滚回对应跨页 render(true); if (page != null) { const si = pageToSpreadIndex(page); setTimeout(()=> scrollToSpread(si, false), 30); } }); // 双击页面:切换单双页 gallery.addEventListener('dblclick', (e)=>{ isDouble = !isDouble; cfg.double=isDouble; saveCfg(cfg); modal.querySelector('#tmk-double').checked = isDouble; render(true); }); // 点击半屏翻页 gallery.addEventListener('click', (e)=>{ if (modal.classList.contains('show')) return; if (e.target.closest('.tmk-toolbar')) return; if (cfg.clickNav === 'off') return; if ((e.target.closest('a,button,label,select,input,textarea'))) return; const vw = window.innerWidth, vh = window.innerHeight; const x = e.clientX, y = e.clientY; if (cfg.clickNav === 'lr') { if (x < vw/2) scrollToSpread(nearestSpread()-1, true); else scrollToSpread(nearestSpread()+1, true); e.preventDefault(); } else if (cfg.clickNav === 'ud') { if (y < vh/2) scrollToSpread(nearestSpread()-1, true); else scrollToSpread(nearestSpread()+1, true); e.preventDefault(); } }); // 一屏滚动:滚轮=>按跨页前后;是否平滑由 cfg.snapSmooth 控制 let lastWheel = 0; function wheelHandler(e){ const now = Date.now(); if (now - lastWheel < 300) { e.preventDefault(); return; } // 节流 lastWheel = now; if (Math.abs(e.deltaY) < 3) return; // 触控板小抖动忽略 if (e.deltaY > 0) scrollToSpread(nearestSpread()+1, !!cfg.snapSmooth); else scrollToSpread(nearestSpread()-1, !!cfg.snapSmooth); e.preventDefault(); } function bindSnapWheel(on){ window.removeEventListener('wheel', wheelHandler, { passive:false }); if (on) window.addEventListener('wheel', wheelHandler, { passive:false }); } // ========== 队列加载(顺序优先+限速+超时+指数退避) ========== function updateProgress(){ if (progressEl) progressEl.textContent = `加载 ${loadedCount}/${totalPages}`; } function rejudgeWide(){ for (let i=1;i<=totalPages;i++) if (dims[i]) isWide[i] = (dims[i].w/dims[i].h)>=cfg.crossAspect; } function backoff(attempt){ const base=500; return Math.min(30000, base*Math.pow(2,Math.max(0,attempt-1))) + Math.random()*300; } function pickNextIndex(){ const now=Date.now(); for (let i=1;i<=totalPages;i++){ if (state[i]===0 && now >= (nextTime[i]||0)) return i; } return 0; } function startLoad(i){ state[i]=1; attempts[i]++; active++; const url=urls[i]; const toMs=Math.max(3000, TIMEOUT_SEC*1000); let done=false, timer=null; const onDone=(ok,w,h)=>{ if (done) return; done=true; active--; clearTimeout(timer); const slot = document.querySelector(`.page-slot[data-idx="${i}"]`); if (ok){ if (state[i]!==2){ state[i]=2; loadedCount++; updateProgress(); if (w && h){ dims[i]={w,h}; const was = isWide[i]; const nowFlag = (w/h)>=cfg.crossAspect; isWide[i]=nowFlag; if (was!==nowFlag){ render(true); return; } } const img = slot?.querySelector('img[data-idx]'); if (img && !img.src) img.src = url; slot?.classList.add('loaded'); } } else { state[i]=0; nextTime[i]=Date.now()+backoff(attempts[i]); const txt = slot?.querySelector('.ph .txt'); if (txt) txt.textContent = `重试中…(#${attempts[i]})`; } tick(); }; const tmp = new Image(); tmp.onload = ()=> onDone(true, tmp.naturalWidth||0, tmp.naturalHeight||0); tmp.onerror = ()=> onDone(false); timer = setTimeout(()=> onDone(false), toMs); tmp.src = url; } function tick(){ while (active < CONCURRENCY){ const idx = pickNextIndex(); if (!idx) break; startLoad(idx); } if (loadedCount === totalPages) updateProgress(); } // 初始应用与渲染 applyCfg(); updateProgress(); render(false); // 滚动时更新进度/保存位置/章节末提示 window.addEventListener('scroll', ()=> { updateSpreadProgress(); checkEndHint(); }, { passive:true }); // 一屏滚动初始绑定 bindSnapWheel(cfg.snapWheel); } })();