Manhuagui 阅读增强 · 全量加载/沉浸式双页排版/自动跨页显示

漫画柜双页浏览,全屏显示,从右到左显示。横图跨页;点击半屏翻页;记忆进度;

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==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="← / →:上一/下一页&#10;Space / Enter:下一页&#10;D:切换单双页&#10;S:切换页面奇偶&#10;F:全屏开/关&#10;Q:打开/关闭设置面板&#10;W:切换显示页码&#10;E:切换‘右到左’方向&#10;G:跳转到页码&#10;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);
  }

})();