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