Greasy Fork is available in English.
覆盖原站,提供左右分栏与垂直两种阅读模式;支持RTL/LTR、封面单页、间距、键盘导航;分栏支持按高度/宽度适配;垂直支持百分比缩放(滑条/按钮/触控捏合/Ctrl+滚轮)。
当前为
// ==UserScript==
// @name 漫画人阅读器
// @namespace https://tampermonkey.net/
// @version 2.0.0
// @description 覆盖原站,提供左右分栏与垂直两种阅读模式;支持RTL/LTR、封面单页、间距、键盘导航;分栏支持按高度/宽度适配;垂直支持百分比缩放(滑条/按钮/触控捏合/Ctrl+滚轮)。
// @author you
// @match https://www.manhuaren.com/m*/
// @match https://www.manhuaren.com/m*/*
// @run-at document-idle
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ----------------------- utils -----------------------
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
const on = (el, ev, fn, opt) => el && el.addEventListener(ev, fn, opt);
const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
const store = {
get(k, v) { try { return JSON.parse(localStorage.getItem(k)); } catch { return v; } },
set(k, v) { localStorage.setItem(k, JSON.stringify(v)); }
};
// ----------------------- state -----------------------
const state = {
pages: [], // 原始页URL
mode: store.get('tm_dpr_mode', 'spread'), // 'spread' | 'vertical'
pairIndex: 0, // 当前双页索引(spread)
rtl: store.get('tm_dpr_rtl', true),
fitSpread: store.get('tm_dpr_fitSpread', 'height'), // 'height' | 'width'
gap: store.get('tm_dpr_gap', 16), // 间距 px
firstSingle: store.get('tm_dpr_firstSingle', true), // 封面单页
zoom: store.get('tm_dpr_zoom', 100), // 垂直模式百分比
};
const save = () => {
store.set('tm_dpr_mode', state.mode);
store.set('tm_dpr_rtl', state.rtl);
store.set('tm_dpr_fitSpread', state.fitSpread);
store.set('tm_dpr_gap', state.gap);
store.set('tm_dpr_firstSingle', state.firstSingle);
store.set('tm_dpr_zoom', state.zoom);
};
// ----------------------- image collection (保留原逻辑) -----------------------
async function collectImages(maxWaitMs = 15000) {
const start = Date.now();
let imgs = [];
while (Date.now() - start < maxWaitMs) {
if (window.newImgs && Array.isArray(window.newImgs) && window.newImgs.length) {
imgs = window.newImgs.slice();
break;
}
const inDom = $$('#cp_img img')
.map(n => n.getAttribute('data-src') || n.src)
.filter(Boolean);
if (inDom.length > 0) {
imgs = inDom;
if (!(window.newImgs && window.newImgs.length)) {
await new Promise(r => setTimeout(r, 200));
if (window.newImgs && window.newImgs.length) imgs = window.newImgs.slice();
}
break;
}
await new Promise(r => setTimeout(r, 150));
}
imgs = imgs.map(s => s.replace(/^\/\//, location.protocol + '//')).filter(Boolean);
imgs = Array.from(new Set(imgs));
return imgs;
}
// ----------------------- css -----------------------
const css = `
:root{
--tm-gap:16px;
--tm-zoom:100; /* 用于垂直模式百分比缩放 */
--tm-bar-h:56px;
--tm-bg:#0b0b0b; --tm-fg:#eaeaea;
--tm-surface:rgba(255,255,255,0.06);
--tm-surface-2:rgba(255,255,255,0.12);
--tm-border:rgba(255,255,255,0.15);
}
html, body { overflow:hidden!important; }
body.tm-dpr-active > *:not(#tm-dpr-root){ display:none !important; }
#tm-dpr-root{
position:fixed; inset:0; z-index:2147483647;
background:linear-gradient(180deg,#090909,#0f0f10 40%,#0a0a0a);
color:var(--tm-fg); font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,PingFang SC,Hiragino Sans GB,Microsoft Yahei,Helvetica Neue,Noto Sans,Arial,sans-serif;
}
#tm-dpr-root *{ box-sizing:border-box; }
#tm-bar{
position:fixed; top:0; left:0; right:0; height:var(--tm-bar-h);
display:flex; align-items:center; gap:10px; padding:8px 12px;
background:rgba(0,0,0,0.48); backdrop-filter: blur(8px);
border-bottom:1px solid var(--tm-surface-2);
}
#tm-bar .seg, #tm-bar .btn, #tm-bar select, #tm-bar input[type="range"]{
height:36px; border-radius:10px; border:1px solid var(--tm-border);
background:var(--tm-surface); color:var(--tm-fg); font-size:12px;
}
#tm-bar .btn{ padding:0 12px; cursor:pointer; line-height:34px; }
#tm-bar .btn:hover{ background:rgba(255,255,255,0.1); }
#tm-bar .seg{ display:inline-flex; overflow:hidden; }
#tm-bar .seg .btn{ border:none; border-right:1px solid var(--tm-surface-2); border-radius:0; }
#tm-bar .seg .btn:last-child{ border-right:none; }
#tm-bar .label{ font-size:12px; opacity:.85; margin:0 4px 0 8px; white-space:nowrap; }
#tm-bar .sp{flex:1;}
#tm-bar .icon{ width:16px; height:16px; vertical-align:-3px; opacity:.85; }
#tm-stage{
position:absolute; top:var(--tm-bar-h); left:0; right:0; bottom:0;
}
/* Spread(左右分栏) */
#stage-spread{
position:absolute; inset:0; display:flex; align-items:center; justify-content:center; gap:var(--tm-gap); overflow:hidden; padding:12px;
}
#stage-spread.fit-height img.page{ max-height: calc(100vh - var(--tm-bar-h) - 24px); width:auto; height:auto; }
#stage-spread.fit-width{ align-items:flex-start; overflow:auto; }
#stage-spread.fit-width img.page{ width: calc((100vw - var(--tm-gap) - 24px) / 2); height:auto; }
.page-wrap{ position:relative; display:flex; align-items:center; justify-content:center; min-width:0; }
img.page{ display:block; max-width:100%; object-fit:contain; user-select:none; -webkit-user-drag:none; background:#111; border-radius:8px; box-shadow:0 10px 30px rgba(0,0,0,.35); }
.badge{ position:absolute; bottom:8px; right:8px; font-size:12px; background:rgba(0,0,0,0.55); padding:2px 6px; border-radius:6px; border:1px solid var(--tm-surface-2); }
.page-left .badge{ left:8px; right:auto; }
/* 点击半屏翻组 */
#spread-overlay{ position:absolute; inset:0; display:grid; grid-template-columns:1fr 1fr; }
#spread-overlay .zone{ cursor:pointer; }
#spread-overlay .zone:active{ background:rgba(255,255,255,0.03); }
/* Vertical(垂直) */
#stage-vertical{
position:absolute; inset:0; overflow:auto; padding:16px 12px 24px;
--tm-gap-vert: var(--tm-gap);
}
.v-item{ position:relative; margin: 0 auto var(--tm-gap-vert) auto; max-width: min(1800px, 100%); }
.v-item img{ display:block; width: calc(var(--tm-zoom) * 1%); height:auto; max-width:none; margin:0 auto; object-fit:contain; user-select:none; -webkit-user-drag:none; border-radius:8px; box-shadow:0 10px 30px rgba(0,0,0,.35); background:#111; }
.v-pageno{ position:absolute; right:8px; bottom:8px; font-size:12px; background:rgba(0,0,0,0.55); padding:2px 6px; border-radius:6px; border:1px solid var(--tm-surface-2); }
/* 小提示 */
#tm-help{ position:fixed; right:12px; top:calc(var(--tm-bar-h) + 8px);
display:none; max-width:420px; padding:12px; line-height:1.65;
background:rgba(0,0,0,0.65); border:1px solid var(--tm-surface-2); border-radius:12px; font-size:12px;
}
#tm-help.visible{ display:block; }
/* 小控件 */
.range{ appearance:none; height:36px; width:160px; vertical-align:middle; background:var(--tm-surface); }
.range::-webkit-slider-thumb{ -webkit-appearance:none; appearance:none; width:14px; height:14px; border-radius:50%; background:#fff; box-shadow:0 0 0 2px rgba(0,0,0,.3); cursor:pointer; }
/* 禁止长按选择,优化触控 */
#tm-dpr-root, #tm-dpr-root *{ -webkit-touch-callout:none; -webkit-tap-highlight-color: transparent; }
`;
// ----------------------- UI -----------------------
function icon(svgPath) {
return `<svg class="icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="${svgPath}"/></svg>`;
}
const ICONS = {
spread: "M3 5h8v14H3V5zm10 0h8v14h-8V5z",
vertical: "M5 3h14v4H5V3zm0 7h14v4H5v-4zm0 7h14v4H5v-4z",
rtl: "M10 6h8v2h-8v2l-4-3 4-3v2zM6 16h12v2H6v-2z",
ltr: "M14 6H6v2h8v2l4-3-4-3v2zM6 16h12v2H6v-2z",
height: "M13 4h-2v4H8l4 4 4-4h-3V4zm-2 12h2v4h3l-4 4-4-4h3v-4z",
width: "M4 11v2h4v3l4-4-4-4v3H4zm16 2v-2h-4V8l-4 4 4 4v-3h4z",
minus: "M5 11h14v2H5z",
plus: "M5 11h14v2H5V11zm7-7h2v6h-2V4zM11 14h2v6h-2v-6z", // 实际显示只留减/加两组
chapterPrev: "M15 6l-6 6 6 6V6z",
chapterNext: "M9 18l6-6-6-6v12z",
help: "M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 17c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7zm-1-5h2c0-3 3-3.25 3-5a3 3 0 10-6 0h2a1 1 0 112 0c0 1.25-3 1.5-3 5z",
};
function buildUI() {
const root = document.createElement('div');
root.id = 'tm-dpr-root';
root.innerHTML = `
<div id="tm-bar">
<span class="label">模式</span>
<div class="seg" id="seg-mode">
<button class="btn" data-mode="spread">${icon(ICONS.spread)} 双页</button>
<button class="btn" data-mode="vertical">${icon(ICONS.vertical)} 垂直</button>
</div>
<span class="label">方向</span>
<div class="seg" id="seg-dir">
<button class="btn" data-dir="rtl">${icon(ICONS.rtl)} 右→左</button>
<button class="btn" data-dir="ltr">${icon(ICONS.ltr)} 左→右</button>
</div>
<span class="label" id="lbl-fit">适配</span>
<div class="seg" id="seg-fit">
<button class="btn" data-fit="height">${icon(ICONS.height)} 按高度</button>
<button class="btn" data-fit="width">${icon(ICONS.width)} 按宽度</button>
</div>
<span class="label">间距</span>
<div class="seg" id="seg-gap">
<button class="btn" data-gap="0">0</button>
<button class="btn" data-gap="8">8</button>
<button class="btn" data-gap="16">16</button>
<button class="btn" data-gap="24">24</button>
<button class="btn" data-gap="32">32</button>
</div>
<label class="label"><input type="checkbox" id="ck-first" style="vertical-align:-2px;margin-right:6px;">封面单页</label>
<span class="label" id="lbl-zoom" style="display:none;">缩放</span>
<button class="btn" id="zoom-dec" style="display:none;">${icon(ICONS.minus)}</button>
<input class="range" type="range" id="zoom-range" min="40" max="240" step="5" style="display:none;">
<button class="btn" id="zoom-inc" style="display:none;">${icon(ICONS.plus)}</button>
<span class="label" id="zoom-val" style="display:none;min-width:54px;text-align:right;">100%</span>
<div class="sp"></div>
<button class="btn" id="btn-prev">上一组 ←</button>
<button class="btn" id="btn-next">下一组 →</button>
<button class="btn" id="btn-prev-ch">${icon(ICONS.chapterPrev)} 上一章</button>
<button class="btn" id="btn-next-ch">${icon(ICONS.chapterNext)} 下一章</button>
<button class="btn" id="btn-help">${icon(ICONS.help)} 帮助</button>
</div>
<div id="tm-stage">
<!-- Spread -->
<div id="stage-spread" class="fit-height" style="--tm-gap:${state.gap}px">
<div class="page-wrap page-left"><img class="page" id="img-left" /></div>
<div class="page-wrap page-right"><img class="page" id="img-right" /></div>
<div id="spread-overlay"><div class="zone" id="zone-left"></div><div class="zone" id="zone-right"></div></div>
</div>
<!-- Vertical -->
<div id="stage-vertical" style="--tm-gap:${state.gap}px; --tm-zoom:${state.zoom}; display:none;"></div>
</div>
<div id="tm-help"></div>
`;
document.body.appendChild(root);
document.body.classList.add('tm-dpr-active');
// 初始按钮状态
$('#ck-first').checked = !!state.firstSingle;
$('#zoom-range').value = state.zoom;
$('#zoom-val').textContent = `${state.zoom}%`;
updateModeButtons(); updateDirButtons(); updateFitButtons(); updateGapButtons();
toggleZoomUI(state.mode === 'vertical');
// 绑定交互
on($('#seg-mode'), 'click', e => {
const btn = e.target.closest('.btn'); if (!btn) return;
state.mode = btn.dataset.mode; save();
updateModeButtons(); toggleZoomUI(state.mode === 'vertical'); renderMode();
});
on($('#seg-dir'), 'click', e => {
const btn = e.target.closest('.btn'); if (!btn) return;
state.rtl = (btn.dataset.dir === 'rtl'); save(); updateDirButtons(); renderSpread(); // 仅影响spread布局/点击方向
});
on($('#seg-fit'), 'click', e => {
const btn = e.target.closest('.btn'); if (!btn) return;
state.fitSpread = btn.dataset.fit; save(); updateFitButtons(); renderSpread();
});
on($('#seg-gap'), 'click', e => {
const btn = e.target.closest('.btn'); if (!btn) return;
state.gap = parseInt(btn.dataset.gap, 10); save(); updateGapButtons();
$('#stage-spread').style.setProperty('--tm-gap', state.gap + 'px');
$('#stage-vertical').style.setProperty('--tm-gap', state.gap + 'px');
});
on($('#ck-first'), 'change', (e) => { state.firstSingle = !!e.target.checked; state.pairIndex = 0; save(); renderSpread(); });
// 导航
on($('#btn-prev'), 'click', () => navPrev());
on($('#btn-next'), 'click', () => navNext());
// 半屏点击
on($('#zone-left'), 'click', () => state.rtl ? navNext() : navPrev());
on($('#zone-right'), 'click', () => state.rtl ? navPrev() : navNext());
// 章节跳转
const prevChUrl = findChapterUrl('上一章');
const nextChUrl = findChapterUrl('下一章');
$('#btn-prev-ch').disabled = !prevChUrl;
$('#btn-next-ch').disabled = !nextChUrl;
on($('#btn-prev-ch'), 'click', () => prevChUrl && (location.href = prevChUrl));
on($('#btn-next-ch'), 'click', () => nextChUrl && (location.href = nextChUrl));
// 帮助
$('#tm-help').innerHTML = helpHTML();
on($('#btn-help'), 'click', () => $('#tm-help').classList.toggle('visible'));
// 键盘
on(window, 'keydown', (ev) => {
const tag = (ev.target && ev.target.tagName) || '';
if (/INPUT|TEXTAREA|SELECT/.test(tag)) return;
const k = ev.key;
if (k === 'ArrowRight' || k === ' ') { ev.preventDefault(); navNext(); }
else if (k === 'ArrowLeft' || k === 'Backspace') { ev.preventDefault(); navPrev(); }
else if (k.toLowerCase() === 'r') { state.rtl = !state.rtl; save(); updateDirButtons(); renderSpread(); }
else if (k.toLowerCase() === 'f') { state.fitSpread = state.fitSpread === 'height' ? 'width' : 'height'; save(); updateFitButtons(); renderSpread(); }
else if (k.toLowerCase() === 'm') { state.mode = state.mode === 'spread' ? 'vertical' : 'spread'; save(); updateModeButtons(); toggleZoomUI(state.mode === 'vertical'); renderMode(); }
else if (k.toLowerCase() === 's') { state.firstSingle = !state.firstSingle; $('#ck-first').checked = state.firstSingle; save(); state.pairIndex = 0; renderSpread(); }
else if (k.toLowerCase() === 'g') { cycleGap(); }
else if (k.toLowerCase() === 'h') { $('#tm-help').classList.toggle('visible'); }
else if ((k === '+' || k === '=') && state.mode === 'vertical') { ev.preventDefault(); setZoom(state.zoom + 5); }
else if ((k === '-' || k === '_') && state.mode === 'vertical') { ev.preventDefault(); setZoom(state.zoom - 5); }
else if ((k === '0') && state.mode === 'vertical') { ev.preventDefault(); setZoom(100); }
});
// 垂直缩放控件
on($('#zoom-range'), 'input', e => setZoom(parseInt(e.target.value, 10)));
on($('#zoom-dec'), 'click', () => setZoom(state.zoom - 5));
on($('#zoom-inc'), 'click', () => setZoom(state.zoom + 5));
// Ctrl+滚轮 缩放(Chrome/Edge/FF)
on($('#stage-vertical'), 'wheel', (ev) => {
if (!ev.ctrlKey) return; // 仅拦截缩放手势
ev.preventDefault();
setZoom(state.zoom + (ev.deltaY < 0 ? 5 : -5));
}, { passive: false });
// 触控捏合缩放(垂直)
let pinch = { active:false, startDist:0, startZoom: state.zoom };
const vert = $('#stage-vertical');
on(vert, 'touchstart', (e) => {
if (e.touches.length === 2) {
pinch.active = true;
pinch.startDist = dist2(e.touches[0], e.touches[1]);
pinch.startZoom = state.zoom;
}
}, { passive: true });
on(vert, 'touchmove', (e) => {
if (pinch.active && e.touches.length === 2) {
const d = dist2(e.touches[0], e.touches[1]);
const scale = d / (pinch.startDist || d);
const z = Math.round(pinch.startZoom * scale);
setZoom(z, /*silent*/true); // 实时但不打断性能
}
}, { passive: true });
on(vert, 'touchend', () => { if (pinch.active) { pinch.active = false; setZoom(state.zoom); } });
function dist2(a,b){ const dx=a.clientX-b.clientX, dy=a.clientY-b.clientY; return Math.hypot(dx,dy); }
}
function updateModeButtons(){
$$('#seg-mode .btn').forEach(b => b.style.opacity = b.dataset.mode === state.mode ? '1' : '0.65');
// 控制面板/按钮可见性
$('#seg-dir').style.display = state.mode === 'spread' ? '' : '';
$('#seg-fit').style.display = state.mode === 'spread' ? '' : 'none';
$('#lbl-fit').style.display = state.mode === 'spread' ? '' : 'none';
// “上一组/下一组”在垂直模式下作为滚动快捷键仍保留
}
function updateDirButtons(){ $$('#seg-dir .btn').forEach(b => b.style.opacity = b.dataset.dir === (state.rtl ? 'rtl':'ltr') ? '1':'0.65'); }
function updateFitButtons(){
$$('#seg-fit .btn').forEach(b => b.style.opacity = b.dataset.fit === state.fitSpread ? '1':'0.65');
const s = $('#stage-spread');
s.classList.toggle('fit-height', state.fitSpread === 'height');
s.classList.toggle('fit-width', state.fitSpread === 'width');
}
function updateGapButtons(){
$$('#seg-gap .btn').forEach(b => b.style.opacity = (parseInt(b.dataset.gap,10) === state.gap) ? '1':'0.65');
}
function cycleGap(){
const steps = [0,8,16,24,32]; let i = steps.indexOf(state.gap); i = (i+1) % steps.length;
state.gap = steps[i]; save(); updateGapButtons();
$('#stage-spread').style.setProperty('--tm-gap', state.gap + 'px');
$('#stage-vertical').style.setProperty('--tm-gap', state.gap + 'px');
}
function toggleZoomUI(show){
['lbl-zoom','zoom-dec','zoom-range','zoom-inc','zoom-val'].forEach(id => { const el = $('#'+id); if (el) el.style.display = show ? '' : 'none'; });
}
function setZoom(z, silent=false){
state.zoom = clamp(z, 40, 240);
$('#stage-vertical').style.setProperty('--tm-zoom', state.zoom);
$('#zoom-range').value = state.zoom;
$('#zoom-val').textContent = `${state.zoom}%`;
if (!silent) save();
}
function helpHTML(){
return `
<b>快捷键</b><br>
→ / 空格:下一组(或向下滚动)<br>
← / Backspace:上一组(或向上滚动)<br>
R:切换阅读方向(右→左 / 左→右)<br>
F:Spread模式切换适配(按高度 / 按宽度)<br>
M:切换模式(双页 / 垂直)<br>
S:切换“封面单页”<br>
G:循环调整间距<br>
H:显示/隐藏本帮助<br>
+ / - / 0:垂直模式放大 / 缩小 / 还原100%<br>
<hr style="border-color: rgba(255,255,255,0.12)">
触控:垂直模式两指捏合缩放;Spread模式点击左右半屏翻组。<br>
Tips:封面单页用于让后续跨页在正确的左右位置(常见于日漫)。`;
}
// ----------------------- data helpers -----------------------
function findChapterUrl(labelText) {
const a = $$('a').find(a => (a.textContent || '').trim() === labelText);
if (!a) return '';
const href = a.getAttribute('href') || '';
const m = href.match(/pushHistory\('([^']+)'\)/);
return m ? (location.origin + m[1]) : (href.startsWith('/') ? (location.origin + href) : '');
}
function pairsCount(){
const N = state.pages.length;
if (state.firstSingle) return 1 + Math.ceil((N - 1) / 2);
return Math.ceil(N / 2);
}
function getPair(idx){
const N = state.pages.length;
if (state.firstSingle){
if (idx === 0){
// 封面单页:显示在右侧更贴近漫画阅读
return { left:null, right: state.pages[0], leftNum:null, rightNum:1 };
}
const start = 1 + (idx - 1) * 2; // 2-3, 4-5, ...
let L = state.pages[start] || null; // 2,4,6...
let R = state.pages[start+1] || null; // 3,5,7...
let lNum = L ? start+1 : null, rNum = R ? start+2 : null;
// RTL 时,右侧应是较小的页码(阅读顺序右→左),交换
if (state.rtl){ [L, R] = [R || null, L || null]; [lNum, rNum] = [rNum, lNum]; }
return { left:L, right:R, leftNum:lNum, rightNum:rNum };
} else {
const start = idx * 2; // 1-2, 3-4, 5-6...
let L = state.pages[start] || null;
let R = state.pages[start+1] || null;
let lNum = L ? start+1 : null, rNum = R ? start+2 : null;
if (state.rtl){ [L, R] = [R || null, L || null]; [lNum, rNum] = [rNum, lNum]; }
return { left:L, right:R, leftNum:lNum, rightNum:rNum };
}
}
// ----------------------- rendering -----------------------
function renderMode(){
const spread = $('#stage-spread');
const vertical = $('#stage-vertical');
if (state.mode === 'spread'){
spread.style.display = '';
vertical.style.display = 'none';
renderSpread();
} else {
spread.style.display = 'none';
vertical.style.display = '';
renderVertical();
}
}
function renderSpread(){
const total = pairsCount();
state.pairIndex = clamp(state.pairIndex, 0, Math.max(0, total-1));
const {left, right, leftNum, rightNum} = getPair(state.pairIndex);
const L = $('#img-left'), R = $('#img-right');
if (left){ L.src = left; L.parentElement.style.visibility = 'visible'; }
else { L.removeAttribute('src'); L.parentElement.style.visibility = 'hidden'; }
if (right){ R.src = right; R.parentElement.style.visibility = 'visible'; }
else { R.removeAttribute('src'); R.parentElement.style.visibility = 'hidden'; }
setBadge($('.page-left'), leftNum);
setBadge($('.page-right'), rightNum);
// 预加载下一组
const nextIdx = state.pairIndex + 1;
if (nextIdx < total){
const n = getPair(nextIdx);
[n.left, n.right].filter(Boolean).slice(0,2).forEach(src => { const i=new Image(); i.src=src; });
}
}
function renderVertical(){
const cont = $('#stage-vertical');
if (!cont.dataset.built){
cont.innerHTML = state.pages.map((src, i) => `
<div class="v-item">
<img loading="lazy" decoding="async" referrerpolicy="no-referrer" src="${src}" />
<div class="v-pageno">第 ${i+1} 页</div>
</div>
`).join('');
cont.dataset.built = '1';
}
// 应用当前缩放
cont.style.setProperty('--tm-zoom', state.zoom);
}
function setBadge(wrap, num){
if (!wrap) return;
let b = $('.badge', wrap);
if (!b){ b = document.createElement('div'); b.className = 'badge'; wrap.appendChild(b); }
b.textContent = num ? `第 ${num} 页` : '';
}
// ----------------------- navigation -----------------------
function navNext(){
if (state.mode === 'spread'){
state.pairIndex++; renderSpread();
} else {
// 垂直:平滑向下滚动
const v = $('#stage-vertical');
v.scrollBy({ top: Math.max(100, window.innerHeight * 0.9), left:0, behavior:'smooth' });
}
}
function navPrev(){
if (state.mode === 'spread'){
state.pairIndex--; renderSpread();
} else {
const v = $('#stage-vertical');
v.scrollBy({ top: -Math.max(100, window.innerHeight * 0.9), left:0, behavior:'smooth' });
}
}
// ----------------------- boot -----------------------
function injectStyle(text){ const s=document.createElement('style'); s.id='tm-dpr-style'; s.textContent=text; document.head.appendChild(s); }
async function boot(){
// 仅在章节页运行:/m数字/
if (!/\/m\d+\/?$/.test(location.pathname)) return;
injectStyle(css);
buildUI();
const imgs = await collectImages(15000);
if (!imgs || !imgs.length){
alert('未能获取到章节图片。请刷新重试。');
return;
}
state.pages = imgs;
renderMode();
// 窗口调整重渲染
on(window, 'resize', () => { if (state.mode === 'spread') renderSpread(); });
}
setTimeout(boot, 0);
})();